/** * `hq sync doctor` — bounded skill-key dedupe + local farm GC (US-003, * hq-sync-windows-skill-symlink-collision). * * The mixed-version amplifier (see `canonicalVaultKeySpelling` in * local-path-codec.ts) leaves the vault holding several percent-encoded * GENERATIONS of every `.claude/skills/:` wrapper key (`:`, `%3A`, * `%253A`, `%25253A`, …) and the local farm holding junk-named wrapper dirs. * US-001/US-002 stop NEW generations from being minted; this doctor is the * one-shot reconcile that collapses the families already in the wild: * * 1. Vault dedupe — list keys under `.claude/skills/`, group them by their * decode-to-fixpoint canonical form, let the newest-bodied spelling win, * (re)write the canonical `:` key when it is missing or stale, and delete * every non-canonical spelling. Deletion is gated hard: only bodies that * start with the `hq-symlink:` marker, or that are byte-identical to the * canonical twin, are auto-deleted — anything else is surfaced for manual * review, never silently removed. * 2. Local farm GC — remove `.claude/skills/` entries whose names decode to * a canonical wrapper that already exists on disk and whose content is * exclusively symlink-farm material. A real file anywhere is a refusal. * * Dry-run is the DEFAULT: the full keep/rewrite/tombstone/rm plan is printed * (counts + every key/path) and nothing is written until `--yes` * (`options.yes`). Remote deletes respect the sync-delete-bulk-asymmetry-guard * policy — same thresholds, env override, and refusal semantics as * `computeDeletePlan` in share.ts. Per-item failures are loud warnings, never * process-fatal (hq-sync-deliberate-skip-not-fatal-error-exit2). */ import { type ObjectIO } from "../object-io.js"; import { type ReconcileTwinsResult } from "../lib/conflict-reconcile.js"; import type { VaultServiceConfig } from "../types.js"; /** Vault prefix the doctor is bounded to — the skill wrapper link farm. */ export declare const SKILLS_KEY_PREFIX = ".claude/skills/"; /** * The narrow object-store seam the doctor needs. Structurally satisfied by * every `ObjectIO` transport; tests inject an in-memory fake (the same * dependency-injection convention the s3.ts suites use via * `setObjectIOFactory`). */ export type DoctorStore = Pick; export interface SyncDoctorOptions { /** Entity whose vault to reconcile (prs_/agt_ uid, cmp_ uid, or slug). */ entity: string; /** Vault service config (auth + region), as for `sync()`/`share()`. */ vaultConfig: VaultServiceConfig; /** HQ root whose local `.claude/skills/` farm the GC pass walks. */ hqRoot: string; /** Apply the plan. Default false = dry-run: print the plan, write nothing. */ yes?: boolean; /** * Explicit bulk-delete override (the `--force-bulk` CLI flag). Equivalent to * `HQ_SYNC_DELETE_BULK_OVERRIDE=1`; without either, a tombstone plan that * trips the asymmetry thresholds is refused (writes and local GC still run). */ bulkDeleteOverride?: boolean; /** DI seam for tests; defaults to the entity's resolved ObjectIO. */ store?: DoctorStore; /** * Platform override for local farm-name encoding (local-path-codec * convention). Decode-to-fixpoint always uses win32 semantics — junk * spellings are a key-space phenomenon, not a host one — but the canonical * LOCAL wrapper name a junk dir must collapse into is host-specific * (`ns%3Askill` on win32, `ns:skill` on POSIX). */ win32?: boolean; /** * The doctor takes the same per-root operation lock as `sync()`. Set when a * caller (runner/daemon) already holds it for this root. */ operationLockAlreadyHeld?: boolean; /** * `--reconcile-conflicts`: instead of the skill-key dedupe, walk the HQ root * for legacy sibling conflict twins (`.conflict--.`) * and reconcile each against its live file — the higher frontmatter * `version:` (else newer mtime) side becomes the live body, the loser is * parked under `.hq/conflict-backups/`, and the conflict index is updated. * Purely local: no vault access. Dry-run unless `yes`. */ reconcileConflicts?: boolean; } /** One canonical key the doctor will (re)write, and the spelling it copies. */ export interface SkillKeyRewrite { key: string; /** Winning (newest-bodied) spelling whose body becomes the canonical body. */ fromSpelling: string; } export interface DoctorManualReviewItem { key: string; reason: string; } export interface LocalGcManualReviewItem { path: string; reason: string; } export interface SyncDoctorPlan { /** Every listed `.claude/skills/` key (the bulk-guard denominator). */ inScopeKeys: number; /** Canonical-spelling keys that need no action. */ keep: string[]; /** Canonical keys to (re)write from their winning spelling's body. */ rewrites: SkillKeyRewrite[]; /** Non-canonical spellings safe to delete (marker body or byte-identical). */ tombstones: string[]; /** Keys the safety gate refused to auto-delete — surfaced, never removed. */ manualReview: DoctorManualReviewItem[]; /** Junk-named local farm entries (symlink-farm content only) to remove. */ localRemovals: string[]; /** Junk-named local entries the GC refused to touch — surfaced only. */ localManualReview: LocalGcManualReviewItem[]; /** True when the tombstone plan tripped the bulk-asymmetry breaker. */ bulkRefused: boolean; } export interface SyncDoctorResult { plan: SyncDoctorPlan; /** False on dry-run (the default) and when `--yes` was absent. */ applied: boolean; keysRewritten: number; keysDeleted: number; dirsRemoved: number; /** Loud non-fatal warnings emitted during plan + apply. */ warnings: number; /** Present only for `reconcileConflicts` runs. */ conflictTwins?: ReconcileTwinsResult; } /** * Run the doctor: build the dedupe + GC plan, print it in full, and — only * with `yes: true` — apply it. See the module doc for semantics. */ export declare function syncDoctor(options: SyncDoctorOptions): Promise; //# sourceMappingURL=doctor.d.ts.map