/** * The daemon-owned skills root (design v3 DECISIONS 1/4/5/6/7): ONE effective garden-shaped plugin * root the operator edits like a filesystem, PUBLISHED as immutable snapshots the engine hands to * every worker. Nothing a worker runs is ever read from `effective/`. * * # Layout — `/` (`/skills` by default; never a `~/.wicked-crew` literal — * the ONE storage root every crew store shares, design v3.1 §1; the worker Read fence is core's * explicit denylist of state-home subtrees — `skills/baseline/`, `skills/effective/`, * `skills/manifest.json`, `skills/current`, … — with the resolved `skills/snapshots//` the * only non-denied path; `tests/fixtures/state-home-subtrees.json` is the shared registry) * * manifest.json the state (`SkillManifest`; presence = seeded), CAS `revision` * baseline// the shipped bundle (dependency closure, bundle.ts) — identity is the * content hash of the bundle, never the version string alone; `.venv` * is provisioned here once per hash (venv.ts) and shared READ-ONLY; * a baseline lives as long as ANY snapshot on disk references it * effective/ the same shape, holding what the operator edits: EVERY skill, enabled * or not (enablement is manifest state, orthogonal to content) * snapshots// immutable published trees, LOCKED read-only at publish: enabled * skills only, nested layout verbatim, support closure, `snapshot.json`, * `.venv -> baseline venv` (only when that env exists), and the * generated delivery VIEWS (below) * snapshots//views/copilot/.github/skills// * the copilot view (design v3.2 §2/§4): a copy of every enabled * PORTABLE skill's own files under its frontmatter name — what core * hands a copilot seat as `--add-dir /views/copilot`. * Generated at publish, part of the content hash, never edited. The * ONLY view today; `portable` is the admission key for every one. * current -> snapshots/ flipped atomically after each publish; the engine receives the * absolute REAL path of the generation as `WICKED_SKILLS_SNAPSHOT` * (engine-env.ts) — the ONLY input core reads (v3.1 §2) * .uv-cache/ the daemon's own uv cache (`UV_CACHE_DIR`), never the operator's * * NOTHING under this root is ever written into the user's own CLI directories — `~/.codex`, * `~/.pi`, `~/.copilot`, `~/.config/opencode`, `~/.claude` are never touched (design v3.2 §1; the * v3 additive mirror is withdrawn). Skills reach workers only through per-launch, wicked-owned * delivery core performs from the snapshot (Claude: the plugin root; pi: `--skill` per portable * skill; copilot: `--add-dir /views/copilot`). A CLI without a lever (codex today) runs * WITHOUT wicked skills, and a unit on such a seat that requires one (`skill_ref` / mandate) is * REFUSED at launch by core — there is no proceed-with-disclosure setting and never a side channel. * * # Identity, ownership, hashes * * A skill is a directory holding `SKILL.md`, keyed by the PATH-DERIVED name * `wicked-garden-` — which the live plugin's frontmatter `name` equals * for all 142 (unique). A declared name that differs is a `name-mismatch` finding, blocking at * publish; a declared name that is ANOTHER catalog skill's key is a `name-collision`, blocking * whether or not the declaring skill is enabled: the manifest key, the directory, and the * invocation identity are one thing. A skill's OWN files are everything under its dir except a * nested skill's subtree — the deepest `SKILL.md` ancestor ON DISK owns a path (v3 §6; codex round * 2: ownership follows the filesystem, not the manifest, so a child created by a direct edit is * still nobody else's), so disabling/resetting/replacing a parent never touches a child, and the * file API refuses a child's path through the parent's endpoint. Every managed file carries * `{baselineHash, effectiveHash, lastPublishedHash}`; provenance is derived from them, never * asserted. Every mutation takes `expectedRevision` (CAS) and bumps `revision`; direct filesystem * edits are detected by hash at publish and REPORTED (`fs-drift`), never silently trusted. * * # Containment (no-follow everywhere, the root included) * * Every path the store reads or writes — a write's DESTINATION (replace/add descendants included), * a baseline READ (reset, the `?side=baseline` copy, every baseline hash lookup) — is lstat-walked * FROM THE SKILLS ROOT (`effective/…`, `baseline//…`), so a skill directory replaced by a * symlink, a symlinked child inside it, or a symlinked baseline ancestor is refused, never * followed (contain.ts / tree.ts). The ROOT ITSELF is checked before EVERY read and mutation * (`assertRootIdentity`, codex round 4): it must be a real directory, never a symlink, whose * canonical path is the one the store bound at boot — a root replaced by a link to a copied store * (or an ancestor swapped under the daemon) refuses every operation (`SkillsRootInvalidError`, a * loud 503), never redirects one. Every PERSISTED name is validated at manifest parse — a skill * `dir`, a baseline hash, a file-record key, and the skill KEY itself (a safe single segment that * IS the path-derived name of its `dir`; the copilot view lays a skill out under its key) — and * `copyFiles` re-checks every destination's shape and walk before the first byte moves. A * path-guard failure on a WRITE answers the normal 2xx `{verdict: 'blocked', findings: * [path-invalid]}` envelope, never a 400 — a refresh included: it preflights every destination, * stages under the root and swaps, so a refused destination changes NOTHING. Names the store itself * owns at the snapshot/root level (`snapshot.json`, `manifest.json`, `current`, `views/`, `.venv`) * are refused as support paths. * * # Publish (serialized, root-bound, crash-safe, idempotent on retry) * * ONE publish at a time (a second concurrent request is refused, `SkillsPublishInFlightError`; the * route answers it as a 2xx `blocked` `publish-in-flight` envelope — nothing was written, so it is * not a 409, codex round 3). The operation binds the root's identity (path + realpath) at start and * re-checks it after every await: a root whose canonical path moved or vanished while the baseline * env was provisioning aborts the publish (`SkillsRootChangedError` → the 2xx `blocked` * `root-changed` envelope) — nothing is written through a root the operation did not validate. (The * root is never re-aimed: `/skills`, full stop — root-fence.ts; there is no `skills_root` * setting and no env override, codex round 5.) Provisions the * baseline's `.venv` (awaited; one provisioning per baseline * hash — a concurrent caller awaits the in-flight one; a snapshot never links an env still being * written; a FAILED provisioning is BLOCKING, `venv-failed` — the shared env is required, not * best-effort), re-checks the CAS, validates the WHOLE tree — frontmatter (strict subset), name == * path, no cross-catalog name collision, the core closure COMPLETE (a missing registered ref or an * absent transitive mandate is BLOCKING), every `${CLAUDE_PLUGIN_ROOT}/

` and `../

` reference * of an enabled skill judged by TARGET (one that ESCAPES the plugin root is BLOCKING; one whose * target is MISSING inside it is a WARNING — design v3.4 §1: an upstream content bug publishes as * found, `verdict: 'warnings'`), every support file INSIDE the bundle closure (a file outside the * ONE allowlist — bundle.ts `inBundleClosure` — is BLOCKING, `outside-closure`), the plugin manifest * naming the plugin and the `.claude-plugin/*` catalogs present AND well-formed, no unregistered * `SKILL.md` — then: allocates the generation from the FILESYSTEM (max existing + 1), writes * `snapshots/.staging-/` (the snapshot files + the generated views), renames it to * `snapshots//` (never over an existing generation), LOCKS it read-only, commits the manifest * (the venv state included — nothing is persisted before validation passes), and only THEN flips * `current`. A crash between the commit and the flip is finished at the next boot (`ensureReady`); * a torn staging dir is swept by the next publish. Generations beyond the newest three that no * LIVE run may still be reading are reaped (`live`, fed from the CoreEvent stream AND from the * daemon's own launch handoffs — live-generations.ts: a generation handed to a launch stays pinned * until the engine reports what that launch used or the run ends, never released by publish * count), and a baseline is removed only once no generation on disk references it. * `analyze` is the same validation as a PURE dry run: it persists nothing and moves no revision; a * `blocked` publish persists nothing either — not the drift it observed, not the provisioning state. * * # `current` is never trusted — for the daemon's whole lifetime * * `currentSnapshot()` realpaths the link, requires the target to be a generation directory directly * under `snapshots/`, requires a well-formed `snapshot.json` whose `gen` matches the directory and * whose skill rows are safe relative `skills/…` dirs with the path-derived name, and re-hashes the * tree against `contentHash` before it exports the path or reads the metadata — on EVERY call * (codex round 2: a verification cached by link text was ineffective after its first success; a * snapshot modified under a running daemon is refused by the same store instance). Any failure is * `SkillsCurrentInvalidError` — a loud config error, never a silent "not published". * * One thing is NOT a failure (F-083): a generation published under OTHER portability rules. Every * publish records the rules identity (`rulesVersion` / `rulesSha256` — refs.ts * `PORTABILITY_RULES_IDENTITY`); when the running daemon's identity differs, or the snapshot * predates the field (every pre-0.7.31 publish), the row re-derivation reports the rows that derive * differently as `CurrentSnapshot.drift` and the generation is ACCEPTED — the runtime raises ONE * `skills.stale-rules` warning naming them and the re-publish remedy. A rule change is not * tampering, and an upgrade must never turn a valid generation into a refusal (0.7.29 → 0.7.30 did * exactly that: every seat launched without skills until an operator re-published). Under the SAME * identity a row that derives differently is refused as before; a content or metadata hash * mismatch is refused under any identity. */ import type { CoreEvent, SkillAnalyzeResult, SkillConflictFinding, SkillEntry, SkillFileTree, SkillKind, SkillManifest, SkillMutationResult, SkillPortability, SkillPortabilityReason, SkillPublishResult, SkillReadResult, SkillRefreshResult, SkillSourceKind, SkillVenvState } from '../core/types.js'; import { NotAPluginRootError } from './bundle.js'; import { LiveGenerations } from './live-generations.js'; import { type PluginSource } from './plugin-source.js'; import { type InstalledPluginState } from './installed.js'; import { type PortabilityRulesIdentity } from './refs.js'; import { type Pacer } from './tree.js'; /** The root-level names the store creates live in ONE table (`root-names.ts`, design v3.5 §2) — re-exported for the tests that observe the flip. */ export { CURRENT_TMP_PREFIX } from './root-names.js'; import { type VenvProvisioner } from './venv.js'; /** The root's name under the daemon state home — a top-level entry `tests/fixtures/state-home-subtrees.json` registers. */ export declare const SKILLS_DIRNAME = "skills"; export declare const MANIFEST_FILENAME = "manifest.json"; export declare const EFFECTIVE_DIRNAME = "effective"; export declare const BASELINE_DIRNAME = "baseline"; export declare const SNAPSHOTS_DIRNAME = "snapshots"; export declare const CURRENT_LINKNAME = "current"; export declare const SNAPSHOT_MANIFEST_FILENAME = "snapshot.json"; /** The generated delivery views inside a snapshot (design v3.2 §4). */ export declare const VIEWS_DIRNAME = "views"; /** The copilot view root inside a snapshot — what core passes as `--add-dir`. */ export declare const COPILOT_VIEW_REL = "views/copilot"; /** Where the copilot view lays its skills out: copilot loads `.github/skills//SKILL.md` from an added dir. */ export declare const COPILOT_VIEW_SKILLS_REL = "views/copilot/.github/skills"; /** * Root-level names the store itself owns — refused as support paths (codex round 2: a support * file named `snapshot.json` published `clear` and then failed `current` verification because * publish overwrote it with the generated metadata). */ export declare const RESERVED_SUPPORT_NAMES: ReadonlySet; export declare const MANIFEST_VERSION = 2; /** Generations kept after a publish (the newest `current` included) — older ones are reaped. */ export declare const KEEP_GENERATIONS = 3; /** * Where the root lives: `/skills` — full stop (design v3.1 §1: the same storage root as * every other crew store; v3.2 §1: never a user CLI directory). There is no setting and no env * override (codex round 5 on #480: a configurable root let seeding write into `~/.codex/skills`); * the daemon's state home is the ONE knob (`--db`), and the boot asserts the resolved root stays * fenced (root-fence.ts). Tests redirect the state home, never the root. */ export declare function resolveSkillsRoot(): string; /** The root has no manifest: nothing was seeded (no installed plugin, or the seed is not armed). */ export declare class SkillsUnseededError extends Error { readonly root: string; constructor(root: string); } /** No plugin source could be found to seed or refresh from — crew does not vendor garden (v3 §8; the discovery order is design v3.6, plugin-source.ts). */ export declare class SkillsSourceUnavailableError extends Error { /** * crew#753: discovery found an installed garden but it is older than the required minimum, so it * was passed over. `null` when nothing was installed at all. */ readonly tooOld: { path: string; version: string; } | null; constructor(detail: string, tooOld?: { path: string; version: string; } | null); } /** `manifest.json` exists but is not a manifest — refused loudly, never blanked (it holds enablement). */ export declare class SkillsManifestCorruptError extends Error { readonly path: string; constructor(path: string, detail: string); } /** * `current` exists but does not point at a valid published generation — a loud config error. The * store never exports or reads through a link it could not verify (design v3 §3: "explicit path * invalid → fails loudly"). */ export declare class SkillsCurrentInvalidError extends Error { readonly link: string; constructor(link: string, detail: string); } /** A publish could not land its generation directory without destroying an existing one, or could not lock it. */ export declare class SkillsPublishError extends Error { constructor(detail: string); } /** A publish is already in flight — one at a time. The route answers a 2xx `blocked` * `publish-in-flight` envelope (nothing was written), never a 409 (codex round 3). */ export declare class SkillsPublishInFlightError extends Error { readonly revision: number; constructor(revision: number); } /** The skills root changed identity (its canonical path moved, or it vanished) while a publish was * awaiting its baseline env — the operation is aborted, nothing was written. The route answers a * 2xx `blocked` `root-changed` envelope, not a 409 (codex round 3). */ export declare class SkillsRootChangedError extends Error { readonly root: string; readonly revision: number; constructor(root: string, detail: string, revision: number); } /** * The skills root ITSELF is not the directory the store bound: a symlink stands in for it, it is * not a directory, or its canonical path moved under the running daemon. Every read and mutation * is refused (a loud 503 on the routes, `skills.config` at boot) — nothing is ever read or written * through it (codex round 4). */ export declare class SkillsRootInvalidError extends Error { readonly root: string; constructor(root: string, detail: string); } /** * A `baseline/` on disk does not hash to `` (a bundle file modified, planted or removed, * a symlink inside) — content-addressed baselines are verified before EVERY reuse (codex round 7): * a refresh refuses to reuse it (its 2xx `blocked` `baseline-corrupt` envelope, nothing copied), * reset refuses to restore from it, publish/analyze report it blocking; only the seed re-captures. */ export declare class SkillsBaselineCorruptError extends Error { readonly dir: string; constructor(dir: string, detail: string); } export declare class UnknownSkillError extends Error { readonly skillName: string; constructor(skillName: string); } /** The caller's `expectedRevision` is stale — the 409 of every mutation. */ export declare class RevisionMismatchError extends Error { readonly expected: number; readonly actual: number; constructor(expected: number, actual: number); } /** Which copy of a file a read addresses. */ export type ReadSide = 'effective' | 'baseline'; /** One skill row of `snapshot.json` — what core checks a seat's invocability against (v3.1 §5). */ export interface SnapshotSkillRow { name: string; /** Plugin-relative dir, nested layout verbatim. */ dir: string; kind: SkillKind; core: boolean; /** REQUIRED boolean: non-Claude seats may only invoke portable skills. */ portable: boolean; /** Per-reason portability (F-079): `portable` again, the sorted unique reasons, up to five * `file:line` anchors. Written by every publish since 0.7.30; absent on older generations * (verify then re-derives `portable` alone). Core ignores it (`portable` is its admission key). */ portability?: SkillPortability; /** `true` when the dir is not directly under `skills/` — Claude Code discovers top-level skill dirs * only, so a nested skill is not invocable for a Claude seat (core enforces; crew just says so). */ nested: boolean; } /** One generated delivery view inside a snapshot: its root (snapshot-relative) and the skills it carries. */ export interface SnapshotView { /** Snapshot-relative dir core hands the CLI (`views/copilot` → `--add-dir /views/copilot`). */ dir: string; /** Frontmatter names of the enabled, PORTABLE skills laid out in the view, sorted. */ skills: string[]; } /** `snapshot.json` inside every published generation — what the engine reads (never a parent dir). */ export interface SnapshotManifest { gen: number; contentHash: string; gardenSource: { kind: SkillSourceKind; path: string; plugin_version: string; baseline: string; }; /** The baseline env's state at publish: `synced` ⇒ `.venv` links it; `skipped` ⇒ no link (nothing to provision). */ venv: SkillVenvState; /** The portability rules the rows were judged under (F-083): `rulesVersion` = the canonical * fixture's `version`, `rulesSha256` = the digest of the live rule table * (`PORTABILITY_RULES_IDENTITY`). Written as a PAIR by every publish since 0.7.31; ABSENT on every * older generation. `current` verification compares it with the running identity: a generation * published under OTHER (or unrecorded) rules is ACCEPTED — its rows are re-derived for readers * (`CurrentSnapshot.drift`) and reported as `skills.stale-rules` — never refused for deriving * differently. The immutable snapshot is never rewritten; a re-publish records the running identity. */ rulesVersion?: number; rulesSha256?: string; skills: SnapshotSkillRow[]; /** The generated delivery views (design v3.2 §4) — today only `copilot`. */ views: { copilot: SnapshotView; }; } /** One skill row whose recorded portability is not what its files derive under the RUNNING rules (F-083). */ export interface SnapshotRowDrift { name: string; /** What the generation records: `portable`, and the per-reason claim when the row carries one (`null` on a pre-0.7.30 row). */ recorded: { portable: boolean; reasons: SkillPortabilityReason[] | null; }; /** What the row's own files derive under the rules this daemon runs — what a re-publish would record. */ derived: SkillPortability; } /** * The verified current generation — what `currentSnapshot()` answers, `GET /skills` reports as * `current`, and the engine is handed (`path`). `rules` names the portability rules the generation * was published under (`recorded`; `null` when the snapshot predates the identity — every * pre-0.7.31 publish) beside the ones this daemon runs (`running`); `stale` when they differ or the * recorded one is absent. `drift` is the rows that derive differently today — non-empty only under * stale rules (under the SAME rules a row that derives differently is tampering, and the generation * is refused). `gen` and `path` are the wire shape api-types 0.35.0 declares; `rules` and `drift` * are additive (declared by the next api-types cut). */ export interface CurrentSnapshot { gen: number; path: string; rules: { recorded: PortabilityRulesIdentity | null; running: PortabilityRulesIdentity; stale: boolean; }; drift: SnapshotRowDrift[]; } /** Whether a skill dir is nested: anything deeper than `skills/

`. */ export declare function isNestedSkillDir(dir: string): boolean; export interface SeedResult { /** `false` when the root was already seeded (idempotent no-op). */ seeded: boolean; baseline: string | null; source: PluginSource | null; } export interface SkillsStoreOptions { root: string; /** Every `skill_ref` of every workflow the daemon knows — the seeds of the core closure. Read at use time. */ registeredSkillRefs: () => ReadonlySet; /** Per-baseline `uv sync` (venv.ts), AWAITED by publish. Tests pass `noVenv`; the daemon passes `uvSyncBaseline`. */ provisionVenv: VenvProvisioner; /** Plugin-source discovery; defaults to the installed plugin (plugin-source.ts, design v3.6), whose discovery findings the default logs through `warn`. */ source?: () => PluginSource | null; /** Clock, ISO-8601 (tests pin it). */ now?: () => string; warn?: (message: string) => void; /** * The pacer a publish runs its tree steps under (crew#852): default `budgetPacer` — the loop * turns whenever a slice's budget is spent. Tests inject one that observes or interleaves at a * yield (a write landing mid-publish) deterministically. */ pacer?: () => Pacer; } /** The dir a user-added skill lands at: always top-level (`skills/`). */ export declare function dirForUserSkill(name: string): string; /** Zero-padded so a lexical listing of `snapshots/` is the generation order. */ export declare function generationDirName(gen: number): string; export declare class SkillsStore { private rootDir; private readonly registeredRefs; private readonly provisionVenv; private readonly sourceFn; /** The injected source (tests, `WICKED_CREW_SKILLS_SOURCE` harnesses), or `null` for live discovery — `installedState` reads it quietly. */ private readonly injectedSource; /** What the last automatic discovery passed over (crew#753: names a too-old garden in the refusal). */ private lastDiscoveryFindings; readonly now: () => string; private readonly warn; /** Generations live runs may still read — the reaper keeps them (fed by `observeEvent`). */ readonly live: LiveGenerations; /** The one publish that may run at a time (module header) — `null` when none is in flight. */ private publishInFlight; private readonly makePacer; /** One provisioning per baseline hash: a concurrent caller awaits the in-flight one. */ private readonly venvInFlight; /** * Where the environment seconds went since this store was built (crew#741): the ms the * provisioner path ran (torn-env removal, `uv sync`, marker, lock, verify) and how many times it * reached the provisioner. A boot over a baseline whose env already carries the verified ready * marker reads `0 / 0` — `ensureVenv`'s fast path never gets here. Read by the daemon's boot * stage line (`api/server.ts`), never persisted. */ private readonly venvStageTotals; /** The canonical (realpath) identity of the root the store BOUND — recorded the first time the * root is seen as a real directory (boot / seed), compared on every operation for the store's * whole lifetime (the root is never re-aimed: a new root is a new store). */ private boundRootReal; /** * `verifyCurrent`'s last VERIFIED answer (DES-L6 PR-L6-1 (c), crew#547): kept only while the * generation's lstat fingerprint (tree.ts `fingerprintTree`) is the one taken BEFORE that * verification — equal → the byte hash and the row re-derivation are skipped; anything else → * the full verification runs and refreshes it. Cleared on every `current` flip. */ private verifiedCurrent; /** * `manifest()` memo keyed on the manifest FILE's identity `(ino, size, mtimeMs, ctimeMs)`: the * same lstat every read already performs decides whether the bytes are re-read, re-parsed and * re-validated. Callers get a structured clone — they mutate before `commit`, never the memo. */ private manifestMemo; constructor(opts: SkillsStoreOptions); get root(): string; /** Whether a publish is running right now (diagnostics + tests). */ isPublishing(): boolean; /** * The `venv` boot stage (crew#741): provisioner time and count since construction. `{ ms: 0, * provisions: 0 }` is the proof that a boot on an unchanged baseline re-ran no `uv sync`. */ venvStage(): { ms: number; provisions: number; }; effectiveDir(): string; baselineDir(hash: string): string; snapshotsDir(): string; snapshotDir(gen: number): string; currentLink(): string; /** The daemon's uv cache (`UV_CACHE_DIR`) — a writable cache under the root, not the operator's. */ uvCacheDir(): string; private manifestPath; private pluginPath; /** `effective/` after the lstat walk from the ROOT — the skill dir itself is a walked component. */ private containedEffective; private containedBaseline; /** * The skills root ITSELF must be a REAL directory whose canonical path is the one the store bound * at boot — checked before EVERY read and mutation, not only at publish (codex round 4: the * containment walk started BENEATH the root, so a root replaced by a symlink to a copied store * after boot redirected file reads, writes and manifest commits outside the configured root). * Refuses a symlink standing in for the root, a non-directory, and a root whose realpath moved (an * ancestor replaced under the running daemon). A root that does not exist yet is not refused — * there is nothing to redirect through; `manifest()` reports it unseeded and the seed binds it * once it has created it. Throws `SkillsRootInvalidError`. */ private assertRootIdentity; /** A skill's dir must be reachable without crossing a link before anything walks or writes it. */ private assertSkillDirContained; /** * The structural storage directories — the root itself and its `effective/`, `baseline/`, * `snapshots/` children — must be REAL directories, never symlinks (codex round 3). The * per-destination walk `copyFiles`/`writeFileAtomic` do starts BENEATH a staging dir, so a * `snapshots -> /outside` (or `baseline -> …`) redirect would take a publish, a baseline * capture, an env provisioning, a reap or a `current` verification outside the store before that * walk ever ran. Refuses a symlink at the `skills` component and each child (the operator's * state-home path ABOVE the root is theirs — only the store's own dirs are checked). Throws * `SymlinkComponentError`; callers map it to their own refusal. */ private assertStorageAncestorsClean; isSeeded(): boolean; /** The manifest, or `SkillsUnseededError` / `SkillsManifestCorruptError` / `SkillsRootInvalidError`. */ manifest(): SkillManifest; /** * The COMPLETE runtime schema of `manifest.json` (codex round 7): every field is typed strictly * and ONE malformed field refuses the whole manifest — `manifest-invalid` → * `SkillsManifestCorruptError`, the routes' 503 and the daemon's `skills.config` — never a * truthiness fallback (`"enabled": "false"` used to load, and a `!== true` check read it as * disabled while `=== false` read it as enabled). Enums (`kind`, `provenance`, `source.kind`, * `venv`), hashes (64-hex, or `null` where allowed), integers (`revision` ≥ 0, `published.gen` * ≥ 1), strings, unknown or missing keys — all refused by name. Every PERSISTED path is validated * here too (codex rounds 3/4): a skill `dir`, a baseline identifier, a file-record key and the * skill KEY itself (a safe single segment that IS the path-derived name of its `dir` — the copilot * view lays a skill out under `views/copilot/.github/skills//`; the manifest key, the * directory and the invocation identity are one thing) — a manifest is disk state an attacker (or * a bad merge) can craft, never a path the store follows out of the root. The same validator runs * on every manifest the store is about to WRITE (`writeManifest`). The answer is a fresh object * holding exactly the validated fields. */ private validateManifest; revision(): number; /** Write the manifest with its revision bumped — every mutation ends here. */ private commit; private writeManifest; private assertRevision; /** * A MUTATION while a publish runs is refused before it touches anything (crew#852): the publish * now yields to the event loop between tree steps, so an API write could otherwise commit between * its validation and its commit — the publish would then either fail its own CAS or stage content * its manifest no longer describes. Same envelope as a second publish (`publish-in-flight`, 2xx, * nothing written — re-read and retry); before the pacing a write in that window queued behind * the blocked loop and failed its CAS after the publish landed. Reads are never refused. */ private refuseDuringPublish; /** * The published snapshot `current` resolves to, VERIFIED (module header), or `null` when there * is no link (never published). `path` is the absolute REAL path of the generation — exactly the * value the engine is handed as `WICKED_SKILLS_SNAPSHOT` (v3.1 §2). A link that exists but fails * verification throws `SkillsCurrentInvalidError`. Verified on EVERY call — never memoized by * link text (codex round 2): the answer holds for the daemon's whole lifetime only because it is * re-derived each time it is handed out. */ currentSnapshot(): CurrentSnapshot | null; /** * `currentSnapshot()` on the event loop (crew#852): the same verification, step for step, with * its walk, fingerprint and hash paced so a request landing mid-verification is answered within * one slice instead of after the whole generation. The route layer's publish and `GET /skills` * drive this one; boot and the synchronous readers keep `currentSnapshot()`. */ currentSnapshotPaced(pacer: Pacer): Promise; private currentSnapshotSteps; /** * The skill rows of the VERIFIED current generation — what the engine's intake admission judges * a run's `skill_ref`s and `base_skill_ref` against (crew#554 / core#468) — as `{gen, skills}`, * or `null` before the first publish. Reads `snapshot.json` at the verified path through the * same structural check every reader uses; a generation that verifies but whose metadata will * not re-read is answered as `null` (nothing is KNOWN to be handed), never as an empty catalog. */ currentSnapshotSkills(): { gen: number; skills: string[]; } | null; /** The garden a published generation was built from (`snapshot.json` `gardenSource`), or `null` when its metadata does not read (crew#874). */ snapshotGardenSourceOf(path: string): { plugin_version: string; baseline: string; } | null; /** `currentSnapshotSkills()` for a generation the caller holds VERIFIED already (the ladder's own, just exported) — the metadata read alone, no re-walk (crew#852). */ snapshotSkillsOf(current: CurrentSnapshot): { gen: number; skills: string[]; } | null; private verifyCurrentSteps; /** * Re-derive every claim of a skill row from the generation's OWN files (codex round 7; the * metadata hash authenticates the rest): `dir/SKILL.md` must be in the tree, `kind` must be what * its frontmatter derives, `portable` what the skill's own files (nested subtrees excluded) * derive, `nested` what the dir spells (checked at parse); the copilot view ON DISK must lay out * EXACTLY the sorted set of portable rows — no skill missing, no extra directory — and nothing * else may sit under `views/`. `core` is the registered-reference closure AT PUBLISH (the * registered set may legitimately move afterwards), so it is authenticated by the metadata hash * rather than re-derived. Read-only mode bits are re-checked by nobody: they are a guard against * accidents, never the integrity boundary — the hashes are. * * `staleRules` (F-083): the generation records a portability rules identity other than the running * one (or none). The portability re-derivation then answers the rows that derive differently as * `drift` instead of a problem — the bytes are authenticated, only the verdict moved. Everything * else (a missing SKILL.md, a kind claim, the view shape against the RECORDED rows) is judged the * same under any rules: none of it depends on the rule table. */ private snapshotRowsProblemSteps; /** Hash over a snapshot tree — files (`snapshot.json` excluded), link entries (path + link text) AND directory entries (codex round 9: an extra empty directory changes it). */ private snapshotHash; private snapshotHashSteps; /** The link text publish writes for a snapshot's `.venv`: relative on POSIX, the absolute target for a Windows junction. */ private venvLinkText; /** * Why a generation's symlinks are not acceptable, or `null`: any link other than the root-level * `.venv` is refused by name and target; `.venv` may exist only when `snapshot.json` records the * env as `synced`, must carry EXACTLY the link text publish writes (the relative path to this * root's `baseline//.venv`), and must resolve to that very directory (a * dangling or redirected link is refused — a worker's `uv run` through it would create or read * an env somewhere else). A `synced` snapshot WITHOUT the link is not what publish wrote either. * * The link is verified INDEPENDENTLY of the metadata text (codex round 6: an altered manifest, * link and recorded hash used to let an outside-pointing `.venv` verify, because the expected * target was JOINED from a `gardenSource.baseline` validated only as a string). The only two facts * of `snapshot.json` this decision consumes are that the baseline is a 64-hex content hash * (`parseSnapshotManifest`) that `manifest.json` knows (`verifyCurrent`); the env it must reach is * then walked FROM THE ROOT with lstat — no link at `baseline`, `` or `.venv` — must be a * REAL directory, and the link's canonical target must equal that directory's canonical path * inside the canonical root. */ private snapshotLinkProblem; /** `snapshot.json` at `dir`, structurally validated (`readSnapshotMetadata`) — for readers that need no authentication (baseline retention). */ private parseSnapshotManifest; /** * `snapshot.json` at `dir`, read NO-FOLLOW and structurally validated — with the sha256 of its * exact bytes (`rawSha`, what `manifest.published.snapshotHash` authenticates; codex round 7) — or * the reason it is not one. Every field a decision consumes is regex- or enum-validated here * (codex round 6): `gardenSource.baseline` must be a sha256 content hash (it authorizes the `.venv` * link — a free string could carry `..`), `venv` one of the four states, `gardenSource.kind` a * known source kind, every skill row fully typed (`isSnapshotSkillRow`), rows sorted and unique, * the view block naming EXACTLY the portable rows. */ private readSnapshotMetadata; /** The verified `snapshot.json` of a published generation. */ readSnapshotManifest(dir: string): SnapshotManifest; /** * Fold one CoreEvent into the live-generation ledger (v3 §1 reaping rule): a live session pins * the EXACT generation the engine reports it was handed (`skillsSnapshotHanded`), plus every * generation published while it stays live; a launch pin the daemon opened when it handed the * launch to the engine (`live.launched`) is released by that report or the terminal frame — never * by publish count (codex round 4); a terminal frame releases the pins and reaps what no other * live session or open launch holds. The daemon calls this from its one `adapter.onEvent` * listener (live-generations.ts). */ observeEvent(event: CoreEvent): void; /** Reap generations beyond the newest `KEEP_GENERATIONS` that no live session or open launch pins (no-op before a publish). */ reapStale(): void; /** * Seed the root from the live plugin when it has no manifest. Idempotent: a seeded root is left * alone. A torn earlier seed (dirs but no manifest) is cleared and redone. Throws * `SkillsSourceUnavailableError` when no plugin is installed, and `PluginSourceSymlinkError` when * a designated entry of the source is a symlink (codex round 6) — BEFORE the root is created: * nothing is copied, the runtime reports it as `skills.config` naming the entry. */ seed(): SeedResult; private requireSource; /** * Copy the bundle to `baseline//` through a staging dir (a torn copy never bears the hash), * LOCKED read-only before it lands. An EXISTING `baseline/` is reused only after its tree * re-hashes to `` (codex round 7): a dir that merely exists proved nothing — modified, * planted or pre-planted content would have been restored by reset and published as ordinary * drift. On a mismatch the seed RE-CAPTURES over it (`recapture`: the seed is creating the root's * first state and says so); a refresh REFUSES (`SkillsBaselineCorruptError` → its 2xx `blocked` * `baseline-corrupt` envelope, nothing copied). */ private captureBaseline; /** * Why `baseline/` is not the bundle its name claims, or `null`: a real directory whose tree * — links ENUMERATED (a link inside is a corruption, never followed), the per-baseline `.venv` * excluded (it is provisioned INTO the dir after capture; `SKIP_DIR_NAMES`) — hashes to `` * (`hashFileSet`, the identity the seed computed). Re-derived on EVERY reuse (codex round 7): * before a refresh reuses it, before publish provisions in it and after the provisioner ran * (`validate`), and per file before reset restores from it. Read-only mode bits are a guard * against accidents, never the integrity boundary — this hash is. * * The pruned subtrees stay pruned AND UNCLASSIFIED here (codex round 10; `walkTree(...).pruned` * is deliberately not consulted), unlike under `effective/`: the provisioned `.venv` is an * interpreter environment and legitimately holds symlinks (`bin/python -> python3.x`, * `lib64 -> lib`). That is safe because nothing beneath a pruned directory is ever DELIVERED * except through the snapshot's `.venv` link, whose target both verifiers check by identity — * `snapshotLinkProblem` here (this root's `baseline//.venv` for the recorded baseline, the * link text as publish wrote it) and core on its side (every component a real directory, the * target ending exactly at `/<64-hex>/.venv`). A link inside the env reaches a worker * only as part of the env the store itself provisioned; the bundle identity re-derived here never * covers it, and no other pruned name is linked from anywhere. */ private baselineProblem; /** `baselineProblem` as steps (crew#852): it walks AND re-hashes the whole bundle — the walk's yields, then one per file hashed. */ private baselineProblemSteps; /** * Lock a captured bundle's FILES read-only — every bundle file loses its write bits. Directories * stay writable: `.venv` is provisioned into the top dir afterwards (venv.ts — the provisioner may * write only there, and publish re-hashes the bundle after it ran), and the store's own reap and * re-capture unlink through them. Mode bits are a guard against accidental edits, NOT the * integrity boundary: `baselineProblem` re-hashes the tree on every reuse regardless of what the * bits say, and `reset` re-hashes every file it restores against the manifest's record. */ private lockBaseline; /** Give the operator's copies their owner-write bit back: a baseline file is locked read-only, an `effective/` file is theirs to edit. */ private restoreOwnerWrite; /** * Re-walk (lstat) and re-hash a staged tree AFTER the copy and BEFORE the swap/rename (design v3.5 * §3; codex round 8): every expected file present with its expected digest, nothing else, no * symlink. Answers why the staged tree is not what the copy was meant to produce, or `null`. */ private stagedTreeProblem; /** The blocking finding for a staged tree (or a copy source) that changed under the operation — nothing swapped, nothing written (v3.5 §3). */ private stagedTreeFinding; /** The blocking `baseline-corrupt` finding (codex round 7). */ private baselineCorruptFinding; /** Remove torn `.staging-*` (and legacy `.tmp-*`) dirs under `parent` — the idempotent-retry sweep. */ private sweepStaging; private baselineRecord; /** * A baseline env is READY only when its on-disk marker is present AND the tree is actually * read-only (codex round 3): the marker is written before the lock, so a marker whose lock never * took is NOT trust — the fast path and the post-lock verification both demand both. On POSIX the * marker's own mode bit and the venv root's prove the lock (a completed `makeTreeReadOnly` strips * every write bit); on Windows (no POSIX modes) the marker alone is the signal. */ private venvReady; /** * Provision the baseline's `.venv` unless it is already READY (marker + read-only bits verify), * and lock a synced env read-only. The marker is written, THEN the tree is locked, THEN both are * re-verified — and a FAILED lock removes the whole env (marker included) so nothing partial is * ever trusted and a retry re-provisions and re-locks from scratch (codex round 3: the marker * used to survive a failed lock and admit the env unconditionally on retry). PERSISTS NOTHING in * the manifest — the publish that succeeds records the state in its commit (a blocked publish * persists nothing, provisioning state included). One provisioning per hash: a concurrent caller * awaits the in-flight one. A `.venv` without a verified marker is a torn earlier sync and is * removed before `uv sync` runs again — the manifest is never the authority on what exists on * disk. AWAITED by publish. */ private ensureVenv; /** * The provisioning paths of baseline `hash`, VALIDATED before `ensureVenv` deletes, syncs, writes * a marker or chmods anything (codex round 4: the structural check covered `baseline/` but never * `baseline/`, so a symlink AT the hash redirected the env removal, `uv sync`, the marker * write and the read-only lock outside the store). Requires: the content-hash charset; the root * and `baseline/` intact; `baseline/` reached without crossing a link and a REAL directory * (a manifest naming a baseline that is not on disk is refused, not provisioned into a void); * `baseline//.venv`, its ready marker and the daemon's `.uv-cache` symlink-free (each may * not exist yet — the walk ends at the first missing component). Throws `SkillsPublishError`. */ private venvPaths; /** * Boot / settings entry point: wait out a publish in flight (never start a second one), seed * when unseeded, finish a publish a crash interrupted between the manifest commit and the * `current` flip, publish when nothing is published. Throws `SkillsSourceUnavailableError` for * the seed and `SkillsCurrentInvalidError` / `SkillsManifestCorruptError` for a corrupt root; a * blocked first publish is returned, not thrown. `source` is the plugin root the seed copied * from — its kind, path and plugin version — so the boot log names what was actually seeded (an * explicit `WICKED_CREW_SKILLS_SOURCE` checkout or directory as readily as the installed plugin; * Copilot on #480); `null` when the root was already seeded. */ ensureReady(): Promise<{ seeded: boolean; source: PluginSource | null; published: SkillPublishResult | null; current?: CurrentSnapshot; }>; /** * What `current` LEXICALLY names (its link text) — a hint for the torn-flip recovery, never a * verified answer: `null` when there is no entry at all, the generation number when the text names * a generation directory, `'other'` for anything else (a non-link entry, a target that is not a * generation dir) — which is never repaired, only refused by `verifyCurrent`. */ private currentLinkGen; /** Every managed file under `effective/`, hashed. */ private scanEffective; /** `scanEffective` as steps (crew#852): the walk's yields, then one per file hashed. */ private scanEffectiveSteps; /** Every manifest `dir` — the registered skills. */ private manifestDirs; /** * Every skill dir that OWNS files (v3 §6): the registered dirs plus every dir holding a `SKILL.md` * on disk in `effective/` — ownership follows the FILESYSTEM, so a nested skill created by a * direct edit (unregistered until publish reports it) still owns its subtree: a parent's * edit/reset/replace never reaches it, and the parent's endpoint refuses its paths (codex round 2). */ private ownershipDirs; /** Plugin-relative paths of the skill's own files ON DISK in `base` (nested skill subtrees excluded). */ private ownFilesIn; /** Manifest file records that belong to the skill (nested skills — registered or on disk — excluded). */ private ownRecords; /** * The baseline copy of a plugin-relative file, lstat-walked from the root (a symlinked baseline * ancestor is refused, never read through), or `null` when the baseline has no such file. */ private baselineFile; /** `sha256` of the baseline copy of `rel`, or `null` when the baseline has none. */ private baselineHashOf; private catalogView; /** * Rebuild the catalog from the file records: register skills for every `skills/**\/SKILL.md` * under its PATH-DERIVED name (a differing declared name is warned here and blocks at publish), * keep existing entries' state, recompute every content-derived field and the core closure. * Answers the recompute's refusals (a skill behind a symlink — `recomputeDerived`). */ private rebuildCatalog; /** * The manifest key for a freshly discovered skill dir: ALWAYS the path-derived name (v3 §5 — * identity is one thing). A declared frontmatter name that differs is warned here and reported * as `name-mismatch` (blocking) at publish; two dirs deriving the same name (`a-b/c` and `a/b-c`) * cannot both be registered — the second is left for publish to report as `unregistered-skill`. * The `SKILL.md` is read CONTAINED (lstat-walked from the root; codex round 5): a dir behind a * link is warned and left unregistered, never read through. */ private nameForDir; /** Every file record grouped by the skill dir that owns it (`null` = support), computed once per pass. */ private recordsByOwner; /** * Recompute provenance / kind / portable / upgradeAvailable / conflict for every entry, then the * core closure. `conflict` is `upgradeAvailable` (a file the last refresh saw change on both * sides — a user DELETION counts as the user's side) or a refresh's name-collision flag — the * latter only meaningful while the skill is user-added (upstream shipped the name at another * dir); refresh clears and re-derives it. * * CONTAINED (codex round 5): every byte this reads — a skill's `SKILL.md`, every recorded file it * judges portability from — is reached by an lstat walk from the SKILLS ROOT through the skill * dir and every component below it, never by a bare join that would follow `effective/skills/ * gamma -> /outside` because only the leaf was checked. A skill whose dir (or a file inside it) * crosses a link is SKIPPED — its derived fields keep their last values, nothing outside is read — * and the refusal is answered as a blocking `path-invalid` finding: publish/analyze report it * blocking; a mutation on ANOTHER skill carries it as a warning (that mutation did land). */ private recomputeDerived; /** `recomputeDerived` as steps (crew#852): it reads every file of every skill for the portability scan — one `yield` per file read, one per skill. */ private recomputeDerivedSteps; /** * The portability validator's view of the bundle (F-079): what the NEXT publish would carry — * support files inside the closure and ENABLED skills' own files. A disabled skill's files, a * file outside the closure, a vanished record: not there. Judging against this universe (rather * than everything under `effective/`) is what lets `snapshotRowsProblem` re-derive the SAME * reasons from the generation alone — a `../` link into a disabled skill is a broken link * (`unresolved-ref` at publish), not a portability fact about a file the snapshot omits. The * judged skill's own files are always visible to it (own-directory links stay portable anyway), * and `extra` lets a write guard see the files it is about to land. */ private publishedBundleView; /** The recompute's refusals as WARNINGS — for a mutation that landed on another skill (the blocking form is publish's). */ private recomputeWarnings; /** * Re-derive every manifest entry's derived fields under the RUNNING portability rules and commit * when any moved (F-083, review M2). `recomputeDerived` otherwise runs only on seed / refresh / * mutation / publish — never at boot — so after an upgrade `GET /skills` kept the previous daemon's * verdicts beside a `current.drift` that said otherwise: two halves of one response disagreeing. * The runtime calls this when the current generation is STALE (its recorded rules identity is not * the running one). The manifest is the EDITOR side — judged over `effective/` against the running * rules, what the next publish would record; the snapshot is what seats run by, and it is never * rewritten. Commits ONLY when a `kind` / `core` / `portable` / `portability` value actually * changed, so a stale-but-agreeing root does not move its revision on every boot (idempotent). * Answers the entries whose `portable` moved and the recompute's path refusals (warnings — a * refused skill keeps its previous values, as everywhere else). */ rederiveUnderRunningRules(): { moved: Array<{ name: string; from: boolean; to: boolean; }>; refused: SkillConflictFinding[]; revision: number; }; /** * The bytes of the REGULAR file at plugin-relative `rel` in `effective/`, every component * lstat-walked from the skills root (`containedEffective`; a link anywhere on the way is a * `SkillPathError` the caller decides on — never a read through it), `null` when absent. */ private containedEffectiveBytes; /** * The bytes of `abs` when it is a REGULAR file, `null` when absent or anything else. The leaf is * lstat'ed here; the PATH to it is the caller's business (`containedEffectiveBytes` / * `recomputeDerived` walk it from the root — a bare join is never enough, codex round 5). */ private regularFileBytes; /** Whether the current baseline ships a skill at `dir` (a user-added skill has no baseline dir). */ private hasBaselineDir; entryOf(name: string): SkillEntry; listFiles(name: string): SkillFileTree; /** * Contain a skill-relative path (the URL wildcard as Fastify hands it — ALREADY decoded once, never * decoded again; codex round 5): validate, refuse a path a NESTED skill owns (registered or merely * present on disk), lstat-walk FROM THE ROOT through the skill dir refusing symlinks. Returns the * on-disk target in `effective/` and both spellings of the path. */ resolveSkillFile(name: string, rawRel: string): { abs: string; rel: string; pluginRel: string; }; /** * Contain a root support path: not under `skills/` (those belong to a skill's endpoint), not a * name the store itself owns (`RESERVED_SUPPORT_NAMES`), INSIDE the bundle closure (the ONE * allowlist — bundle.ts `inBundleClosure`; codex round 6), no symlinks. */ resolveSupportFile(rawRel: string): { abs: string; rel: string; }; /** A typed, capped read of one of the skill's files (`side: 'baseline'` reads the shipped copy, contained the same way). */ readFile(name: string, rawRel: string, side?: ReadSide): Promise; readSupport(rawRel: string, side?: ReadSide): Promise; /** * The capped read as the wire spells it: `content` is `null` — not `""` — when the file is binary. * The leaf the containment walk judged is lstat'ed here and the read opens it `O_NOFOLLOW` with a * dev/ino identity check against that lstat (v3.5 §3): a link swapped in between the walk and the * open is refused, never served. */ private typedRead; private result; private blocked; /** * A containment failure on a WRITE is a guard result, not a transport error (design v3 §API: * "guard results ALWAYS return 2xx {verdict, findings, revision}"). Anything that is not a * `SkillPathError` is rethrown. */ private pathFinding; /** Re-hash the skill's own files on disk into the records (absent baseline files stay as `effectiveHash: null`). */ private refreshRecords; /** The named entry, or `UnknownSkillError` — checked BEFORE the revision: a missing resource is a 404, not a stale-client 409. */ private requireEntry; /** * Enable — with the content + containment guards a write gets (codex round 2: enable used to * flip the flag blind): the skill dir and its `SKILL.md` are lstat-walked from the root (a * symlinked skill is `path-invalid`), the `SKILL.md` must exist (`missing-skill-md`), parse * (`frontmatter-invalid`) and declare the path-derived name (`name-mismatch`) — a skill that * would block the next publish is not enabled. */ enable(name: string, expectedRevision: number): SkillMutationResult; /** Disable — core membership is RECOMPUTED here (registered refs are read at use time), never trusted from the cached entry. */ disable(name: string, expectedRevision: number): SkillMutationResult; /** * Restore the skill's own files from the current baseline (mode bits ride the copy). `enabled` * is untouched by design. Both sides are walked from the root (a symlinked baseline ancestor is * refused, never imported from; codex round 2). * * Restore candidates EXCLUDE every path a nested `SKILL.md` owns in the EFFECTIVE tree — not only * one present in the baseline (codex round 3): a child created directly under the parent * (`alpha/refs/SKILL.md`) owns its files even though the baseline has none, so a parent reset must * not restore baseline bytes over the child's. And the reset PREFLIGHTS every destination * (containment, no-follow) and STAGES the baseline bytes into a temp dir under the root BEFORE it * removes a single effective file, so a blocked reset (a symlinked `alpha/refs`) mutates nothing * — the `SymlinkComponentError` maps to the 2xx `blocked` envelope, never an escaped 500 that * already deleted the parent's SKILL.md (codex round 3). */ reset(name: string, expectedRevision: number): SkillMutationResult; /** Write one file inside the skill (containment via `resolveSkillFile` → a `blocked` envelope on refusal); guards on SKILL.md + support paths. */ writeFile(name: string, rawRel: string, content: string, expectedRevision: number): SkillMutationResult; /** * Stage ONE file for the park-and-place transaction (codex round 7): the content is written under * `staging/new` (an existing regular file's mode bits carried over), the existing file — if any — * is parked, the staged file placed. `commitSwap` then commits the manifest or rolls this back. */ private stageSingleFile; /** Write one root support file (`scripts/`, `schemas/`, `.claude-plugin/`, …) — always a warning. */ writeSupport(rawRel: string, content: string, expectedRevision: number): SkillMutationResult; /** * Every destination of a multi-file write, lstat-walked from the ROOT (the skill dir AND every * component below it): a symlinked child directory inside the skill would otherwise redirect a * descendant write outside the store (codex round 2: `gamma/link -> /outside` + `link/victim.txt`). * And every destination is checked for an entry OF THE OTHER KIND already standing there (codex * round 5): a key that names an existing directory (one the swap cannot clear — it holds anything * but the skill's own files and empty dirs), or a key whose would-be parent is an existing NON-own * regular file, cannot land; refused here, BEFORE any mutation, so the ENOTDIR/EISDIR the * placement would hit never happens after the old files are gone. Answers the blocking * `path-invalid` finding for the first refused path, or the resolved targets. */ private containedDestinations; /** Whether a directory holds nothing but the skill's own regular files (removed by the swap) and empty subdirectories. */ private clearableDir; /** * The atomic heart of `add` and `replace` (codex round 5): the replacement is WRITTEN in full into * a staging dir under the root, THEN the skill's own files are parked and the staged files placed * by the park-and-place transaction every multi-file swap shares (`swapStaged` — reset and refresh * go through the same one, codex round 6). Any failure ROLLS BACK: the skill is byte-for-byte * what it was, the revision unchanged. The removal used to come first, so a placement that failed * (`x` a file, `x/y` needing it as a directory) had already destroyed the notes it could not replace. */ private swapOwnFiles; /** * The park-and-place transaction EVERY content mutation goes through — replace/add (codex round * 5), reset and refresh-baseline (codex round 6), the single-file write (codex round 7). `park` is * every existing file the swap removes OR overwrites; `place` every staged source (already written * under `staging/new`) and its destination. * * 1. park: every `park` file is RENAMED into `staging/old` (never deleted), then the directories * it left empty are pruned up to `effective/`; * 2. place: every staged file is renamed into its destination (a destination that is now an * empty directory tree is removed first, parents are created). * * Every step is a same-filesystem rename. A failure INSIDE the swap rolls back — what was placed * is removed and its parents pruned, what was parked is renamed back (mode bits ride the rename) * — removes the staging and answers a blocking `path-invalid` finding: the tree is byte-for-byte * what it was. A swap that LANDED answers a `SwapHandle` whose parked originals stay under the * staging until `commitSwap` either committed the manifest or rolled the content back (codex round * 7): content and revision move together or not at all. */ private swapStaged; /** * The manifest half of a mutation whose content swap already LANDED (codex round 7): `finish` * updates the in-memory manifest (records re-hashed from the placed bytes, derived fields * recomputed from them — which is why it runs after the swap), then the manifest is validated and * written (`commit`: `manifest.json.tmp-…` + rename). If ANY of that fails — a record refresh, the * schema, the temp write, the rename into place — the content swap is rolled back from the parked * originals, so `effective/` and `manifest.json` move together or not at all: the request fails * with the error, the content is byte-for-byte what it was, and the persisted revision is * unchanged (it was never reusable against changed content). The parked originals are released * only after the commit landed or the rollback ran. */ private commitSwap; /** Remove `path` when it is a directory tree holding only (empty) directories; anything else is left alone. */ private removeEmptyDirTree; /** Add a user skill at `skills/` — staged, then swapped in (`swapOwnFiles`). */ add(name: string, files: Readonly>, expectedRevision: number): SkillMutationResult; /** * Replace the skill's own files wholesale (nested skills — registered or on disk — untouched); an * existing file's mode bits survive the replace. EVERY destination is lstat-walked from the root * and checked against entries of the other kind BEFORE the first byte moves, and the swap itself * is staged + rolled back on failure (`swapOwnFiles`): a refused or failed replace changes nothing. */ replace(name: string, files: Readonly>, expectedRevision: number): SkillMutationResult; private putGuards; /** * The portability validator's context for a write of `files` (skill-relative keys) into * `skillDir`: the would-be bundle plus the files about to land, keyed by the skill-relative * path the guard iterates. */ private writeContext; private filesGuards; private addGuards; private replaceGuards; /** * The installed plugin as `GET /skills` reports it (`installed.ts`), from the SAME source this * store seeds and refreshes from — discovered quietly (no discovery warnings: this is a probe the * diagnostics read on a timer, not a seed). */ installedState(): InstalledPluginState | null; /** crew#1021: runtime probes share the store's source without blocking requests on hash/git. */ installedStateAsync(): Promise; /** * Whether the catalog holds ANY operator change a refresh would have to merge (crew#874): an * edited/replaced/added skill, a refresh conflict, a file whose effective bytes differ from the * baseline it was captured from, or a file on disk under `effective/` the manifest does not * record (or records with other bytes). `pristine: true` ⇔ a refresh would take upstream for every * file — the only case the daemon may refresh and publish on its own. */ pristine(): { pristine: true; } | { pristine: false; reason: string; }; /** * Re-capture the live plugin as a new baseline and merge per file (base_old / base_new / * effective) with the v3 §7 truth table. A user DELETION is a modification: with upstream * unchanged the deletion stands; with upstream changed the deletion is kept and the file is a * `conflict` (the new side readable as `?side=baseline`). A held-back upstream skill (its * path-derived name collides with a user-added skill at another dir) is left in the baseline only * and flagged. The previous baseline dir is NOT removed here — a baseline lives while any * snapshot on disk references it (`reapBaselines`). * * ATOMIC against refusal (codex round 4): the merge is DECIDED in memory first; then EVERY * destination it would write or remove is preflighted (lstat-walked from the root, no symlink * component — `containedEffective`); only then is the new baseline captured, every taken file * STAGED into a temp dir under the root, and the swap (removals + renames) performed, followed by * the manifest commit. A refused destination — an upstream-added file whose `effective/` parent * is a symlink, say — answers the normal 2xx `blocked` `path-invalid` envelope with NOTHING * changed: no file copied or removed, no baseline captured, the revision unchanged (it used to * copy file by file and escape as a 500 after the files ahead of the refusal had landed). */ refreshBaseline(expectedRevision: number): SkillRefreshResult; /** PURE dry run of the publish validation: nothing is persisted, the revision does not move. */ analyze(): SkillAnalyzeResult; /** `analyze()` on the event loop (crew#852): the same whole-tree validation, its scan paced. Nothing is written either way. */ analyzePaced(pacer: Pacer): Promise; /** * ONE publish at a time (module header): a concurrent call is refused with * `SkillsPublishInFlightError` (the route's 409) rather than queued — the caller's revision would * be stale by the time a queued publish ran. */ publish(expectedRevision: number, afterCommit?: (result: SkillPublishResult, pacer: Pacer) => Promise): Promise; /** The root's identity a publish binds to: the configured path AND what it resolves to. */ private bindRoot; /** After EVERY await: the root must still be the very directory the operation started on. */ private assertRootUnchanged; /** * Bind the root, CAS, provision the baseline env (awaited), re-check the root identity and the * CAS, validate the whole tree (a failed provisioning is a blocking `venv-failed`), then write * `snapshots//` (staging + rename, never over an existing generation) with the generated * views, LOCK it read-only, COMMIT the manifest (venv state included), flip `current`, and reap * generations / baselines nothing references. A `blocked` verdict persists nothing — the caller's * revision stays valid. * * Nothing here holds the event loop for longer than one slice (crew#852): every walk, hash, copy * and lock over the tree runs as PACED steps (tree.ts `paced` / `budgetPacer`), so `/health`, the * run listing and the stall watchdog keep answering while a 15k-file catalogue publishes. The * loop therefore turns many times between the CAS and the commit — a read in that window sees the * previous generation and the manifest as it was (nothing is written before the commit); a WRITE * that commits in that window moves the revision, and the commit below re-checks the root identity * and the revision first: a moved revision removes the staged generation (no manifest, no * generation) and fails the caller's CAS exactly as a stale `expectedRevision` does. The * published-generation semantics are unchanged: `gen` increments once, the manifest lands before * `current` flips, a reader sees the old generation or the new one, never half of either. */ private publishSerialized; /** * The `unchanged` answer, or `null` when the slow path must run (DES-L6 PR-L6-1 (b); crew#547). * Byte-for-byte the slow path's `contentHash` — the validated file set + the copilot view files, * the `.venv` link entry exactly when the slow path would write one, the implied directories — * computed BEFORE `ensureVenv`. The link is deterministic in exactly two states: the baseline env * is READY (the slow path's `ensureVenv` answers `synced` without provisioning and links it), or * the baseline is recorded `skipped` with no env on disk (no `pyproject.toml`: nothing to * provision, no link). Any other state provisions, so the slow path decides. `unchanged` then * needs ALL of: a recorded publish; the candidate equal to its `contentHash`; `current` naming that * generation AND verifying (a tampered or unverifiable current generation is re-minted, never * declared current); `snapshot.json` being the metadata that publish wrote (its hash) and recording * the RUNNING portability rules identity — under other rules the rows may derive differently and * `skills.stale-rules` (F-083) must clear through a real publish. No manifest field, no schema * touch: a rollback reads the same manifest. A blocked validation is left to the slow path so the * findings (a `venv-failed` included) are reported exactly as before. */ private unchangedPublishSteps; /** * The generated delivery views (design v3.2 §2/§4) as snapshot-relative file records over the * EFFECTIVE bytes the validation admitted — today the copilot view only: every enabled, PORTABLE * skill's own files (nested subtrees excluded — they are their own skills) under * `views/copilot/.github/skills//`. A non-portable skill needs the plugin root, * the snapshot cwd or a sibling link, none of which a flat `.github/skills` layout provides. */ private viewFiles; /** The next generation: one past the highest that EXISTS (on disk or in the manifest) — never a reuse. */ private nextGeneration; private entryExists; /** * `current -> snapshots/`, flipped atomically: the link is created INSIDE `snapshots/` under * `.tmp-current-` — a name core's launch-time fence classifies (`snapshots/.tmp-*` is a * denied child, and v3.3 §1's listing of `snapshots/` accepts `.staging-*` / `.tmp-*` entries) — * and `rename()`d over `current` on the same filesystem, so NO unclassified child ever appears * under `skills/` (core#399 round 4: the former `skills/current.tmp-` would have refused a * launch that listed `skills/` in that window). The link's RELATIVE target (`snapshots/`) is * spelled for its FINAL location; while it sits in `snapshots/` it dangles and nothing reads it * there — a torn flip leaves a `.tmp-current-*` the next publish's staging sweep removes. */ private flipCurrent; /** A directory symlink: the given link text on POSIX (relative); Windows junctions need the absolute target (and it must exist). */ private symlink; /** * Remove every generation older than the newest `KEEP_GENERATIONS` — never the current one, and * never one a live session still pins (it is reaped by the release that frees it). */ private reapGenerations; /** The generations `reapGenerations(currentGen)` would remove: beyond the newest `KEEP_GENERATIONS`, never the current one, never a pinned one. */ private generationsToReap; /** * Drop, IN MEMORY, every baseline record that neither `m.baseline` nor any of `liveGens` (the * generations that will remain on disk) references — and every record whose directory is gone. * The caller commits `m` (a publish's or refresh's OWN commit, or the standalone reap's) and only * THEN removes the directories answered here (codex round 9): the record change rides a validated * commit with the revision advanced, a client never sees two manifests at one revision, and a * failed commit removes nothing. */ private pruneBaselineRecords; /** * Remove every baseline dir (and its record) that neither the manifest nor ANY generation on * disk references — a retained snapshot keeps the `.venv` it links alive. Dropping the records is * a COMMITTED, revision-advancing mutation (the validated `manifest.json.tmp-…` → rename path, * codex round 9), landed BEFORE any directory is removed. Answers the revision that commit * produced, or `null` when no record changed (nothing to reap, or an unseeded root) — directories * with no record left are still removed in that case. */ private reapBaselines; /** The generations present on disk, ascending. */ generationsOnDisk(): number[]; /** The baseline hashes present on disk (diagnostics + tests). */ baselinesOnDisk(): string[]; /** The 2xx `blocked` finding for a plugin source whose designated entry is a symlink (seed/refresh; codex round 6). */ private sourceSymlinkFinding; /** * Whole-tree validation (v3 §API refs — an ESCAPE blocking, a MISSING target a warning per design * v3.4 §1 —, §5 core closure, §6 nested ownership, the bundle closure as the ONE allowlist — * codex round 6). Mutates `m` IN MEMORY for bookkeeping only (drift into the file records, * derived fields, the core closure) — the caller decides whether that is persisted (publish * commits it; analyze and a blocked publish drop it). Never touches `effective/`. */ private validate; /** `validate` as steps (crew#852): the effective scan's yields (walk + per-file hash); the rules themselves are cheap. */ private validateSteps; } /** Re-export for callers that only need to classify the source-missing case. */ export { NotAPluginRootError }; //# sourceMappingURL=store.d.ts.map