import { type PluginLayout } from "./core/layout.js"; import type { ExcludeSet } from "./exclude.js"; export interface LoadedPlugin { /** A `.claude/settings.json`-shaped object with hooks resolved. */ readonly settings: { hooks?: unknown; }; /** Files to materialize in the sandbox (CLAUDE.md, skills, agents, commands). */ readonly files: Record; /** * Surfaces that are present in the plugin but cannot be exercised at the * deterministic tier (subagents and slash commands need a real model; MCP * servers aren't wired by the loader). Empty when the plugin is fully * covered. Surfaced so "load the whole plugin" never silently tests nothing — * read it in a test, or just to know what the deterministic run won't reach. */ readonly warnings: readonly string[]; /** * Map from a materialized `files` key to the ABSOLUTE on-disk path it was read * from. A surface can be materialized under a canonical key (`.claude/skills/ * foo/SKILL.md`) while living on disk at a different root (the repo-root * `skills/` OR the project-level `.claude/skills/`), so a consumer that needs * the real dir (e.g. resolving a skill's bundled resources) must reverse-map * through this instead of guessing from the key. Present for every surface file. */ readonly sources: Record; /** * The project settings' `hooks` block when a plugin manifest or `hooks.json` * supplied {@link settings} instead — `undefined` otherwise (including when * the settings ARE what `settings` holds, so nothing is counted twice). * * 🔴 A REPO CAN BE BOTH, AND BOTH ARE LIVE. A plugin's manifest hooks run for * whoever INSTALLS it; its `.claude/settings.json` hooks run for whoever works * IN it. The loader picks one source for `settings` (the sandbox runs the * plugin), so the other was never looked at: the vigiles repository's own * compiled nudges pointed at a runtime that did not exist for three weeks and * `hook-script-exists` never read the file they were wired in (measured * 2026-10-11). Raw — no token is expanded — and only PATH-CHECKED; the * deterministic tier never runs it. */ readonly shadowedSettingsHooks?: unknown; /** * {@link settings}' `hooks` block as the file wrote it — no root variable * expanded. `settings` writes the plugin root in so the shipped wiring runs; * a READER of a hook line (what it names, which hook it is) needs the line * the author wrote, or every message quotes a machine path. Absent when * `settings` holds no hooks. */ readonly hooksAsWritten?: unknown; } /** * Load the real harness at `pluginPath`. Returns the resolved settings (hooks), * the files (CLAUDE.md + skills + agents + commands) to write into the test * sandbox, and `warnings` for surfaces the deterministic tier can't drive. Merge * `settings` with any inline settings and spread `files` into the fixture. * * `surfaceRoots` is this function's parameter name for what the repo owner * writes as `.vigilesrc.json#harnesses[""].roots` — extra repo-relative * bases to read `//…` from, for a repo that keeps its skills * somewhere no harness reads by default. The config key is nested UNDER a * harness name precisely so a root cannot be declared without saying whose * layout reads it; this parameter receives one harness's slice of that, which * is why it is still a bare list here. `excludes` still wins over it: * both the per-tree check in {@link materializeSurfaces} and `readTree` drop an * excluded path whatever declared it, so a root that is declared AND excluded is * read exactly as if it had never been declared. */ export declare function loadPlugin(pluginPath: string, layout: PluginLayout, excludes?: ExcludeSet, surfaceRoots?: readonly string[]): LoadedPlugin; /** * ONE declared harness for {@link loadPlugins}: the layout to read the repo * under, and the extra roots declared for THAT harness. */ export interface HarnessLoad { readonly layout: PluginLayout; readonly roots?: readonly string[]; } /** * Load the repo once PER DECLARED HARNESS and merge the results into one * `LoadedPlugin` (#240). * * 🔴 THE MERGE IS WHY THIS EXISTS, AND THE DEDUPLICATION IS ITS WHOLE CONTRACT. * A repo that declares two harnesses is a repo whose grade must cover both — the * flat `harness` array could not do that, because whichever name sat first * decided the ONE layout everything was read under: measured on a repo with * `AGENTS.md` + `.ai/skills/alpha/SKILL.md`, Claude-Code-first graded the skill * and reported 0 chars of always-loaded instructions, Codex-first read * `AGENTS.md` and reported no skill at all. Neither order produced both halves. * * ⚠️ AND THE OBVIOUS FIX HAS AN OBVIOUS SECOND BUG: a repo honest enough to say * one tree serves both tools would then have that tree read twice and every * skill in it counted twice, so declaring the truth would lower the grade. So the * merge keys on the REAL ON-DISK PATH (`sources`), not on the materialized key: * the first harness to claim a file keeps it, later ones skip it, and the counts * are of files rather than of claims. Keying on the materialized key would not * do — two layouts can reach one file under two different keys (`.ai/.agents` * + `skills` and `.ai` + `.agents/skills` are the same directory), and that is * exactly the case an honest dual declaration produces. * * Settings come from the FIRST harness that yields any, and the file-map merge * is first-wins for the same reason: the primary harness is the one whose * dialect the report is rendered in, so its reading of a shared path is the one * the rest of the report is consistent with. */ export declare function loadPlugins(pluginPath: string, harnesses: readonly HarnessLoad[], excludes?: ExcludeSet): LoadedPlugin; export declare function danglingRefs(root: string, layout: PluginLayout): string[]; /** * Resolve the effective harness for a test/eval (arm): load the plugin if given, * then layer inline settings + files on top. Shared by `runHarnessTest` and * `runEval` so both test the assembled machine the same way. `layout` is * REQUIRED (no harness default at the composition root) — the Claude Code * wrapper supplies `claudeCodeLayout` to preserve `resolveHarness(opts)`. */ export declare function resolveHarness(opts: { plugin?: string; settings?: unknown; files?: Record; }, layout: PluginLayout): { settings: unknown; files: Record; }; //# sourceMappingURL=plugin-loader.d.ts.map