/** * Where the shipped baseline comes from: the installed wicked-garden plugin. * * The daemon seeds (and refreshes) its skills root from the plugin Claude Code actually runs — * the marketplace cache under `/plugins/cache/wicked-garden/ * wicked-garden/` (highest version wins). Design v3 §3/§8 (codex review of #480) made that * the ONLY automatic source, because the hand-installed `/plugins/wicked-garden` copy * was the exact stale artifact the operator's `clis.toml` hack pointed workers at (12.28.1 while the * live cache was 12.32.0 — design v3 §"Verified mechanics"). Amendment v3.6 (#490) admits that copy * as a LAST resort — `npx wicked-installer install wicked-garden` lays it down without registering * the marketplace, so an installer-only machine had no source at all — without hiding the difference: * * Design amendment v3.6 (verbatim): * "Discovery order: (1) `WICKED_CREW_SKILLS_SOURCE` explicit override; (2) the marketplace cache * `/plugins/cache/wicked-garden/wicked-garden/` for each dir in * `CLAUDE_CONFIG_DIR` (may list several) else `~/.claude`; (3) LAST resort, the installer-managed * copy `/plugins/wicked-garden` for each of those dirs AND `~/.claude/plugins/wicked-garden` * (garden's `install.mjs` hard-codes homedir), accepted only when its `.claude-plugin/plugin.json` * parses with a `version`. A source of kind (3) is recorded in the baseline as `source.kind: * 'installer-copy'` and surfaces a persistent WARNING finding `skills.source` in * `GET /diagnostics.skills.findings` — 'seeded from the installer copy at ; register the plugin * with Claude Code (marketplace) to receive marketplace updates' — so the daemon works on * installer-only machines without hiding the difference. When BOTH a cache and a copy exist, the * cache wins regardless of version; the copy is never preferred. Every source kind passes the same * no-follow, closure and validation rules." * * How the automatic tiers walk (codex on #491, both passes): the config dirs are `CLAUDE_CONFIG_DIR`'s * entries in the order listed (platform path delimiter) with the literal `~/.claude` appended once * unless already listed — the default when the variable is unset. Each config dir is resolved exactly * ONCE, at the top of discovery (`realpath` — the one link the automatic tiers follow: operators * symlink `~/.claude`); that canonical root is carried through both tiers, every level below it that * discovery touches — `plugins`, `cache`, the marketplace dir, the plugin dir, each version dir, the * copy dir — is lstat-walked ON THE CANONICAL PATH, a symlink at any of them skips that candidate with * a `symlink` finding (never followed), and a candidate's manifest is read only below that * already-validated canonical dir — never through the spelled path, so a link retargeted mid-walk is * never read. The recorded `PluginSource.path` is that canonical path. Tier (2) visits every dir in * order and the FIRST dir holding a valid cache wins; inside a cache every entry is judged: a name * must be a SemVer version by the semver.org grammar (no leading zeros in numeric identifiers, legal * pre-release / build identifiers — else ignored with a finding), EVERY such dir's `plugin.json` * version must equal its name (else skipped with a finding — every dir is validated, not only the * winner; a manifest that does not parse is a `no-manifest` finding, never a crash), and the pick is * the highest by SemVer PRECEDENCE (build metadata ignored for ordering, * pre-release below release, numeric identifiers before alphanumeric) with a documented tie-break * (`cacheDirOrder`). Tier (3) visits the same dirs in the same order; ANY cache beats ANY copy — a * `~/.claude` cache beats a `$CLAUDE_CONFIG_DIR/plugins/wicked-garden` copy. What was passed over * rides beside the answer as `DiscoveryFinding`s (the daemon logs them). NEVER a repo checkout by * default. `WICKED_CREW_SKILLS_SOURCE` is the one explicit override — for tests, and for an operator * who deliberately wants a checkout or some other plugin root. A machine with neither a cache nor a * copy has no source at all: crew does not vendor garden — the seed says "install garden first" * loudly (`SkillsSourceUnavailableError`) and the runtime leaves the engine input unset. * * # No-follow BELOW the root (codex round 6 on #480) * * The source ROOT may legitimately be reached through a symlink — operators symlink `~/.claude`, * and the marketplace cache may sit behind a linked config dir — so the root is resolved ONCE * (`realPluginRoot`, the one place a link is followed) and every designated entry BELOW it is * lstat-walked (`noFollowEntry`): the manifest dir, `plugin.json`, each catalog, each root file, * each bundle directory and everything inside it. A symlink among them is refused by name * (`PluginSourceSymlinkError`) — a seed refuses to start (the runtime's `skills.config`), a * refresh answers a `blocked` envelope — and nothing is copied: a linked catalog or a linked skill * dir would otherwise copy bytes from outside the declared source into the daemon's root and hand * them to every worker. */ import type { SkillSourceKind } from '../core/types.js'; /** Explicit plugin-source override — the seam tests and proof scripts aim at a fixture plugin. */ export declare const SKILLS_SOURCE_ENV = "WICKED_CREW_SKILLS_SOURCE"; /** * The oldest wicked-garden this daemon uses (crew#753). Automatic discovery passes over an installed * plugin older than this with a `too-old` finding, and the daemon reports it as a blocking * `skills.garden` error naming found vs required, so an old copy is never used without a word. * 12.40.0 is the garden the walkthrough proof reads: its QE ledger rows carry the crew run id * (WT-G4) and its demo recorder seals a take (WT-G1); the draft skill (12.35.0) and the verdict * vocabulary (12.37.0) come with it. An explicit `WICKED_CREW_SKILLS_SOURCE` is deliberate and * is not judged. */ export declare const REQUIRED_GARDEN_VERSION = "12.40.0"; /** The one install command the README, the findings and the refusals name. */ export declare const GARDEN_INSTALL_COMMAND = "npx wicked-installer install wicked-garden"; /** Whether `version` is at least the required garden (SemVer precedence; an unparsable version is not). */ export declare function gardenMeetsMinimum(version: string, required?: string): boolean; /** Relative path of the Claude Code plugin manifest inside a plugin root. */ export declare const PLUGIN_MANIFEST_REL: string; export interface PluginSource { /** Absolute plugin root (the directory holding `.claude-plugin/plugin.json`). */ path: string; kind: SkillSourceKind; /** `version` from the plugin manifest. */ plugin_version: string; } /** * The Claude Code config dirs discovery walks, IN ORDER: every non-empty entry of `CLAUDE_CONFIG_DIR` * (it may list several, separated by the platform path delimiter) as listed, then the literal * `~/.claude` appended once unless already listed — the default when the variable is unset, and the * dir garden's `install.mjs` hard-codes (design v3.6; codex on #491). Spelled as given (the walk * resolves each dir once); a dir listed twice is walked once. */ export declare function claudeConfigDirs(env?: NodeJS.ProcessEnv, home?: string): string[]; /** * A designated entry of the plugin source is a symlink — the seed/refresh is refused by name and * nothing is copied (module header). `entry` is the plugin-relative POSIX path of the link. */ export declare class PluginSourceSymlinkError extends Error { readonly root: string; readonly entry: string; readonly target: string; constructor(root: string, entry: string, target: string); } /** * The plugin root with every link resolved — the ONE place a link is followed (an operator's * symlinked config dir reaches the root). `null` when it does not exist or is not a directory. */ export declare function realPluginRoot(dir: string): string | null; /** * `join(root, ...segments)` after lstat-walking every segment BELOW the canonical root: a symlink at * any of them throws `PluginSourceSymlinkError` naming it; a component that does not exist (or sits * below a regular file) answers `null`. `sourceSpelling` is the root as the operator spelled it, for * the error. */ export declare function noFollowEntry(root: string, segments: ReadonlyArray, sourceSpelling?: string): string | null; /** * The plugin manifest's `version` at `dir`, or `null` when `dir` is not a plugin root — the root * resolved once (`realPluginRoot`), then `manifestVersionBelow`. For the explicit override and * `pluginSourceAt`; the automatic tiers never call this (they resolve the CONFIG dir once and read * below the canonical candidate directly). */ export declare function pluginVersionAt(dir: string): string | null; /** * The manifest `version` below an already-canonical plugin dir, or `null` when there is no * `.claude-plugin/plugin.json` that PARSES with a non-empty string `version` — missing, not a * regular file, not JSON, not an object, or versionless are all "not a plugin root" (design v3.6: * "accepted only when its plugin.json parses with a version"; Copilot on #491: one corrupt manifest * in a cache must skip that candidate, not take discovery down). NO-FOLLOW below the root and no * re-resolution of the root itself: a symlinked `.claude-plugin/` or `plugin.json` throws * `PluginSourceSymlinkError` — never a version read through a link (codex round 6) — and an I/O * error reading the file stays an error. `spelling` is the root as the caller spelled it, for the * symlink error. */ export declare function manifestVersionBelow(root: string, spelling?: string): string | null; /** * A parsed SemVer 2.0.0 version (semver.org). Numeric identifiers are kept as DIGIT STRINGS, never * converted to `number`: above `Number.MAX_SAFE_INTEGER` adjacent values would collapse into one * (`9007199254740992` == `9007199254740993` as doubles) and the tie-break would pick the lower dir * (codex on #491). SemVer forbids leading zeros, so digit strings compare losslessly by length, * then lexically (`compareNumeric`). */ export interface Semver { major: string; minor: string; patch: string; /** Dot-separated pre-release identifiers; empty for a release. */ prerelease: string[]; /** Build metadata after `+`, or `null`; ignored by precedence. */ build: string | null; } /** Parse a SemVer version, or `null` when the string is not one by the official grammar. */ export declare function parseSemver(v: string): Semver | null; /** * SemVer PRECEDENCE (semver.org §11): major, minor, patch numerically; a pre-release version has * LOWER precedence than its release; pre-release identifiers compare left to right — numeric ones * numerically (losslessly — digit strings, never `number`), numeric before alphanumeric, * alphanumeric ones in ASCII order, a shorter set lower when every preceding identifier is equal. * Build metadata is IGNORED: `1.0.1` and `1.0.1+build` * have equal precedence (0). Throws for a string that is not a SemVer version — callers validate * names first (`parseSemver`). */ export declare function compareSemver(a: string, b: string): number; /** * The PICK order over cache version-directory names (all valid SemVer): the highest precedence * first; for EQUAL precedence (names differing only in build metadata — `1.0.1` vs `1.0.1+build`) * the plain release name (no build metadata) first, else the lexicographically smallest full name * (`1.0.1+a` before `1.0.1+b`). A total order over distinct names, so the pick never depends on * `readdir` or creation order (codex on #491). */ export declare function cacheDirOrder(a: string, b: string): number; /** * Classify a plugin root by what it IS: a git checkout when it carries `.git`; the installer-managed * copy when it is a `plugins/wicked-garden` directory (garden's `install.mjs` layout — design v3.6, * so an explicit override aimed at that copy is recorded, and warned about, as the copy it is); the * installed plugin when it sits anywhere else under a `plugins/` directory (the marketplace cache); * anything else is an explicit plugin-shaped directory (an unpacked tarball, a test fixture). */ export declare function classifySource(dir: string): SkillSourceKind; /** A `PluginSource` for an explicit plugin root, or `null` when it is not a plugin root. */ export declare function pluginSourceAt(dir: string): PluginSource | null; /** The marketplace cache dir the live plugin versions live under, for a config dir. */ export declare function livePluginCacheDir(configDir: string): string; /** The installer-managed copy (`npx wicked-installer install wicked-garden`, garden's `install.mjs`), for a config dir. */ export declare function installerCopyDir(configDir: string): string; /** Why discovery passed over something that EXISTS (an absent path is never a finding) — logged by the daemon, asserted by tests. */ export type DiscoveryFindingKind = 'symlink' | 'non-semver-name' | 'version-mismatch' | 'no-manifest' | 'too-old'; export interface DiscoveryFinding { kind: DiscoveryFindingKind; /** The entry judged — the CANONICAL path (below the once-resolved config dir). */ path: string; message: string; /** `too-old` only: the plugin version found (crew#753). */ version?: string; } /** What discovery answered and what it passed over on the way (design v3.6; codex on #491). */ export interface Discovery { source: PluginSource | null; findings: DiscoveryFinding[]; } /** * TEST SEAM for the TOCTOU probe (codex confirmation pass on #491): called with the canonical * candidate dir just before its manifest is read, so a test can retarget the config-dir link * mid-walk and prove the retargeted manifest is never read. The daemon passes nothing. */ export interface DiscoveryHooks { beforeManifestRead?: (canonicalDir: string) => void; } export interface DiscoverOptions { env?: NodeJS.ProcessEnv; home?: string; /** Test seam — see `DiscoveryHooks`. */ hooks?: DiscoveryHooks; /** The minimum garden for the automatic tiers (default {@link REQUIRED_GARDEN_VERSION}). */ requiredVersion?: string; } /** * Discover the installed plugin in the order design amendment v3.6 fixes (module header), with what * was passed over: (1) the explicit `WICKED_CREW_SKILLS_SOURCE` override; (2) the FIRST config dir * (as listed, `~/.claude` appended) holding a valid marketplace cache — the highest-precedence * agreeing version dir inside it; (3) LAST resort, the first of those dirs holding a valid installer * copy. ANY cache beats ANY copy. Each config dir is resolved exactly ONCE, here, and only its * canonical root is handed to the tiers (two spellings of one dir walk once). `env`/`home` are * injectable so tests never read the developer's real config dir. `source` is `null` when nothing is * installed — the caller says "install garden first" loudly. A finding is reported once per entry * even when both tiers meet it (a linked `plugins/`, say). */ export declare function discoverLivePluginDetailed(opts?: DiscoverOptions): Discovery; /** `discoverLivePluginDetailed` answering the source alone; pass `findings` to collect what was passed over. */ export declare function discoverLivePlugin(opts?: DiscoverOptions & { findings?: DiscoveryFinding[]; }): PluginSource | null; export interface GitState { git_sha: string | null; git_dirty: boolean; /** Present when the source is a checkout but git could not answer — the caller logs it. */ error?: string; } /** The checkout's HEAD sha + dirty flag; `{null,false}` for a non-checkout source. */ export declare function gitStateOf(source: PluginSource): GitState; /** crew#1021: checkout probes must never hold the request loop while git runs. */ export declare function gitStateOfAsync(source: PluginSource): Promise; //# sourceMappingURL=plugin-source.d.ts.map