import { type PortableSkillOptions } from "./portable-skills.js"; import { RemoteSkillsClient } from "./remote-client.js"; export type ReconcileConflictPolicy = "local" | "remote" | "skip"; /** * The default policy, named exactly as the plan specifies: when the digests are identical * the local state wins by construction (nothing to resolve); every other divergence is * skipped and reported unless --conflict says otherwise. */ export declare const DEFAULT_CONFLICT_POLICY: "local-wins-on-identical-digest-else-skip-and-report"; export declare const CONFLICT_POLICIES: readonly ReconcileConflictPolicy[]; /** Corpus-root cursor recording the last sync run. */ export declare const SYNC_CURSOR_FILE = ".sync-cursor.json"; export declare const SYNC_CURSOR_SCHEMA_VERSION: 1; export interface ReconcileRegistryOptions extends PortableSkillOptions { /** Push local-only, changed-locally, and conflicts won by local. */ push?: boolean; /** Pull remote-only, changed-remotely, and conflicts won by remote. */ pull?: boolean; /** Both directions (the default when neither --push nor --pull is given). */ all?: boolean; /** Plan and report without writing anything. */ dryRun?: boolean; /** Conflict resolution policy. */ conflict?: ReconcileConflictPolicy; /** Client override. `undefined` resolves one from configuration; `null` models "no credential". */ client?: RemoteSkillsClient | null; /** HMAC signing key for bundle signature verification. Defaults to $SKILLS_SIGNING_KEY. */ signingKey?: string; } export type ReconcileSkillState = "local-only" | "remote-only" | "changed-locally" | "changed-remotely" | "conflict" | "in-sync"; export type ReconcileAction = "push" | "pull" | "skip" | "none"; export interface ReconcileSkillEntry { slug: string; state: ReconcileSkillState; action: ReconcileAction; localVersion?: string; remoteVersion?: string; localSha256?: string; remoteSha256?: string; /** Why the entry was classified or skipped this way. */ reason?: string; /** Present only for executed (non-dry-run) actions. */ result?: { ok: boolean; detail?: string; }; } export interface ReconcileSummary { /** Skills found in the local corpus. */ local: number; /** Skills the registry serves (published plus bundled). */ remote: number; inSync: number; pushed: number; pulled: number; /** Divergences the conflict policy had to resolve (or decline to resolve). */ conflicts: number; /** Divergences skipped under the policy. */ skipped: number; errors: number; } export interface ReconcileCursor { schemaVersion: typeof SYNC_CURSOR_SCHEMA_VERSION; managedBy: string; lastSyncedAt: string; runCount: number; summary: ReconcileSummary; } export interface ReconcileRegistryResult { corpusRoot: string; /** True when a real run would first migrate the legacy corpus layout into place. */ migrationPending: boolean; direction: "push" | "pull" | "all"; dryRun: boolean; conflictPolicy: ReconcileConflictPolicy; /** The full declared policy label; identical to conflictPolicy for local/remote. */ conflictPolicyDescription: string; summary: ReconcileSummary; skills: ReconcileSkillEntry[]; /** Present only when the run was real and completed without errors. */ cursor?: Pick; } export declare class ReconcileRegistryError extends Error { readonly detail?: string[] | undefined; constructor(message: string, detail?: string[] | undefined); } /** * Re-check the local side of one pull candidate, immediately before the pull batch. * * Packs FIRST, then samples existence: a directory that appeared between the plan and * the pack is provably on disk when the sample runs. Sampling before the pack let a * concurrent editor's partial tree read as "still absent" — the pack then threw on it * while the stale sample stayed false, so localMoved was false and the pull replaced * the newly created directory (review P1). The ops seam exists so a test can pin that * ordering deterministically: pack throws on the partial tree, the post-pack sample * sees it, and the candidate counts as moved. * * Returns true when the local side moved since the plan: a digest different from the * planned one, or a directory present where the plan saw none (a concurrent editor). */ export declare function recheckLocalSide(plannedLocal: string | undefined, localDir: string, ops?: { pack: (dir: string) => string; exists: (dir: string) => boolean; }): boolean; /** * Run one sync pass. * * The plan (classification + per-skill action) is computed in full before anything is * executed, and a dry run stops at the plan: no publish, no pull, no marker, no cursor — * proven by readback in the tests. Every executed mutation re-checks both sides first * (see the module header); a side that moved since the plan skips that skill and reports * it rather than overwriting state the plan did not see. */ export declare function reconcileRegistry(options?: ReconcileRegistryOptions): Promise;