/** * Four-step install saga with reverse-order compensation. * * GJC writes to four files it does not own, across two applications, with no * distributed lock available. Nothing here can be made atomic across files, so * the design is instead: refuse early, record enough to recover, and undo in * reverse on failure. * * Forward order is fixed: * 1. `~/.paseo/config.json` provider entry + provider-key provenance * 2. `~/.paseo/orchestration-preferences.json` seeded roles + seeded-key provenance * 3. `/paseo-skills/` symlink bridge * 4. `~/.gjc/agent/config.yml` `skills.customDirectories` append * * Steps 1 and 2 each mutate a Paseo file AND the GJC provenance ledger. Those * are separate files, so a durable intent record is written BEFORE either one, * carrying preflight and expected-post identities for BOTH. Recovery classifies * each file independently and refuses whenever the ledger diverged. */ import type { CasReceipt } from "../../config/atomic-yaml-patch"; import { type IntentStep, type ProvenanceLedger } from "./paseo-ownership"; import type { PartialInstallEvidence } from "./result-types"; /** One completed forward step, retaining exactly what its inverse needs. */ export interface CompletedStep { readonly label: string; /** Undo this step. Resolves to `"reverted"`, or `"conflict"` when the resource moved underneath us. */ undo(): Promise; } export type StepUndoResult = { readonly status: "reverted"; } | { readonly status: "conflict"; readonly detail: string; readonly retained: readonly string[]; }; export declare class SagaStepError extends Error { readonly label: string; readonly retained: readonly string[]; constructor(label: string, message: string, retained?: readonly string[]); } export interface CompensationOutcome { readonly compensated: readonly string[]; readonly uncompensated: readonly string[]; readonly evidence: PartialInstallEvidence; } /** * Undo completed steps newest-first, halting at the first inverse that reports * a conflict. * * Halting is deliberate: once one resource has diverged, continuing to unwind * the others would leave a stranger mix of reverted and live state that no * later run could interpret. Stopping preserves an interpretable prefix. */ export declare function compensate(completed: readonly CompletedStep[], failure: SagaStepError): Promise; export interface JsonStepInput { readonly label: string; readonly step: IntentStep; readonly targetPath: string; readonly provenancePath: string; readonly intentPath: string; readonly ownedKeys: readonly string[]; /** Mutates the parsed target in place. */ readonly mutate: (draft: Record) => void; /** Produces the ledger that must exist once this step commits. */ readonly nextLedger: (ledger: ProvenanceLedger) => ProvenanceLedger; /** Reverts the target, removing only what this step added. */ readonly revert: (draft: Record) => void; /** Produces the ledger that must exist once this step is undone. */ readonly revertLedger: (ledger: ProvenanceLedger) => ProvenanceLedger; readonly now: Date; } export interface JsonStepOutput { readonly completed: CompletedStep; readonly changed: boolean; readonly backupPath?: string; } /** * Run one JSON step: intent, target publish, ledger commit, intent clear. * * The intent is written first and cleared last. Between those points a crash is * recoverable because the record alone identifies whether the target carries * pre-write bytes, the exact bytes we intended, or something a third party * produced. */ export declare function runJsonStep(input: JsonStepInput): Promise; /** Wrap a `CasReceipt` as a compensable step. */ export declare function receiptStep(label: string, receipt: CasReceipt): CompletedStep; export interface RecoverIntentOptions { /** * Act on the classification rather than only reporting it. * * Install passes `true`. `--check` passes `false` because it must stay * read-only: it surfaces the lingering intent as drift and leaves the repair * to the next install. */ readonly repair: boolean; } /** * Classify a lingering intent left by an interrupted run, and optionally act. * * The outcome comes from classifying BOTH the target and the ledger: * * - `discard` the publish never landed, so nothing was mutated. Safe to clear. * - `complete-ledger` the publish landed but the ledger commit did not. Re-running * the same install re-derives and commits the identical provenance in step 1, * so recovery is to PROCEED. The intent is deliberately left in place until * that step overwrites it, because clearing it first would lose recoverability * if the retry is interrupted too. * - `refuse` a third party changed one of the files. Never replay a recorded * write over someone else's change. */ export declare function recoverIntent(intentPath: string, options?: RecoverIntentOptions): Promise<{ recovered: boolean; detail: string; } | undefined>;