/**
* `vigiles audit
` — point vigiles at any plugin/repo and see what it ships
* and what's broken, with **no model and no API key**.
*
* This is the deterministic substrate under the plugin/skill leaderboard
* (research/divergent-bets.md #9) and the harness-aware scan
* (research/agent-supply-chain-security.md #1): it re-aims the machinery that
* already exists — `loadPlugin` (surfaces + dangling-ref/MCP/empty-machine
* warnings), `parseAgentTools` (the declared tool contract), and
* `findUntestedSurfaces` — into one read-only report. Behavioral checks that
* need to RUN the plugin (observed egress under the sandbox, real trigger-rate)
* stack on top later; this core stays pure so it runs anywhere in CI for free.
*/
import { type PluginLayout } from "./core/layout.js";
import type { ExcludeSet } from "./exclude.js";
import type { DeprecatedKeyFilesFinding } from "./deprecated-key-files.js";
import type { HarnessDialect } from "./core/dialect.js";
import type { ToolIssue } from "./core/tool-contract.js";
import { type HookEventIssue } from "./core/hook-events.js";
import { type McpIssue } from "./core/mcp-config.js";
import type { DescriptionOverlap } from "./core/description-overlap.js";
import type { DescriptionBudgetIssue } from "./core/skill-description-budget.js";
import type { McpToolIssue } from "./core/mcp-tool.js";
import { type McpContractToolError } from "./core/mcp-contract-message.js";
import { type McpHookIssue } from "./core/mcp-hook.js";
import type { TrifectaFinding } from "./core/lethal-trifecta.js";
import type { SkillResourceFinding } from "./core/skill-resources.js";
import type { SkillFenceFinding } from "./core/skill-missing-fence.js";
import { type PluginLayoutFinding } from "./core/plugin-dir-layout.js";
import { type UnclaimedSurfaceFinding } from "./core/surface-discovery.js";
import type { DelegationTrifectaFinding } from "./core/delegation-trifecta.js";
import { type HookBlockFinding } from "./core/hook-block-ineffective.js";
import { type HookMatcherFinding } from "./core/hook-matcher.js";
import { type EvidenceCounts } from "./coverage-evidence.js";
import type { PurityLevel, EffectSurface } from "./core/effects.js";
import { type HandWrittenHook, type StaleHook } from "./scan-core.js";
import { type InstructionWeight } from "./core/instruction-weight.js";
import type { FilteredScan } from "./core/apply-rules.js";
export * from "./scan-core.js";
export interface ScanSkill {
readonly name: string;
readonly path: string;
readonly hasDescription: boolean;
/**
* The skill's effective description (frontmatter `description`, else the first
* body paragraph — the same text the selector keys on), trimmed; `undefined`
* when neither exists. Feeds the model trigger tier's auto-generated probes.
*/
readonly description?: string;
readonly userInvoked: boolean;
/**
* SKILL.md body references to a bundled file (`scripts/`/`references/`/`assets/`
* or a relative markdown link with an extension) that don't resolve on disk
* under the skill dir, each tagged `missing` or `repo-root-only`. Computed by
* `skillResourceIssues()` (one detector, no drift); the rule module that
* reports them (`src/rules/`) reads them through core's `ScanInput`.
*/
readonly resourceIssues: readonly SkillResourceFinding[];
/**
* Lethal-trifecta finding when a MODEL-INVOCABLE skill's `disallowed-tools:`
* fence leaves all three legs standing (read-private + ingest-untrusted +
* exfiltrate), else null. Computed by `skillTrifectaIssue()` — NOT from
* `allowed-tools:`, which is a pre-approval and bounds nothing (measured
* 2026-08-11; see `src/core/lethal-trifecta.ts`). The `fence` field on the
* finding says whether the skill declared no fence at all (`"none"` — the
* ecosystem default, aggregated in the report) or a fence that closes no leg
* (`"ineffective"` — per-skill). A user-invoked skill is excluded (it can't be
* selected by attacker content).
*/
readonly trifecta: TrifectaFinding | null;
/**
* Set when the SKILL.md opens with frontmatter-looking keys (`name:`, …) but
* has NO opening `---` fence, so the whole file loads as body — no name, no
* description, no trigger (the skill is invisible). Computed by
* `skillMissingFence()` (one detector, no drift).
*/
readonly fenceIssue: SkillFenceFinding | null;
}
export interface ScanAgent {
readonly name: string;
readonly path: string;
/** Declared tool contract, or null when the agent ships no `tools:` (inherits all). */
readonly tools: readonly string[] | null;
/** Contract entries that don't resolve to a real built-in / MCP tool (typo, never-available). */
readonly toolIssues: readonly ToolIssue[];
/** MCP tool entries naming a server the plugin doesn't declare (can't resolve). */
readonly mcpToolIssues: readonly McpToolIssue[];
/**
* ADVISORY tool findings — a real tool withheld only under a condition vigiles
* cannot see, or a name not in its capture. Surfaced via `vocabularyNotes`,
* never scored. Optional: absence is not a claim of zero.
*/
readonly toolNotes?: readonly ToolIssue[];
/** `disallowedTools:` block-list entries that are typos of a real tool (block nothing). */
readonly disallowedToolIssues: readonly ToolIssue[];
/**
* Static effect-surface purity of the agent's declared tool contract.
* - `"pure"` — no side-effecting tools (read-only, deterministically testable).
* - `"bounded"` — has side-effecting tools (Edit/Write/…) but no Bash or unknown.
* - `"unrestricted"` — has Bash, any MCP/unknown tool, or inherits-all (no contract).
* Computed by `effectSurface()` from `src/core/effects.ts` — one detector, no drift.
*/
readonly purity: PurityLevel;
/**
* The three tool buckets from `effectSurface()`: read-only, side-effecting, and
* unknown-effect (MCP or unrecognized) tool names in the declared contract.
*/
readonly effectBuckets: Pick;
/**
* Lethal-trifecta finding when the subagent's declared tools hold all three legs
* (read-private + ingest-untrusted + exfiltrate), else null. An inherits-all
* agent (no `tools:` line) is the "advisory" case. Computed by
* `lethalTrifectaIssues()` (one detector, no drift).
*/
readonly trifecta: TrifectaFinding | null;
}
/** A lethal-trifecta finding tagged with the surface (subagent/skill) that holds it. */
/** One advisory vocabulary finding, tagged with the surface that carries it. */
export interface VocabularyNote {
/** Where it was found — a hook event key, or an agent's path. */
readonly where: string;
readonly message: string;
}
export interface ScanTrifectaFinding {
readonly path: string;
readonly kind: "subagent" | "skill";
readonly name: string;
readonly finding: TrifectaFinding;
}
/** A missing-frontmatter-fence finding tagged with the skill path. */
export interface ScanSkillFenceFinding {
readonly path: string;
readonly name: string;
readonly finding: SkillFenceFinding;
}
/** A delegation-trifecta finding tagged with the subagent path that holds it. */
export interface ScanDelegationFinding {
readonly path: string;
readonly finding: DelegationTrifectaFinding;
}
/** A skill/agent whose frontmatter is missing a required field (name / description). */
export interface FrontmatterIssue {
readonly path: string;
readonly kind: "skill" | "agent";
readonly missing: readonly ("name" | "description")[];
readonly message: string;
}
/** A skill/agent whose `---` block exists but isn't valid YAML (may not parse as intended). */
export interface FrontmatterParseIssue {
readonly path: string;
readonly message: string;
}
/** An agent frontmatter field whose VALUE is invalid (a typo of a real model/color). */
export interface FrontmatterValueIssue {
readonly path: string;
readonly field: "model" | "color";
readonly value: string;
readonly suggestion: string;
readonly message: string;
}
/** ok = file present; missing = referenced but absent; unresolved = path still has an unexpanded var, can't check. */
export type HookStatus = "ok" | "missing" | "unresolved";
export interface ScanHook {
/** The hook line that names this file, as the config wrote it. */
readonly command: string;
readonly script: string;
readonly status: HookStatus;
/**
* The hook EVENT this script is registered under (`PreToolUse`, `PostToolUse`,
* `SessionStart`, …), when it can be determined from the canonical
* object-keyed-by-event settings shape; `undefined` for a non-object/array
* config. The safety battery uses it to test only the blocking-capable
* `PreToolUse` guards — so a `SessionStart`/`PostToolUse` hook isn't unfairly
* scored against "does it block rm -rf".
*/
readonly event?: string;
}
/**
* The repo's top-level instruction file (`CLAUDE.md` / `AGENTS.md`), if present.
* Every cc/codex repo has one even when it ships no plugin surface, so `scan`
* reports it — otherwise a plain instruction-only repo looks empty. `hasSpec` is
* the deterministic fact that a `.spec.ts` sits beside it (spec-managed vs
* hand-written); it is informational, NOT the `require-instructions-spec` gate (that's lint).
*/
export interface ScanInstructions {
readonly file: string;
readonly hasSpec: boolean;
}
export interface ScanReport {
readonly dir: string;
/** The detected instruction file (CLAUDE.md/AGENTS.md), or null if none. */
readonly instructions: ScanInstructions | null;
readonly skills: readonly ScanSkill[];
readonly agents: readonly ScanAgent[];
readonly hooks: readonly ScanHook[];
/** Hook entries with no script file (inline shell one-liners) — can't be path-checked. */
readonly inlineHooks: number;
/**
* The hook lines vigiles did not write (a compiled hook's line is
* `hook-runtime …`, read by its own template). Each one is a
* `hook-script-exists` finding: a static reader cannot tell what a
* hand-written line runs or whether it swallows its own failure.
*/
readonly handWrittenHooks: readonly HandWrittenHook[];
/**
* Compiled lines that are no longer the line `compile` writes (edited, or
* compiled by another version). `hook-script-exists` reports them.
*/
readonly staleHooks: readonly StaleHook[];
readonly commands: number;
/**
* Output styles found, exempt ones aside, or `"not-supported"` when the
* harness has none — the inventory side of `untested-output-style`. Optional
* so a hand-built report predating the field still type-checks.
*/
readonly outputStyles?: number | "not-supported";
readonly mcp: boolean;
/**
* Intra-plugin file references (hook scripts, skill bodies) pointing at files
* that don't exist on disk — the broken-path / partial-vendor class. A
* first-class structural finding, not just a free-text warning, so the verdict
* and the leaderboard can count it.
*/
readonly danglingRefs: readonly string[];
/**
* Skills naming a sibling skill that does not exist (a near-miss of a real one
* — a rename or a typo). Separate from `danglingRefs` because that field holds
* PATH tokens from executable sources; these are full sentences about prose.
* Both are broken intra-plugin references and score in the same category.
*
* OPTIONAL because absence is not a claim: a producer that predates this field
* (or a browser scan that has not been taught it) reports nothing rather than
* reporting zero, the same `undefined`-vs-`[]` distinction the check counter
* and `assertNoWrite` already draw.
*/
readonly skillRefIssues?: readonly string[];
/** Hooks registered under an event name the harness doesn't define (typo / dead). */
readonly hookEventIssues: readonly HookEventIssue[];
/**
* ADVISORY vocabulary findings — names vigiles could not confirm, and real
* names the platform withholds only under a condition it cannot see. Surfaced
* and NEVER scored, which is the point: the two failure modes this replaced
* were silence (which reads as approval and hides vigiles's own staleness) and
* an error (which blames the user for our gap). Neither is a verdict, so these
* get a third channel instead of a grade.
*
* OPTIONAL because absence is not a claim: a producer predating this field
* reports nothing rather than reporting zero.
*/
readonly vocabularyNotes?: readonly VocabularyNote[];
/** Skills/agents missing a required frontmatter field (name; agents also description). */
readonly frontmatterIssues: readonly FrontmatterIssue[];
/** Agent frontmatter fields with an invalid value (a typo of a real model/color). */
readonly frontmatterValueIssues: readonly FrontmatterValueIssue[];
/** Skills lacking an EXPLICIT name/description — a best-practice recommendation, not a defect. */
readonly skillMetaIssues: readonly FrontmatterIssue[];
/** Declared MCP servers that can't start (no command/url). */
readonly mcpIssues: readonly McpIssue[];
/** `type: mcp_tool` hook actions that are incomplete or target an undeclared server. */
readonly mcpHookIssues: readonly McpHookIssue[];
/** Pairs of model-invocable skills whose descriptions are near-identical (precision collision). */
readonly descriptionOverlaps: readonly DescriptionOverlap[];
/** Model-invocable skills whose description is so long the trigger signal is buried. */
readonly descriptionBudgetIssues: readonly DescriptionBudgetIssue[];
/**
* Lethal-trifecta findings across subagents + model-invocable skills — a unit
* holding all three legs (read-private + ingest-untrusted + exfiltrate). Each
* carries the surface path + kind for reporting/annotations. Shared by `scan`
* (the report) and the `lethal-trifecta` lint rule (one detector, no drift).
*/
readonly trifectaFindings: readonly ScanTrifectaFinding[];
/**
* Skills whose frontmatter-looking opening has NO `---` fence, so they load as
* pure body (invisible — no name/description/trigger). Shared by `scan` and the
* `skill-missing-fence` lint rule (one detector, no drift).
*/
readonly skillFenceIssues: readonly ScanSkillFenceFinding[];
/**
* Functional surface dirs (skills/agents/commands) nested INSIDE the manifest
* dir (`.claude-plugin/`) where the harness can't see them. Shared by `scan`
* and the `plugin-dir-layout` lint rule (one detector, no drift).
*/
readonly pluginLayoutIssues: readonly PluginLayoutFinding[];
/**
* Surface directories found by SHAPE in the bounded root set that NO
* registered harness claims — a harness sitting somewhere vigiles does not
* read, reported instead of silently graded around (#240). Computed by
* `core/surface-discovery.ts` from paths alone; shared with the browser twin
* (one detector, no drift).
*/
readonly unclaimedSurfaces: readonly UnclaimedSurfaceFinding[];
/**
* Lethal trifectas that EMERGE across a delegation edge — a subagent whose
* effective (own ∪ delegated-to) capability holds all three legs though no
* single unit does. Shared by `scan` and the `delegation-trifecta` lint rule
* (one detector, no drift).
*/
readonly delegationTrifecta: readonly ScanDelegationFinding[];
/**
* Hooks that LOOK like they block but silently don't — a block decision on a
* non-blocking event, or the legacy `decision` field on a permission-gated
* event (#19009, the #1 verified hook pain). Shared by `scan` and the
* `hook-block-ineffective` lint rule (one detector, no drift). Empty when the
* dialect doesn't declare its blocking-event semantics.
*/
readonly hookBlockFindings: readonly HookBlockFinding[];
/**
* Hook `matcher` strings that don't fire as written — a tool-name typo, a
* matcher that doesn't compile, an MCP pattern that matches no tool name or is
* too narrow for real server naming, or an undeclared MCP server. Shared by
* `scan` and the `hook-matcher` lint rule (one detector, no drift).
*/
readonly hookMatcherFindings: readonly HookMatcherFinding[];
/**
* What the harness loads WITHOUT being asked, weighed in ITS OWN unit — and
* what it does when that is too much. `null` when the adapter declares no
* budget. Reported, never gated: both corpora this was built against sit near
* four times the Claude Code threshold, and a rule that fails every real repo
* on day one is switched off on day one.
*/
readonly instructionWeight: InstructionWeight | null;
/**
* Instruction specs still using the deprecated `keyFiles` — the
* `deprecated-key-files` rule's own detector (`findDeprecatedKeyFiles`),
* handed in through `opts` by `audit`, so `lint` and `audit` cannot
* disagree. Advisory: printed with its fix, never scored.
*
* ABSENT when there are none, never `[]`, for the parity reason
* `coverageCaveats` gives. ⚠️ The browser twin produces none: it would need the
* TypeScript parser in the site bundle, and the demo does not fetch spec
* sources. No vendored parity fixture carries a spec, so the gate stays
* byte-identical; a spec added to one would show this gap as a parity diff.
*/
readonly deprecatedKeyFiles?: readonly DeprecatedKeyFilesFinding[];
/** Skills/agents whose `---` block isn't valid YAML — informational (may still load via salvage). */
readonly malformedFrontmatter: readonly FrontmatterParseIssue[];
readonly warnings: readonly string[];
/** Surfaces covered by NEITHER tier — the union count (unchanged). */
readonly untested: number;
/**
* Surfaces with no DETERMINISTIC harness (`*.harness.*`) — free,
* millisecond, every-push. Feeds the `Tested` ring. Optional so a hand-built
* report (and any producer predating the split) falls back to `untested`.
*/
readonly untestedHarness?: number;
/**
* Surfaces whose FIRING was never measured — no `*.eval.mjs` covers them. Paid,
* scheduled, real-model. Feeds the `Evaluated` ring. A DIFFERENT gap from
* `untestedHarness`, at a cost three orders of magnitude apart — which is
* exactly why it is a different number and not a slash in one finding.
*/
readonly unevaluated?: number;
/**
* Surfaces whose firing COULD be measured at all — the `Evaluated` ring's
* denominator. 0 → the ring is n/a (nothing to evaluate), never a false 0.
*/
readonly evaluable?: number;
/**
* HOW the covered surfaces were decided to be covered — declared / colocated /
* name-mentioned. A coverage count with no provenance is what let a one-line
* COMMENT confer coverage unnoticed; carrying the derivation makes "28 covered"
* distinguishable from "28 names that happen to appear in some file".
*/
readonly coverageEvidence?: EvidenceCounts;
/**
* The caveats that QUALIFY the coverage numbers above — "measured, but not
* this version", "still carrying a marker that stopped counting". Already
* formatted, because both renderers print the same sentence and a second
* wording is a second thing to keep true.
*
* Absent when there are none, never `[]`: byte-parity between the disk and
* browser engines is asserted field-for-field, and an empty array on one side
* against an absent field on the other is a diff where two absent fields are
* not.
*
* ⚠️ THE BROWSER TWIN PRODUCES NONE OF THESE, and only two of the three have
* an excuse: "measured, but not this version" needs `.vigiles/coverage.json`
* and the retired-marker note needs to read test files, neither of which a
* filesystem-free scan has. The retired-SUFFIX note is derivable from the file
* map alone and is simply not wired there yet. The parity test cannot see the
* gap either way — no vendored fixture carries any of the three inputs, which
* is the same blind spot `scan-files.test.ts` documents for the
* single-skill-at-root branch.
*/
readonly coverageCaveats?: readonly string[];
/**
* Whether the repo has its OWN test setup (a real `package.json` `test` script or
* a conventional test dir). When true, the `untested` count — which only counts
* vigiles-native `.eval.mjs`/`.harness.mjs` — is misleading: the team clearly
* tests, they just don't use vigiles's skill-trigger tier. So the `Tested` ring is
* contextualized as OPTIONAL rather than reading as a failure (it's advisory /
* ungraded either way). Optional — the browser `scanFiles` path leaves it unset
* (false); only the fs-based CLI scan detects it. See gate-first-adoption G3.
*/
readonly ownTestSignal?: boolean;
/**
* Harness-level purity summary: how many scanned agents fall into each purity
* rung. A high `pure` count means more of the harness is statically testable
* (deterministic, no mocks); `unrestricted` is the blind-spot count.
* Computed by `effectSurface()` (one detector, no drift).
*/
readonly puritySummary: {
pure: number;
bounded: number;
unrestricted: number;
};
}
/**
* ONE harness a scan reads the repo under — the resolved form of a
* `.vigilesrc.json#harnesses` entry (#240).
*
* All three parts travel together on purpose. The LAYOUT says where this harness
* reads and what a surface is there; the DIALECT says what it understands (and
* carries its own instruction budget, in its own unit); the ROOTS are the extra
* places this repo's owner says that harness's surfaces also live. Splitting
* them is how a root came to be read under a layout nobody chose for it.
*/
export interface ScanHarness {
readonly layout: PluginLayout;
readonly dialect: HarnessDialect;
readonly roots: readonly string[];
}
/**
* The always-loaded instruction weight of ONE directory under ONE harness —
* the bounded read, the harness's chain, and the sum, in one place.
*
* `scanPlugin` reports it, `lint`'s instruction-weight ratchet gates on it and
* `compile` prints it, so all three must arrive at the same number by the same
* route; this function is that route. `null` when the harness publishes no
* budget (there is no unit to count in).
*/
export declare function measureInstructionWeight(dir: string, harness: Pick): InstructionWeight | null;
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
export declare function scanPlugin(dir: string, layout: PluginLayout, dialect: HarnessDialect, opts?: {
sharedDirs?: readonly string[];
sharedDirsRoot?: string;
/**
* The repo's `.vigilesrc.json#exclude`, so surface DISCOVERY honours it.
*
* It rides in `opts` rather than as a fifth positional parameter because ~25
* call sites pass the first three and nothing else; a required parameter here
* would be twenty-odd mechanical edits for one behavioural change.
*
* ⚠️ Omitting it is NOT "the repo excludes nothing" — it is "this caller has
* no ExcludeSet to give", and the walk then reads everything. `audit` and
* every `lint` rule checker supply one (the checkers through the per-bundle
* context `overBundles` builds, so a new checker cannot forget it).
*/
excludes?: ExcludeSet;
/**
* The `deprecated-key-files` findings for this directory, from the rule's
* own detector (`findDeprecatedKeyFiles`), computed by the composition root
* — the one place allowed to walk the repository and read specs. `audit`
* passes them; every other caller leaves the field off the report.
*/
deprecatedKeyFiles?: readonly DeprecatedKeyFilesFinding[];
/**
* The repo's `.vigilesrc.json#harnesses`, resolved — every declared harness
* with its own layout, dialect and roots, IN DECLARATION ORDER.
*
* 🔴 THE LIST IS READ WHOLE, AND THAT IS THE FIX (#240). The repo is loaded
* once per entry, each under its OWN layout, and the file maps merge
* (deduplicated by on-disk path, so one tree serving two harnesses is
* counted once). Under the flat `harness` array only the first entry's
* layout was ever used, so a repo declaring both got its skills OR its
* instruction file depending on which name it happened to list first, and no
* order gave it both.
*
* The first entry is the PRIMARY: it supplies the dialect every
* dialect-shaped check runs under and the `harness` the report is labelled
* with. The INSTRUCTION FILE is the deliberate exception — it comes from the
* first declared harness whose instruction file is actually present, because
* reporting "no instruction file" for a repo holding the other declared
* harness's one is the half of the bug that has nothing to do with skills.
*
* Rides in `opts` for the same reason `excludes` does: ~25 call sites pass
* the first three positionals and nothing else. Omitting it means "this
* caller has no declaration to pass" and the scan runs single-harness on the
* positional `layout`/`dialect`, exactly as before.
*/
harnesses?: readonly ScanHarness[];
}): ScanReport;
/**
* LIVE MCP tool resolution for a scanned plugin — the dynamic check no static
* linter can do: it STARTS each declared MCP server and checks every
* `mcp__server__tool` the plugin's agents reference actually exists on it
* (catching rename/removal rot, e.g. `create_issue`→`issue_write`). Reuses the
* already-computed `report` (its agents' tool lists) + the declared server configs;
* returns `[]` when the plugin declares no MCP servers (nothing to start). Async +
* side-effecting (spawns servers) — so `audit` runs it by default only for the
* user's OWN repo (own-repo, like running your own tools); a FOREIGN plugin's
* servers are never spawned, and `--fast` opts out. See `verifyMcpContractTools`
* (core/mcp.ts).
*/
export declare function verifyLiveMcpTools(report: ScanReport, layout: PluginLayout, dialect: HarnessDialect, timeoutMs?: number): Promise;
/** Render the live MCP tool-check result (human-readable). */
export declare function formatMcpContractReport(errors: readonly McpContractToolError[]): string;
/**
* A plugin MARKETPLACE (`.claude-plugin/marketplace.json`) decomposed into its
* members. A marketplace either VENDORS its plugins in-tree (string `source`
* paths, e.g. wshobson/agents — `onDisk` populated) or CURATES external ones
* (object `source` with a git/url, e.g. obra/superpowers-marketplace,
* anthropics/claude-plugins-community — `external` populated, nothing on disk).
* Distinguishing the two lets `scan` report a curated marketplace honestly
* instead of mistaking it for an empty repo.
*/
export interface MarketplaceInfo {
readonly name: string;
/** Member plugin dirs that exist on disk (string `source` paths). */
readonly onDisk: readonly string[];
/** Members referencing an off-disk source (url/git/github) — can't be scanned here. */
readonly external: number;
readonly total: number;
}
/**
* Read a `marketplace.json` beside the layout's plugin manifest and classify its
* members into on-disk vs external. Returns `null` when `dir` is not a
* marketplace. The source of truth behind {@link expandMarketplace} and the
* curated-marketplace report in `vigiles audit`.
*/
export declare function inspectMarketplace(dir: string, layout: PluginLayout): MarketplaceInfo | null;
/**
* If `dir` is a plugin MARKETPLACE (a `marketplace.json` beside the layout's
* plugin manifest, e.g. `.claude-plugin/marketplace.json`), expand it into the
* absolute dirs of its member plugins. Returns `null` when there's no
* marketplace, `[]` when it's a marketplace whose members are all external (not
* on disk). Used by `vigiles audit` to rank a whole marketplace — wshobson/agents
* alone ships 80+ plugins under one `marketplace.json`. See {@link inspectMarketplace}.
*/
export declare function expandMarketplace(dir: string, layout: PluginLayout): string[] | null;
export declare function formatScanReport(
/**
* Minted by `applyRules`: an off rule's findings are already gone, and an
* ungraded group's are in `ungraded` — printed AFTER the verdict line and
* never counted in it, from the same data the JSON and the HTML render.
*/
r: FilteredScan): string;
//# sourceMappingURL=scan.d.ts.map