import { type EccInstallPreviewArtifact } from "../../ecc/install-preview.js"; import { AihError } from "../../errors.js"; import { type PlanResult } from "../../internals/execute.js"; import { type Action } from "../../internals/plan.js"; import type { Runner } from "../../internals/proc.js"; import { type FrameworkAdapter, type ProvisionResult } from "../adapter.js"; import { type ClaudeDriftEntry, type PluginCacheLocator, type PluginScope } from "../hosts/claude/index.js"; import { type BindingOwnershipEntry } from "../lock.js"; import { type ResolvedGitSource } from "../scan-gate.js"; /** * The ECC **Lean** `FrameworkAdapter` (W4b) — the FIRST D6 * `adapterType: "upstream-local-installer"`. It is the BINDING orchestration * layer around ECC's own selective installer: it does NOT re-implement any ECC * install machinery (D9 — the upstream installer is INVOKED, never reimplemented), * it drives the installer through an injected seam and records BINDING state in * the W2 lock. W4c adds the ECC **Full** variant to THIS file (below the `== ECC * Full ==` banners): explicit opt-in (`mode: "full"`), the PROJECT-SCOPE PLUGIN * path — Full keeps the file's declared `adapterType` but internally composes the * SAME W3 host services `superpowers.ts` uses (`bindPlugin` marketplace/install + * D7 subtree identity), adds the two mandatory D18 project-scoped state-root env * fields, owns each individually-selected `mcp:` in `.mcp.json`, and * feeds a static state-write inventory into the strict/lax LABEL DECISION. The * mode switch on `declaration.framework.mode` ({@link eccMode}) routes lean/full, * and {@link assertModeMatchesExistingLock} keeps the two mutually exclusive. * * D10 (the locked binding ruling this implements EXACTLY): * ECC's upstream `minimal` profile is NOT Lean — minimal ships `workflow-quality` * (`baseline:workflow`, which bundles `continuous-learning`/`continuous-learning-v2` * and `browser-qa`) and all-language stack coverage. ECC Lean (the default mode, * `mode: "lean"` or absent) is an EXACT COMPONENT ALLOWLIST executed through ECC's * OWN selective installer against the pinned commit: * - ALLOWED: common rules, planning, TDD, debugging, review, security review, * verification ({@link ECC_LEAN_ALLOWLIST}). * - EXPLICITLY EXCLUDED: `hooks-runtime` (`baseline:hooks`), `continuous-learning` * (`skill:continuous-learning`), `continuous-learning-v2` (bundled in * `baseline:workflow`), MCP (`mcp:*`), browser/runtime automation * (`capability:*`, `browser-qa`), auto-update (`baseline:commands`/ * `baseline:platform`), and unrelated language/framework skills * ({@link ECC_LEAN_EXCLUDED}). * - `provision` runs the upstream PREVIEW/PLAN from the exact scanned commit (the * pin-bound {@link EccInstallPreviewArtifact}) and DIFFS its output against the * allowlist BEFORE applying; ANY mismatch (an extra runtime surface an * allowlisted component would install, or an allowlisted component the preview * cannot deliver) FAILS THE BIND with {@link EccLeanAllowlistError} — nothing * applied, the installer seam is never called. * - Post-install verification checks the ABSENCE of runtime surfaces: no hooks * entries, no MCP servers, no writes under `~/.claude/skills/learned`. * * How the existing `src/ecc` seams are composed (READ-only reuse, no duplication): * - the PREVIEW is the shipped, catalog-pin-bound `ecc-install-preview.json` * (`readEccInstallPreview`/`parseEccInstallPreview` from `src/ecc/install-preview.ts`) * — that artifact IS ECC's upstream installer plan output, materialized at * release from the exact pinned commit and asserted bound to the ECC baseline * catalog (`baselineCatalogById("ecc")`), so re-running upstream JS here would * be redundant and would require the checkout's own dependencies; * - the selective INSTALL is driven through the injected {@link EccLeanInstaller} * exec/Runner seam. A real evidence-gated install (ECC's `executeEccEvidencePipeline` * / `verifiedEccInstallPlan`) runs the upstream installer against the pinned * checkout under `npm ci` — a REAL host mutation — and therefore only runs in * the orchestrator-triggered acceptance phase; the default installer here fails * closed with {@link EccLeanInstallerUnavailableError} and unit tests inject a * fixture installer (fakeRunner + mkdtemp home). See the acceptance `it.skip` * in the test file for the manual real-installer procedure. * * D6 method mapping (mirrors `superpowers.ts`): * - `inspect`: cheap static notes over a checkout tree (no network, no CLI). * - `resolve`: delegates to `resolveGitSource` with the declaration's git source. * - `plan`: PURE preview (D8 + feature-key checks + mode routing, then the * allowlist, the installer's expected writes, and D18 ownership intents — no * disk write). * - `provision`: preview-diff allowlist gate -> selective install -> capture * writes -> allowlisted-only + runtime-surface-absence verification -> atomic * `BindingLock`. * - `verify`: lock present + installed files still match recorded digests + a * runtime-surface absence re-check. * - `remove`: SYNC plan-only (partition home-scoped vs repo-relative ownership; * `planClaudeRemoval` over the repo-relative subset). See {@link EccLeanRemoveResult}. * - `report`: Framework Card input lines (framework, mode, pin, allowlist, * exclusions, runtime-surface absence attestation, labeled context-cost estimate). */ /** Adapter-local fail-closed error: wrong framework routed here, or a non-git source/resolution. */ export declare class EccBindingError extends AihError { constructor(message: string); } /** * Fail-closed guard for a mode value outside {@link EccMode} — unreachable through * the declaration schema (which restricts `mode` to `lean`/`full`), kept as defense * in depth so a widened schema can never silently route an unknown mode. W4b used * this to stub Full; W4c implements Full, so the only remaining throw site is the * impossible-mode branch in {@link eccMode}. */ export declare class EccModeNotImplementedError extends AihError { constructor(message: string); } /** * D10 point 5 — Lean and Full are MUTUALLY EXCLUSIVE per project. Thrown (typed, * fail-closed) when a `plan`/`provision` for one mode meets an existing lock that * records the OTHER mode, in BOTH directions (Full over a Lean lock, Lean over a * Full lock). Re-binding the SAME mode is allowed. The check lives in the shared * mode-routing area ({@link assertModeMatchesExistingLock}) so both directions are * covered from one place. */ export declare class EccModeConflictError extends AihError { constructor(message: string); } /** * The pin-bound preview would NOT install exactly the Lean allowlist — an extra * runtime surface an allowlisted component drags in, an allowlisted component the * preview cannot deliver, or (post-install) a stray/runtime file. The bind fails * closed; nothing is applied. */ export declare class EccLeanAllowlistError extends AihError { constructor(message: string); } /** * No {@link EccLeanInstaller} was injected. The default installer refuses to run: * a real ECC selective install mutates the machine host (upstream `npm ci` + the * pinned installer JS), which only happens in the orchestrator-triggered * acceptance phase — the CLI/acceptance layer wires a real installer around ECC's * `executeEccEvidencePipeline`; unit tests inject a fixture installer. */ export declare class EccLeanInstallerUnavailableError extends AihError { constructor(message: string); } /** * affaan-m/ecc pin — the maintainer-locked commit this binding is pinned to. * MIRRORS (deliberately does not import) the "affaan-m"/"ecc" entry in * `src/internals/baseline-sources.ts` — also the pin of the shipped * `src/baseline-evidence/ecc-install-preview.json` and of * `baselineCatalogById("ecc").pinnedSha`. Mirrored rather than imported for the * same two reasons as `SUPERPOWERS_PIN_COMMIT` in `superpowers.ts`: (1) importing * would pull the ECC baseline-evidence catalog module into the binding path just * to read one string; (2) the binding pin is a DELIBERATE, independently-reviewed * value — if the shared baseline pin moves, this constant must be updated * explicitly in the same review, never silently follow. The default preview * source cross-checks this constant against the live catalog, so a silent drift * fails closed rather than binding the wrong commit. */ export declare const ECC_PIN_COMMIT = "623f2c020f052319657674e4e6c29ab5d0ad566b"; /** * The ECC adapter version (W7 §C.2) — bumped when this adapter's provisioning / * qualification logic changes. Keyed into the runtime-qualification cache * (`scan-cache-tiers.ts` `runtimeQualKey`), so a bump re-keys every prior host * qualification (a cache miss / recompute). Registered alongside the factory in * `registry.ts` (`ADAPTER_VERSIONS`). One version covers both ECC modes (lean/full). */ export declare const ADAPTER_VERSION: 1; /** The pinned git source location (`owner/repo` shape; see `isPlausibleGitRepository`). */ export declare const ECC_REPOSITORY = "affaan-m/ecc"; /** The single ECC install target for the `claude` host (the preview's `target`). */ export declare const ECC_HOST_TARGET = "claude"; /** * The ECC Lean EXACT COMPONENT ALLOWLIST — the only ECC components a Lean bind * installs (D10). Every id resolves to ≥1 clean (non-runtime) operation in the * pinned preview for the `claude` target; the provision gate fails closed if any * cannot be delivered or if any drags in a runtime surface. * * Mapping to the D10 ALLOWED prose: * - common rules → `baseline:rules` * - planning → `agent:planner` * - TDD → `skill:tdd-workflow`, `agent:tdd-guide` * - debugging → `agent:build-error-resolver` * - review → `agent:code-reviewer` * - security review → `agent:security-reviewer`, `skill:security-review` * - verification → `skill:verification-loop` */ export declare const ECC_LEAN_ALLOWLIST: readonly string[]; /** * The explicitly-EXCLUDED ECC surfaces (D10). Human-readable, for the Framework * Card `report`; the enforcement is the allowlist gate + the runtime-surface * matchers ({@link eccRuntimeSurfaceHit}), not this list. */ export declare const ECC_LEAN_EXCLUDED: readonly string[]; /** Route input: absent or `"lean"` -> Lean; `"full"` -> Full. */ export type EccMode = "lean" | "full"; /** * The plugin name AIH installs for ECC Full. Full is EXPLICIT OPT-IN * (`mode: "full"`) and binds through the SAME W3 host services `superpowers.ts` * uses — the scanned checkout is registered as a marketplace and the plugin is * installed at PROJECT scope (`bindPlugin`, D7 subtree identity). The file's * declared `adapterType` (`upstream-local-installer`) is unchanged; Full internally * uses host-plugin mechanics (D10). */ export declare const ECC_FULL_PLUGIN_NAME = "ecc"; /** The marketplace name the PINNED ECC checkout's own manifest declares. The * claude host registers a marketplace under the manifest's name — never a * registrar-chosen one — so this mirrors the manifest at {@link ECC_PIN_COMMIT}; * `bindPlugin` asserts the match before any host mutation (W4 live-run * correction). */ export declare const ECC_FULL_MARKETPLACE_NAME = "ecc"; /** Default project-local path for `ECC_AGENT_DATA_HOME` (repo-relative, gitignored `.aih/`). */ export declare const ECC_AGENT_DATA_HOME_DEFAULT = ".aih/ecc/agent-data"; /** Default project-local path for `CLV2_HOMUNCULUS_DIR` (repo-relative, gitignored `.aih/`). */ export declare const ECC_HOMUNCULUS_DIR_DEFAULT = ".aih/ecc/homunculus"; /** * The ECC Full MCP CONNECTOR ALLOWLIST — the connector ids a Full bind may select, * one per `mcp:` declaration `features` key (D10 point 3). * * MIRRORS (deliberately does not import) the resolvable subset of * `EXPLICIT_MCP_COMPONENTS` in `src/ecc/components.ts`, as consumed by * `selectedEccMcpServers`/`orgAllowedEccMcpComponents` in `src/ecc/mcp.ts` — those * resolve each `mcp:` against the validated `mcpServers()` catalog in * `src/mcp/servers.ts`. Mirrored rather than imported for the same reasons as the * pin constants: (1) the allowlist is a DELIBERATE, independently-reviewed value — * if the upstream catalog grows a connector, adding it here is a reviewed change, * never a silent follow; (2) it keeps the binding path from depending on the ECC * component-selection module just to read a list. `exa` is intentionally OMITTED: * `EXPLICIT_MCP_COMPONENTS` lists `mcp:exa`, but `src/mcp/servers.ts` ships no * validated `exa` config, so `selectedEccMcpServers(["mcp:exa"])` would throw — a * connector with no config can never be a bindable selection. A test asserts every * id here resolves through `selectedEccMcpServers`, so a drift fails closed. */ export declare const ECC_FULL_MCP_CONNECTORS: readonly string[]; /** The Full known-feature-key list: exactly one `mcp:` key per allowlisted connector. */ export declare const ECC_FULL_FEATURE_KEYS: readonly string[]; /** A preview operation normalized to a scope + a POSIX path relative to that scope. */ export interface NormalizedEccOp { componentId: string; scope: "home" | "project"; /** POSIX path relative to `` or ``. */ rel: string; kind: string; } /** The allowlist-vs-preview verdict computed BEFORE any install runs. */ export interface EccLeanPreviewDiff { ok: boolean; /** Allowlisted components with no deliverable operation in the pinned preview. */ missing: string[]; /** Allowlisted operations whose destination hits a forbidden runtime surface. */ runtimeEscapes: string[]; /** Component ids the preview can deliver for the target. */ deliverable: string[]; /** Every operation whose component is in the allowlist. */ selectedOps: NormalizedEccOp[]; } /** Normalize every preview operation for a single install target to a scope + relative path. */ export declare function normalizeEccOperations(artifact: EccInstallPreviewArtifact, target?: string): NormalizedEccOp[]; /** * Runtime-surface matcher (D10 absence rule). Returns a surface label when a * scope-relative path lands on hooks-runtime, continuous-learning output, learned * skills, an MCP registration, or a Claude SETTINGS file — otherwise `undefined`. * Used both as the pre-install "extra surface" gate and the post-install / verify * absence check. * * A `.claude/settings.json` / `.claude/settings.local.json` write is out-of-allowlist * for Lean in EITHER scope: those files carry hook ENTRIES, `enabledPlugins`, `env`, * and `skillOverrides` — none of which a rules/skills/agents-only Lean bind may touch. * The plain path-based hooks/MCP checks miss them (the runtime lives INSIDE the JSON, * not in a `hooks/` directory), so they are matched here explicitly. */ export declare function eccRuntimeSurfaceHit(scope: "home" | "project", rel: string): string | undefined; /** * Diff the pinned preview against the Lean allowlist. This is the D10 gate output: * `missing` (an allowlisted component the preview cannot deliver) and * `runtimeEscapes` (an allowlisted component that would install a runtime surface) * each make `ok` false. Pure — no I/O, no Runner. */ export declare function computeEccLeanPreviewDiff(artifact: EccInstallPreviewArtifact, allowlist?: readonly string[], target?: string): EccLeanPreviewDiff; export interface EccLeanManifest { /** Sorted, deduped `:` entries. */ entries: string[]; /** The digest text (a source-pin header + the sorted entries). */ text: string; /** sha256 of {@link text}. */ digest: string; } /** * The install manifest digest — the D7 identity for an installer adapter. Both * `scannedDigest` (from the pinned preview) and `loadedDigest` (from what was * actually written) are this digest over their respective file SET, prefixed with * the exact scanned source tree digest so the lock is bound to the vetted commit. * A faithful Lean bind makes the two SETS identical, so `match` is honestly true; * `verify` later recomputes the loaded set from disk and reports per-file drift. */ export declare function eccLeanManifest(entries: readonly string[], sourceTreeDigest: string): EccLeanManifest; /** * The common install root for a component's files: the single file when there is * one, else the longest shared leading directory. Used as the home-scoped * ownership target (what removal deletes) — bounded to one entry per component. */ export declare function componentInstallRoot(rels: readonly string[]): string; /** A file the selective installer actually wrote, captured for the lock + verification. */ export interface EccLeanInstalledFile { scope: "home" | "project"; /** POSIX path relative to `` (scope "home") or the project root (scope "project"). */ rel: string; /** sha256 hex of the written bytes. */ contentDigest: string; /** The allowlisted component this file belongs to. */ componentId: string; } export interface EccLeanInstallInput { resolved: ResolvedGitSource; /** The vetted Lean allowlist (component ids) to drive the selective install with. */ components: readonly string[]; /** The vetted preview diff (the exact operations the install must produce). */ diff: EccLeanPreviewDiff; /** Project root. */ root: string; /** Machine home (`~`) — a fixture temp dir in tests. */ home: string; /** The subprocess seam for ECC's own installer. */ runner: Runner; env: NodeJS.ProcessEnv; timeoutMs?: number; } export interface EccLeanInstallResult { installed: EccLeanInstalledFile[]; } export type EccLeanInstaller = (input: EccLeanInstallInput) => Promise; export interface EccLeanAdapterDeps { /** Project root the binding lock lives under and repo-relative ownership is resolved against. */ root: string; /** The subprocess seam threaded to the installer. */ runner: Runner; /** Environment (home resolution for machine-scope surfaces). Defaults to `{}`. */ env?: NodeJS.ProcessEnv; /** Git checkout cache root for `resolve()`. Defaults to `bindingCacheHome(env)`. */ cacheHome?: string; /** * The upstream preview/plan (pinned to the exact commit). Defaults to the shipped, * catalog-bound `ecc-install-preview.json` via {@link readEccInstallPreview}. */ installPreview?: EccInstallPreviewArtifact; /** The selective-install exec seam. Defaults to a fail-closed installer (see {@link EccLeanInstallerUnavailableError}). */ installer?: EccLeanInstaller; /** Per-call installer timeout override. */ timeoutMs?: number; /** * Full only: injectable plugin-cache locator threaded to `bindPlugin` (D7) and to * `verify`/`remove`. Defaults to {@link defaultPluginCacheLocator}. Tests inject a * fixture locator so no real `claude` runs and no real `~/.claude` is touched. */ locateCache?: PluginCacheLocator; /** * Full only: injectable apply seam for the repo-relative D18 writes (the env-root * fields and the `.mcp.json` connectors, plus `bindPlugin`'s `enabledPlugins`). * Defaults to a real `executePlan` wrapper (worktree gate skipped). */ applyActions?: (root: string, actions: Action[]) => Promise; /** * Full only: state-write surfaces the LABEL DECISION may treat as compliant * because they were removed through a SUPPORTED upstream selection mechanism * (D10 point 4). The only supported mechanism today is the plugin's own * `--config` userConfig options; this list is populated ONLY from such a * mechanism — NEVER by editing plugin content. Each string matches a * {@link EccStateWriteFinding.surface}. */ excludedSurfaces?: string[]; } /** * `remove()` stays SYNCHRONOUS (the D6 contract) so it can only PLAN the teardown. * The caller applies an "apply"-mode result in EXACTLY this order — repo-relative * first, then machine-scope: * 1. repo-relative restore: `executePlan(plan("...", ...repoRelativeActions), ctx)` * — restores any project-scoped owned surfaces to their pre-bind value; * 2. machine-scope teardown: delete each recorded `home:`-scoped install root in * {@link EccLeanRemoveResult.homeOwnership} (ECC's installer has no `uninstall` * command, so removal is conservative deletion of exactly the roots the lock * recorded creating — never un-recorded state). * Then the caller drops the binding lock itself (mirrors the W3 roundtrip * precedent). `"drift-report-only"` mode mirrors {@link planBindingRemoval}: * nothing to apply; `reason` explains why. */ export type EccLeanRemoveResult = { mode: "drift-report-only"; reason: string; } | { mode: "apply"; repoRelativeActions: Action[]; repoRelativeDrift: ClaudeDriftEntry[]; /** Home-scoped ECC install roots to delete (conservative, recorded-only). */ homeOwnership: BindingOwnershipEntry[]; }; /** The exact acceptance tuple ECC Lean binds decisions against (W4 ruling (e)). * The resolve-side binding (repository/commit/treeDigest must match the live * resolution) is asserted by the LIVE installer composition — the only place * acceptance enters the flow — via `acceptanceResolutionMismatches`; fixture * installers never consult acceptance, so a dry-run stays fixture-digested. */ export declare const ECC_LEAN_ACCEPTANCE_TUPLE: { readonly framework: "ecc"; readonly profile: "ecc-lean-v1"; readonly host: "claude"; readonly adapter: "ecc-lean"; }; /** One state-writing surface the Full inventory found in the scanned checkout. */ export interface EccStateWriteFinding { /** Which detector produced this finding. */ kind: "continuous-learning-skill" | "learned-skills-write" | "hook-definition" | "outside-root-write"; /** * A STABLE surface id — enumerated in {@link EccFullLabelInput.sharedWrites} and * matched against an {@link EccLeanAdapterDeps.excludedSurfaces} entry. Shaped * `:` so it is both human-readable and deterministic across runs. */ surface: string; /** Human-readable detail for the Framework Card / report lines. */ detail: string; } /** The Full LABEL DECISION input recorded in `report()` and returned from `provision`. */ export interface EccFullLabelInput { /** * `true` only when the inventory found ZERO noncompliant surfaces, OR every one * was excluded through a supported selection mechanism (D10 point 4). Otherwise * `false` with {@link sharedWrites} enumerating the surfaces that keep it lax. */ strict: boolean; /** The noncompliant shared-write surfaces NOT excluded (sorted, deduped). */ sharedWrites: string[]; } /** * STATIC state-write surface inventory over the SCANNED checkout — the LABEL * DECISION input (D10 point 4). Three detectors: * (a) the known legacy case — any `skills/continuous-learning` directory (matched * by prefix), and any shell / js / ts / py file whose CONTENT writes to the * shared learned-skills dir (`$HOME`/`${HOME}`/`~` + `/.claude/skills/learned`); * (b) any hook definitions the plugin manifest carries ({@link hookManifestFindings}); * (c) a best-effort HEURISTIC net for anything ELSE writing outside the * project-scoped env roots — a write verb targeting a home/absolute path * ({@link OUTSIDE_WRITE_RE}), excluding scripts that reference the project env * roots. Being textual, (c) does not parse shell or resolve variables, so it * can miss obfuscated writes and occasionally over-report; it is intentionally * conservative (one finding per file) to keep the surface set deterministic. * Read-only: no network, no CLI, no host mutation. */ export declare function eccFullStateWriteInventory(scannedTreePath: string): EccStateWriteFinding[]; /** * Compute the Full LABEL DECISION from the inventory (D10 point 4). `strict` is * `true` iff every found surface is excluded through a supported selection mechanism * ({@link EccLeanAdapterDeps.excludedSurfaces}); the remaining surfaces are * enumerated (sorted, deduped) as {@link EccFullLabelInput.sharedWrites}. */ export declare function computeEccFullLabel(findings: readonly EccStateWriteFinding[], excludedSurfaces?: readonly string[]): EccFullLabelInput; /** * The Full `provision` result — the standard `{ lock }` PLUS the LABEL DECISION * input (D10 point 4). Extending the result WITHOUT touching the W2 `ProvisionResult` * / lock schema: the adapter's `provision` still satisfies the `ProvisionResult` * contract, and this widened shape rides through it (a caller casts, exactly as the * Lean/Full remove results do). `report()` recomputes the same label by re-running * the inventory over the marketplace source path recorded in the lock. */ export interface EccFullProvisionResult extends ProvisionResult { labelInput: EccFullLabelInput; } /** * The Full teardown plan — mirrors `SuperpowersRemoveResult` (the host-plugin path * needs `plugin`/`marketplace` for `claude plugin uninstall`, unlike Lean's * installer teardown). The caller applies repo-relative restore FIRST, then the * machine-scope `removePlugin` teardown (the hard host-ordering constraint). */ export type EccFullRemoveResult = { mode: "drift-report-only"; reason: string; } | { mode: "apply"; repoRelativeActions: Action[]; repoRelativeDrift: ClaudeDriftEntry[]; homeOwnership: BindingOwnershipEntry[]; plugin: string; marketplace: string; scope: PluginScope; }; /** * Widening note for `frameworks/registry.ts`: `BindingRegistryDeps` widens to also * carry ECC's construction deps ({@link EccLeanAdapterDeps}'s ECC-only optionals — * `installer`, `installPreview`, and the Full-only `excludedSurfaces`; * `locateCache`/`applyActions` are already shared with `SuperpowersAdapterDeps`). * `root`/`runner`/`env`/`cacheHome`/`timeoutMs` are shared with `SuperpowersAdapterDeps`. * * Mode routing (D10): `plan`/`provision` route on the declaration's mode * ({@link eccMode} — absent/`"lean"` -> Lean, `"full"` -> Full); `verify`/`remove`/ * `report` route on the LOCK's recorded mode (the applied-state authority), falling * back to the declaration when no lock is present. Lean and Full share one file and * one `adapterType`; only the provisioning mechanics differ (D6 installer vs the * project-scope host-plugin path). */ export declare function createEccAdapter(deps: EccLeanAdapterDeps): FrameworkAdapter;