/** * Doctor — `cleo skills doctor adopt-orphans` business logic. * * @remarks * An "orphan" is a skill directory that exists under any tracked path * (`~/.cleo/skills/`, legacy `~/.local/share/agents/skills/`, * `~/.agents/skills/` as a real dir) but has NO row in the per-user * `skills.db` registry described in * `docs/architecture/SG-CLEO-SKILLS-architecture-v3.md` §4. * * The handler offers four per-orphan dispositions: * * - **canonical-adopt** — REFUSED on a user machine. Canonical writes * must flow via PR to `packages/skills/skills/` (architecture-v3 §6 * invariant). The handler emits a refusal explaining the PR flow. * - **user-adopt** — inserts a row into `skills.db` with * `source_type='user'`, `lifecycle_state='active'`, `installedAt=now`. * - **delete** — archives the directory to * `~/.cleo/skills/.archive/-/` before unlinking from the * original location. * - **skip** — no action; the orphan is logged but otherwise ignored. * * All decisions are recorded to a structured JSON audit log at * `~/.cleo/skills/.audit-log/adopt-.json` regardless of mode. * * ## Chokepoint compliance (ADR-068) * * This module emits PURE DATA. The skills.db reads and writes are deferred * to caller-supplied callbacks (`loadRegisteredNames`, `recordRow`). The * `cleo` dispatch layer in `packages/cleo/src/cli/commands/skills.ts` plugs * the canonical `openSkillsDb`/`upsertSkillRow` helpers from * `@cleocode/core/store/skills-db`. * * Three execution modes: * * - default (TTY) — interactive prompt per orphan. * - `--non-interactive` — list orphans and exit without action * (read-only audit). * - `--auto-user-adopt` — bulk user-adopt all orphans without prompting * (safe default for `cleo-init`-style scripts). * * ## Locality (T9740 Wave B — T9744) * * Moved from `packages/caamp/src/commands/skills/doctor-adopt.ts` to CORE * so the cleo CLI can import this without crossing the `core → caamp` dep * boundary. The legacy Commander registrar in caamp was deleted at the * same time — cleo dispatches via citty and never wired the registrar in. * * @task T9744 * @epic T9740 * @saga T9560 * @architecture docs/architecture/SG-CLEO-SKILLS-architecture-v3.md §1, §6 */ /** * One orphan disposition decision. * * @public */ export type OrphanDecision = 'canonical-adopt' | 'user-adopt' | 'delete' | 'skip'; /** * Reason an action was refused (canonical-adopt on user machine, etc.). * * @public */ export interface OrphanRefusal { /** Stable code for programmatic handling. */ code: 'E_CANONICAL_ADOPT_REFUSED'; /** Human-readable explanation. */ message: string; /** Suggested next step (e.g. PR flow). */ remediation: string; } /** * A single orphan skill directory discovered on disk. * * @remarks * Named `DoctorAdoptOrphanRecord` (not `OrphanRecord`) to avoid colliding * with the existing `OrphanRecord` export in `./doctor.ts`. The two record * types describe distinct concerns (adopt-orphans flow vs. diagnostic * report) and intentionally diverge. * * @public */ export interface DoctorAdoptOrphanRecord { /** Skill name (basename of the orphan directory). */ name: string; /** Absolute path to the orphan directory on disk. */ path: string; /** Which tracked root this orphan was discovered under. */ discoveredVia: 'cleo' | 'legacy-agents' | 'home-agents'; /** Whether a `SKILL.md` sentinel exists at the root. */ hasSkillMd: boolean; /** Size of the directory in bytes (best-effort; 0 on stat failure). */ sizeBytes: number; } /** * Outcome of acting on a single orphan. * * @public */ export interface OrphanActionResult { /** The orphan that was acted upon. */ orphan: DoctorAdoptOrphanRecord; /** Decision the user (or flag) made. */ decision: OrphanDecision; /** Whether the action completed successfully. */ applied: boolean; /** Refusal payload when `applied=false` due to a policy block. */ refusal: OrphanRefusal | null; /** Where the directory was archived to (delete only). */ archivedTo: string | null; /** ISO-8601 timestamp when the action was taken. */ decidedAt: string; } /** * Top-level result returned in the LAFS envelope. * * @public */ export interface DoctorAdoptResult { /** Total orphans discovered. */ totalOrphans: number; /** Per-orphan action results. */ results: OrphanActionResult[]; /** Audit log file path (always written). */ auditLogPath: string; /** Execution mode used. */ mode: 'interactive' | 'non-interactive' | 'auto-user-adopt'; } /** * Pure-data payload emitted when a `user-adopt` decision is applied. * * @remarks * The dispatch layer translates this into an `upsertSkillRow` call via the * `openSkillsDb` chokepoint. Keeping it as pure data means the helper never * has to touch sqlite directly. * * @public */ export interface AdoptedSkillRowData { /** Skill name (PK in skills.db). */ name: string; /** Absolute install path on disk. */ installPath: string; /** Wall-clock timestamp the adoption occurred. */ installedAt: string; /** Always `'user'` for the adopt-orphans flow (canonical is refused). */ sourceType: 'user'; /** Always `'active'` post-adoption. */ lifecycleState: 'active'; } /** * Discover orphan skills across all tracked roots. * * @remarks * Visits the three tracked roots in priority order and de-duplicates by * basename — the first occurrence of `` wins (so a `~/.cleo/skills/` * entry shadows the same-named legacy entry). Symlinks under * `~/.agents/skills/` that resolve back into `~/.cleo/skills/` are dropped * because those are the bridge symlinks, not real orphans. * * Read-side IO (the set of names already known to `skills.db`) is supplied * via the `registeredNames` callback so this module never opens sqlite * directly — see ADR-068 chokepoint compliance in the file header. * * @param registeredNames - Pre-computed set of skill names known to the * registry. Callers in production wire this through `openSkillsDb()`; * tests wire it through a sandboxed `DatabaseSync` open. * @returns Sorted-by-name list of `DoctorAdoptOrphanRecord`s. * * @public */ export declare function discoverOrphans(registeredNames: ReadonlySet): DoctorAdoptOrphanRecord[]; /** * Callback signature for persisting a `user-adopt` decision. * * @remarks * Production wiring lives in `packages/cleo/src/cli/commands/skills.ts` and * funnels into `upsertSkillRow` from `@cleocode/core/store/skills-db`, * which routes through the canonical `openSkillsDb()` chokepoint. * Test code passes a sandboxed sqlite write. May be synchronous or async. * * @public */ export type RecordRowFn = (data: AdoptedSkillRowData) => void | Promise; /** * Apply a single decision to an orphan, returning a structured outcome. * * @remarks * This is the policy chokepoint — `canonical-adopt` is unconditionally * refused, `user-adopt` invokes `recordRow` with the canonical * {@link AdoptedSkillRowData} payload, `delete` archives-then-rms, and * `skip` records the intent without side effects. Errors during * `user-adopt` or `delete` produce an `applied=false` result with a * synthesised refusal payload rather than throwing, so the bulk loop can * proceed across all orphans. * * @param orphan - Orphan to act on. * @param decision - Decision to apply. * @param now - ISO-8601 timestamp to record on the result. * @param recordRow - Callback invoked to persist a successful `user-adopt`. * Tests pass a sandbox write; production passes the cleo-dispatch wrapper. * @returns A populated `OrphanActionResult`. * * @public */ export declare function applyDecision(orphan: DoctorAdoptOrphanRecord, decision: OrphanDecision, now: string, recordRow: RecordRowFn): Promise; /** * Write the audit log to `~/.cleo/skills/.audit-log/adopt-.json`. * * @remarks * The log is written atomically (tmp-then-rename) so a SIGINT mid-write * cannot leave a half-written file. The payload is a structured object * containing the run timestamp, mode, full per-orphan results, and a * stable `runId` UUID for cross-referencing in other CLEO audit streams * (e.g. release-ship logs). * * @param result - The doctor-adopt result to persist. * @returns Absolute path the audit log was written to. * * @public */ export declare function writeAuditLog(result: DoctorAdoptResult): string; /** * Options controlling a `runDoctorAdopt` invocation. * * @public */ export interface DoctorAdoptOptions { /** Skip prompting and write nothing — list-only audit mode. */ nonInteractive?: boolean; /** Skip prompting and bulk-adopt every orphan as `source_type='user'`. */ autoUserAdopt?: boolean; /** * Loads the set of skill names already known to the registry. * * Production wiring opens `skills.db` via `openSkillsDb()`; tests inject * a sandbox-scoped reader. */ loadRegisteredNames: () => ReadonlySet | Promise>; /** * Persists a single `user-adopt` decision to `skills.db`. * * Production wiring calls `upsertSkillRow` from `@cleocode/core/store`; * tests inject a sandbox writer. */ recordRow: RecordRowFn; /** Test-only injection of a readline-compatible prompt. */ prompt?: (orphan: DoctorAdoptOrphanRecord) => Promise; /** Test-only opt-out of writing the audit log to disk. */ skipAuditLog?: boolean; /** Test-only override for the discovery step. */ discoverFn?: (registeredNames: ReadonlySet) => DoctorAdoptOrphanRecord[]; } /** * Execute the doctor-adopt workflow and return a structured result. * * @remarks * Designed for both CLI invocation and direct testing — every side effect * is overridable via {@link DoctorAdoptOptions}. The function never throws * on per-orphan failures; instead each failure produces an `applied=false` * entry with a refusal payload, so the caller gets a complete report even * on partial failure. * * @param options - Mode flags + dependency-injected callbacks. The * `loadRegisteredNames` and `recordRow` callbacks are MANDATORY so the * caller (cleo dispatch or test harness) owns the sqlite open via the * chokepoint. * @returns The populated `DoctorAdoptResult`. * * @public */ export declare function runDoctorAdopt(options: DoctorAdoptOptions): Promise; /** * Default skill-name loader bound at CLI dispatch time. * * @remarks * Re-exported so the cleo dispatch layer can construct it once and inject * the same instance into {@link runDoctorAdopt}. * * @public */ export type RegisteredNamesLoader = () => ReadonlySet | Promise>; /** * Adapter callbacks the CLI registrar needs to satisfy * {@link DoctorAdoptOptions}'s mandatory deps. * * @remarks * The cleo CLI overrides both with chokepoint-routed implementations that * funnel through `openSkillsDb()` + `upsertSkillRow`. A no-op default * (`caampStandaloneAdapters`) is exported for environments (e.g. legacy * caamp standalone CLI) that do not have a live `skills.db`. * * @public */ export interface DoctorAdoptCliAdapters { loadRegisteredNames: RegisteredNamesLoader; recordRow: RecordRowFn; } /** * Standalone-CLI defaults — no DB access, every directory is an orphan. * * @remarks * Surfaces a clear error rather than silently no-op'ing. The cleo CLI * overrides this with the real chokepoint-routed adapters. * * @public */ export declare const caampStandaloneAdapters: DoctorAdoptCliAdapters; //# sourceMappingURL=doctor-adopt.d.ts.map