/** * `wicked-crew governance replay [--governance-db | --db ] [--dry-run]` * (crew#495, F-022): drain a dead-letter outbox back into the governance store. * * The engine spools every `wicked.*` event it could not store as one NDJSON line (`emit.rs` * `spool_record`: the envelope plus `deadletter_reason`, and — on engines carrying crew#495's * companion — `ts`, `pid`, `origin`). This command hands that file to the engine, which writes * each record as the EVENT node it should have been, restoring the original `ts` where the record * carries one so an id-ordered scan of the store is still chronological. * * Drain order matters, because the daemon may be appending to the SAME file while this runs: * 1. RENAME the outbox to `.replayed-` first — atomic, so the daemon's next spool * recreates a fresh file and no entry is lost between the read and the archive; * 2. replay from the archived file; * 3. APPEND the entries that failed to land back onto the live outbox — they stay dead letters, * visible on `/diagnostics`, and the archive keeps the full record of what was attempted. * `--dry-run` folds the outbox in place (count, types, reasons, timestamp range) and moves nothing. * * Target store resolution is `serve`'s (`core/governance-store.ts`): `--governance-db` › * `WICKED_CREW_GOVERNANCE_DB` › an inherited `WICKED_ESTATE_DB` › `.governance/ * governance.db`, with `--db` naming the core db (default: the state home's `core.db`). * * Exit codes: 0 every entry landed (or dry run); 1 some entries failed (they are back on the * outbox); 2 the installed engine cannot replay (`Core.replayEmitOutbox` absent — upgrade * wicked-core-ts), or the arguments are wrong. */ import { Transform, type TransformCallback } from 'node:stream'; import { type GovernanceStoreLocation } from '../core/governance-store.js'; import { foldDeadletters, type DeadletterFold } from '../api/governance-health.js'; export declare const GOVERNANCE_USAGE: string; /** crew's own package version, for the origin stamp the engine copies onto dead-letter records. */ export declare function crewPackageVersion(): string; export declare class UsageError extends Error { constructor(message: string); } /** The non-flag arguments, in order (`--dry-run` takes no value; `--db` / `--governance-db` do). */ export declare function positionalArgs(args: string[]): string[]; /** Resolve the target store for a replay — `serve`'s rule, over the core db named (or defaulted), * with the cross-product bus resolved the way `serve` resolves it (`WICKED_BUS_DB` › * `WICKED_BUS_DATA_DIR` › `.bus/bus.db`) so the bus db is refused as a target here too. */ export declare function replayTarget(args: string[], env?: NodeJS.ProcessEnv): GovernanceStoreLocation; /** The archive name a drained outbox is renamed to: timestamp + pid + a random nonce, so two replays * of the same outbox in the same millisecond (or two processes) can never pick one name and have a * later rename replace the earlier archive — the full recovery/audit copy. */ export declare function archiveNameFor(outbox: string, now?: Date, nonce?: string): string; /** * Put the lines that did not land back onto the live outbox — and if THAT write fails (permissions, * a full disk), say exactly where they are: the archive is retained (nothing is lost on disk), but * `/diagnostics` folds only the live path, so without this the operator would see fewer dead letters * than exist. */ export declare function appendFailedLines(outbox: string, archive: string, lines: string[]): void; /** * The caveat a replay that found entries `already_present` must state when the outbox holds * UNTIMESTAMPED (pre-stamp) entries: replay ids are content-addressed, and with no stamp to tell * them apart, byte-identical unstamped lines share one id — two dead letters of the same event land * once. `null` when there is nothing to say. */ export declare function conflationNote(alreadyPresent: number, fold: Pick): string | null; /** * What a dry run must say about UNTIMESTAMPED (pre-stamp) entries (crew#829): a replay cannot order * them — they carry no `ts`, so they land in file order among themselves and nothing says when they * happened — and byte-identical unstamped lines share one replay id (see {@link conflationNote}). * `null` when every entry is stamped. */ export declare function untimestampedNote(fold: Pick): string | null; /** * Keep every NDJSON record whole while streaming one file onto another that a concurrent writer may * also be appending to. Every chunk this transform emits is a whole number of records — it buffers * up to the last `\n` it has seen and holds the partial tail until more arrives — so an `O_APPEND` * write from the other writer landing between two of our writes can only ever sit BETWEEN records, * never inside one (read-stream chunks are 64 KiB and not line-aligned; forwarding them raw would * let a fresh record be spliced into the middle of an archived one). Two repairs ride INSIDE the * data writes, never as separate writes a concurrent spool could slip in front of: a leading `\n` * always opens the first emitted chunk (the live file may gain a torn tail between any look and * our first write — a check-then-write cannot close that, an unconditional separator does; an * empty NDJSON line is skipped by every reader), and a source whose last record lacks its newline * gets one appended to that same final write. */ export declare class LineBoundaryGuard extends Transform { private readonly leading; private carry; private first; constructor(leading: boolean); /** Push one chunk of whole records, opening the very first with the leading separator. */ private pushRecords; _transform(chunk: Buffer | string, _enc: BufferEncoding, cb: TransformCallback): void; _flush(cb: TransformCallback): void; } /** * Append NDJSON lines to a live outbox a daemon may be writing — ONE `write()`, ALWAYS behind a * `\n`: the file may be absent, at a boundary, or mid-record at the instant of the write, and no * look beforehand can know which (the engine writes a record and its newline as two syscalls), so * an unconditional separator is the only race-free choice. At worst it is an empty line, which * every reader — the fold, the engine's replay — skips. */ export declare function appendLines(outbox: string, lines: string[]): void; /** * Put an archived outbox back after a replay that threw — APPEND-ONLY, never a rename over the * live path: a daemon can create a fresh outbox at any instant, and an `exists`-then-`rename` would * replace it and lose what it had just spooled. Appending (`O_APPEND`, so a concurrent spool is * never clobbered — the file is created if absent) is loss-free in every interleaving; the cost is * the two batches' relative order when the daemon did spool meanwhile (each entry carries its own * `ts` on a stamping engine). Streamed, never the whole archive in memory, through * {@link LineBoundaryGuard}: whole records per write, the live tail separated first, a torn last * record terminated in its own write. The archive is removed only after its last byte is on the * live outbox. */ export declare function restoreOutbox(outbox: string, archive: string): Promise; export interface ReplayOutcome { outbox: string; store: { path: string; source: string; }; archive: string | null; read: number; replayed: number; /** Entries an earlier replay had already landed (a re-replay is a no-op); `null` on an engine that does not report it. */ alreadyPresent: number | null; failed: number; dryRun: boolean; /** The conflation caveat (see `conflationNote`), or `null`. */ note: string | null; fold?: Awaited>; } /** * The command body, separated from process I/O so a test can drive it. Returns the outcome and the * exit code; throws {@link UsageError} for bad arguments and {@link GovernanceReplayUnsupportedError} * on an addon without the binding (the CLI maps both to exit 2). */ export declare function replayOutbox(args: string[], env?: NodeJS.ProcessEnv): Promise<{ outcome: ReplayOutcome; exitCode: 0 | 1; }>; /** * The replay itself, over a resolved outbox and target store — shared by the CLI (which resolves * both from its arguments) and the daemon's `POST /governance/deadletters/replay` (which replays * its OWN outbox into its OWN store). Same drain order, same refusals, same outcome. */ export declare function replayOutboxInto(outbox: string, store: GovernanceStoreLocation, { dryRun }: { dryRun: boolean; }): Promise<{ outcome: ReplayOutcome; exitCode: 0 | 1; }>; /** The `wicked-crew governance …` entry: prints a JSON outcome and exits with the documented code. */ export declare function runGovernance(argv: string[]): Promise; //# sourceMappingURL=governance.d.ts.map