/** * Prune bundled skills that CLEO no longer installs — quarantining only what * CLEO provably wrote (T12678). * * `initCoreSkills` installs every `@cleocode/skills` manifest entry declared * `metadata.install: harness` but never removed anything, so a skill later * declared `internal` (ct-grade) or retired (ct-docs-lookup, …) stayed in * every harness. * * ## Ownership — never inferred * * The canonical skills root (`resolveSkillsRoot()`) is shared: CAAMP * installs every user skill there too, so "it is in the root" or "a harness * link points into the root" proves only that CAAMP put it there. The ONLY * proof CLEO accepts is its own **bundled-install ledger** * (`/.cleo-bundled.json`), written by `initCoreSkills` at install * time with a SHA-256 for every file it wrote. A skill is quarantined only * when: * * - it is a prune candidate (a non-harness manifest entry, or listed in the * manifest's `retiredSkills`); * - the ledger records it; * - its CAAMP lock entry, if any, has source `library:`, and its * skills.db row, if any, is `canonical` — anything else means a user * installed it; * - the files on disk still hash exactly to what the ledger recorded (a * user-edited copy is kept). * * A harness symlink is taken only when it points exactly at that skill's * canonical path; a harness copy only when it hashes to the ledger. Skills * installed before the ledger existed have no record and are reported, not * pruned. * * ## Reversible * * Nothing is deleted. Owned paths are moved into * `/skills-quarantine//` with a `quarantine.json` describing * every move, the lock entry removed and the skills.db state changed; * {@link restoreQuarantine} puts all of it back. * * @task T12678 */ /** File under the canonical skills root recording what CLEO installed. */ export declare const BUNDLED_LEDGER_FILE = ".cleo-bundled.json"; /** Directory name, beside the skills root, holding quarantined skills. */ export declare const QUARANTINE_DIR = "skills-quarantine"; /** One ledger record: the files CLEO wrote for a skill, by SHA-256. */ export interface BundledLedgerEntry { /** ISO timestamp of the install that wrote these files. */ installedAt: string; /** Relative file path → hex SHA-256. */ files: Record; } /** The bundled-install ledger. */ export interface BundledLedger { /** Format version. */ version: 2; /** Skill name → record. */ skills: Record; /** * Names the user restored from quarantine. The user chose to keep them, so * prune never takes them again — until {@link unkeepSkill} hands one back. */ kept?: string[]; /** * Per kept name, the hashes of the copy as restored (T12699). Only an * exact match lets {@link unkeepSkill} return the skill to CLEO. */ keptFiles?: Record>; } /** What happened to one path. */ export interface BundledSkillPruneAction { /** Skill name. */ name: string; /** Absolute path considered. */ path: string; /** `quarantined`, `would-quarantine` (dry run) or `kept`. */ action: 'quarantined' | 'would-quarantine' | 'kept'; /** Why. */ reason: string; } /** A CAAMP lock entry, as far as pruning needs it. */ export interface PruneLockEntry { /** Original source string (`library:` for bundled installs). */ source: string; /** Every other lock field, preserved for restore. */ [key: string]: unknown; } /** skills.db lifecycle states. */ export type PruneLifecycleState = 'active' | 'stale' | 'archived'; /** Registry access the prune needs (injectable for tests). */ export interface PruneRegistry { /** CAAMP lock entry for a skill, or null. */ lockEntry(name: string): Promise; /** Remove a skill's CAAMP lock entry. */ removeLockEntry(name: string): Promise; /** Put a removed lock entry back. */ restoreLockEntry(name: string, entry: PruneLockEntry): Promise; /** skills.db source type and lifecycle state, or null when no row. */ skillRow(name: string): Promise<{ sourceType: string; lifecycleState: string; } | null>; /** Set a skills.db row's lifecycle state. */ setLifecycleState(name: string, state: PruneLifecycleState, from?: string): Promise; } /** Everything one quarantine run changed, so it can be restored. */ export interface QuarantineRecord { /** Quarantine id (directory name). */ id: string; /** ISO timestamp. */ at: string; /** Moves performed, in order. */ moves: Array<{ name: string; from: string; to: string; }>; /** Lock entries removed. */ lockEntries: Array<{ name: string; entry: PruneLockEntry; }>; /** skills.db rows archived, with their prior state. */ rows: Array<{ name: string; priorState: string; }>; } /** Receipt for one prune run. */ export interface BundledSkillPruneReceipt { /** ISO timestamp. */ at: string; /** True when nothing was moved. */ dryRun: boolean; /** Names the bundled manifest says CLEO must not install. */ candidates: string[]; /** Per-path outcomes. */ actions: BundledSkillPruneAction[]; /** Quarantine id, when anything was moved. */ quarantineId: string | null; /** Failures (`path: message`). */ errors: string[]; } /** Inputs for {@link pruneBundledSkills}. */ export interface PruneBundledSkillsOptions { /** `<@cleocode/skills>/skills` — holds `manifest.json`. */ bundledSkillsDir: string; /** CLEO's canonical skills root. */ skillsRoot: string; /** Harness skills directories. */ providerSkillDirs: string[]; /** Lock and skills.db access. */ registry: PruneRegistry; /** Report only. */ dryRun?: boolean; /** Quarantine root; defaults to `/skills-quarantine`. */ quarantineRoot?: string; /** JSONL receipt log; appended on a non-dry run that moved anything. */ receiptPath?: string; } /** Shape of the bundled manifest fields this module reads. */ interface BundledManifest { skills?: Array<{ name?: string; install?: string; }>; retiredSkills?: string[]; } /** * Names the bundled manifest says must not be installed to harnesses. * * @param manifest - Parsed `skills/manifest.json`. * @returns Sorted unique candidate names. */ export declare function pruneCandidates(manifest: BundledManifest): string[]; /** * SHA-256 of every file under `dir` (relative POSIX paths), skipping * `__pycache__`. Empty when `dir` is missing. * * @param dir - Directory to hash. * @returns Relative path → hex digest. */ export declare function hashTree(dir: string): Record; /** * Read the bundled-install ledger; an absent, unreadable or older-format * ledger yields an empty one (nothing is then provably CLEO's). * * @param skillsRoot - Canonical skills root. * @returns The ledger. */ export declare function readBundledLedger(skillsRoot: string): BundledLedger; /** * Record the skills CLEO just installed from the bundle, hashing the files * it wrote into each canonical copy. * * @param skillsRoot - Canonical skills root. * @param names - Skills installed by this run. */ export declare function recordBundledInstalls(skillsRoot: string, names: string[]): Promise; /** * Quarantine harness entries and canonical copies of skills CLEO no longer * installs, where the ledger proves CLEO wrote them and they are unmodified. * See the module doc. * * @param opts - Paths, registry, dry-run flag. * @returns The receipt. */ export declare function pruneBundledSkills(opts: PruneBundledSkillsOptions): Promise; /** * List quarantine ids, newest first. * * @param quarantineRoot - Quarantine root. * @returns Ids that carry a `quarantine.json`. */ export declare function listQuarantines(quarantineRoot: string): string[]; /** * Put back everything one quarantine run moved: files and links, CAAMP lock * entries and skills.db lifecycle states. A skill whose path is occupied again * (the user reinstalled it) is reported as a conflict, and its lock entry and * row are left alone. Every cleanly restored skill joins the ledger's `kept` * list, so later prunes leave it in place. * * @param opts - Quarantine root and id, skills root, registry. * @returns What was restored and what could not be (destination occupied). */ export declare function restoreQuarantine(opts: { quarantineRoot: string; id: string; skillsRoot: string; registry: PruneRegistry; }): Promise<{ restored: string[]; conflicts: string[]; }>; /** Outcome of {@link unkeepSkill}. */ export interface UnkeepResult { /** Skill name. */ name: string; /** True when the skill left the kept list. */ unkept: boolean; /** True when CLEO owns it again (ledger record restored). */ reledgered: boolean; /** Why. */ reason: string; } /** * Hand a kept skill back to CLEO (`cleo skills doctor restore --unkeep * `, T12699). * * Restoring from quarantine marks a skill kept, so prune leaves it alone for * good. This reverses that only when CLEO can prove the canonical copy is * byte-for-byte what came out of quarantine: every file hashes to the * `keptFiles` record. A copy the user changed since the restore, or one * restored before hashes were recorded, stays kept. With no canonical copy * left there is nothing to own, so the name simply leaves the kept list. * * @param skillsRoot - Canonical skills root. * @param name - Kept skill name. * @returns What happened. */ export declare function unkeepSkill(skillsRoot: string, name: string): Promise; /** * The real registry: CAAMP's lock file and CLEO's skills.db. * * @returns A {@link PruneRegistry}. */ export declare function defaultPruneRegistry(): Promise; /** * Run the bundled-skill prune against this machine (`cleo skills doctor * prune`). * * @param opts - `dryRun` reports without moving anything. * @returns The receipt, or `null` when the bundled skills cannot be found. */ export declare function runBundledSkillPrune(opts: { dryRun?: boolean; }): Promise; /** * List quarantines, or restore one (`cleo skills doctor restore [id]`). * * @param id - Quarantine id; omit to list. * @returns The ids, or the restore outcome. */ export declare function restoreBundledSkillQuarantine(id?: string): Promise<{ quarantines: string[]; } | { restored: string[]; conflicts: string[]; }>; /** * {@link unkeepSkill} against this machine's canonical skills root. * * @param name - Kept skill name. * @returns What happened. */ export declare function unkeepBundledSkill(name: string): Promise; /** * Dry-run the prune `initCoreSkills` would perform against the real bundle, * canonical root, harnesses and registries. * * @returns One line per path that would be quarantined or kept. */ export declare function previewBundledSkillPrune(): Promise; export {}; //# sourceMappingURL=prune-bundled.d.ts.map