/** * saga.reconcile — idempotent, cron-safe saga auto-close repair. * * Periodic safety net for the saga auto-close pipeline. T10116 implements the * primary auto-close hook in `completeTask` (root-cause), but state can drift * out of band via bulk SQL repair, crash recovery, manual `cleo update` * sweeps, or migrations that touch `tasks.status` directly. This verb walks * the saga table and re-applies the same closure logic for any saga whose * members reached 100% terminal status while the saga row itself stayed in * a non-terminal state. * * **Idempotent**: re-running on an already-correct saga is a no-op and * surfaces `action: 'no-op'` in the structured result. * * **Cron-safe**: each saga is serialized through a per-saga advisory lock * file under `/locks/saga-reconcile-.lock`. Concurrent * invocations against the same saga either block on the lock or no-op with * `action: 'blocked'` (depending on the contention). * * **Observable**: every reconcile decision (close, no-op, blocked, error) * appends a JSON-line entry to `.cleo/audit/saga-reconcile.jsonl` so the * repair history is auditable post-hoc — mirroring the `saga-detach.jsonl` * pattern from {@link detachSagaMember}. * * Supersedes T10098 — the original "standalone reconcile verb" scope is * absorbed here. See the {@link reconcileSaga} comment block for the * supersession note. * * @task T10121 * @task T10098 — superseded standalone scope (closed externally) * @saga T10113 — SG-SAGA-FIRST-CLASS * @epic T10210 — E-SAGA-AUTO-CLOSE * @see ADR-073-above-epic-naming.md §1.3 * @see packages/core/src/sagas/detach.ts (audit-log idiom) */ import { type EngineResult } from '../engine-result.js'; /** Relative path within project root for the saga-reconcile audit log. */ export declare const SAGA_RECONCILE_AUDIT_FILE = ".cleo/audit/saga-reconcile.jsonl"; /** Default human-readable reason recorded when the verb closes a drifted saga. */ export declare const SAGA_RECONCILE_CLOSE_REASON = "all members terminal"; /** Per-decision action taken by the reconciler for a given saga. */ export type SagaReconcileAction = 'close' | 'no-op' | 'blocked' | 'error'; /** Input parameters for {@link reconcileSaga}. */ export interface ReconcileSagaParams { /** * Single saga to reconcile. When omitted, the verb walks every saga * returned by `taskList({ type: 'epic', label: 'saga' })`. */ sagaId?: string; /** * When `true`, run in report-only mode — log what would happen without * mutating any rows or writing to the audit log. The structured result * still surfaces the same `action` values so an operator can preview the * exact closure set. */ dryRun?: boolean; } /** Per-saga reconciliation outcome. */ export interface SagaReconcileEntry { sagaId: string; action: SagaReconcileAction; /** Member task IDs considered by the closure check. */ members: string[]; /** Members that satisfied the terminal-status predicate. */ terminalMembers: string[]; /** Members that did NOT satisfy the terminal-status predicate. */ pendingMembers: string[]; /** Saga status BEFORE this run. */ statusBefore: string; /** Saga status AFTER this run (== `statusBefore` for no-op/blocked/error). */ statusAfter: string; /** Free-form human-readable reason recorded for the audit entry. */ reason: string; /** ISO 8601 timestamp the decision was recorded. */ timestamp: string; } /** Aggregate result for {@link reconcileSaga}. */ export interface ReconcileResult { /** Total number of sagas inspected (== `entries.length`). */ total: number; /** Number of sagas the run flipped to `status='done'`. */ closed: number; /** Number of sagas already in the correct terminal state. */ noOp: number; /** Number of sagas blocked behind a concurrent lock holder. */ blocked: number; /** Number of sagas with pending non-terminal members (not closed). */ pending: number; /** Number of sagas that errored out during reconciliation. */ errors: number; /** Whether this run ran in dry-run mode. */ dryRun: boolean; /** Detailed per-saga entries in stable id order. */ entries: SagaReconcileEntry[]; } /** * Walk every saga (or single sagaId if specified) and re-apply the T10116 * saga auto-close logic. Idempotent — re-running on an already-correct * saga is a no-op. * * For each saga the function: * 1. Acquires a per-saga advisory lock (non-blocking; `action: 'blocked'` * when contended). * 2. Resolves members via `task_relations.type='groups'`. * 3. If all members are terminal AND the saga itself is not `done`, * flips `status='done'` (+ `completedAt`/`updatedAt`). * 4. Releases the lock. * 5. Appends a JSON-line entry to `.cleo/audit/saga-reconcile.jsonl`. * * @param projectRoot - Absolute path to the project root. * @param params - Optional single-saga scope + dry-run flag. * @returns Aggregate result with per-saga entries and counters. * * @task T10121 * @task T10098 — superseded standalone scope * @saga T10113 * @epic T10210 */ export declare function reconcileSaga(projectRoot: string, params?: ReconcileSagaParams): Promise>; //# sourceMappingURL=reconcile.d.ts.map