/** * `totem ecl-gc` — ECL outbox retention prune (mmnto-ai/totem#2279; parent * mmnto-ai/totem-strategy#700 / doctrine/ecl-discipline.md § 4.4). * * The binary-guaranteed cohort-wide replacement for the interim * `scripts/prune-outbox.mjs`: it deletes an agent's OWN outbox dispatches once * they age past the retention window N (default 14 days). Outbox dispatches are * TRANSPORT, not archive — the durable record of whatever a dispatch carried * lives in its home (rulings → ADRs / issues, work-state → the GH board, * session history → `journal/`), so an aged courier file is disposable. * * SINGLE-WRITER INVARIANT (ADR-106): each agent prunes only its OWN * `/.totem/orchestration//outbox/` — never a peer's, never * the operator's chore. Self-resolution (Tenet-21 reuse of `resolveSelfSender`) * makes pruning a peer structurally unreachable: the target path is composed * from the resolved single agent-id and nothing else. This command NEVER reads * or touches `journal/` (bounded-past record + MCP-indexed) or `processed/` * (the handled-state cursor — erasing it makes consumed backlog re-read as * unread) or any inbox / other seat. Scope is `outbox/` only. * * Safe by default: dry-run (list only) unless `--apply` is passed. * * COMPACTION (mmnto-ai/totem#2307; contract ADR-106 § A2 + ecl-discipline § 4.5, * ratified strategy#826). `eclCompact` is the cursor-coupled processed-mark GC * sibling of the prune above: it deletes an agent's OWN `processed/` marks that * shadow nothing — a mark whose inbound dispatch its sender already swept per * § 4.4. The retained cursor is `processed ∩ raw-addressed-inbound` (A2.1: the * PRE-dedupe scan, never `pollMail`'s `inbound − processed` list). Deletion is * licensed ONLY against a provably-complete poll (A2.2: full expected roster * present, zero warnings, not truncated — else zero deletes), binds to exactly * one seat (A2.3), and self-verifies via an immediate re-poll (A2.4). Unlike the * prune's age window, compaction couples to the outbox lifecycle, not to time. */ import { type TotemConfig } from '@mmnto/totem'; export interface EclGcOptions { /** Actually delete (default: dry-run — list would-prune, delete nothing). */ apply?: boolean; /** Retention window in days (default 14). Must be a non-negative integer. */ retainDays?: number; /** * Override the self-resolved agent-id (visiting / orchestrator case only). * NOT used by the signoff step — that path self-resolves. */ agentId?: string; /** * Walk-START directory (default: `process.cwd()`), not the definitive root: * the effective repo root is derived by walking up to the nearest * `.totem`/`.git` marker (mmnto-ai/totem#2312); a marker-less start is used * as-is. Test injection point. */ repoRoot?: string; /** Env override (default: `process.env`). Test injection point. */ env?: Record; /** Clock injection for deterministic cutoffs in tests (default: `new Date()`). */ now?: () => Date; } /** * Structured prune result. Also the `--json` payload. `pruned` lists the files * actually removed under `--apply` (or the would-prune set in dry-run); * `failed` captures per-file delete failures (never fatal); `skipped` surfaces * every entry left untouched together with WHY (non-file, non-`.md`, * unparseable stamp). */ export interface EclGcResult { agent: string; retainDays: number; dryRun: boolean; outbox: string; cutoffKey: string; pruned: string[]; failed: { file: string; error: string; }[]; kept: number; skipped: { file: string; reason: string; }[]; warnings: string[]; } /** * Canonicalize either stamp form to a 14-digit `YYYYMMDDHHMMSS` key (seconds * default to `00`) so mixed-length stamps compare correctly. Ported from * `scripts/prune-outbox.mjs:toKey`. */ export declare function toStampKey(stamp: string): string; /** * Cutoff = `now − retainDays`, as a comparable 14-digit `YYYYMMDDHHMMSS` key. * Derives from the injected `now` for determinism (Tenet 15). Ported from * `scripts/prune-outbox.mjs:cutoffKey`. */ export declare function cutoffKey(now: Date, retainDays: number): string; /** A directory entry reduced to the classification inputs (pure-helper seam). */ export interface DirEntryLike { name: string; isFile: boolean; } /** The per-entry verdict a classifier returns. */ export type PruneClass = { action: 'prune'; } | { action: 'keep'; } | { action: 'skip'; reason: string; }; /** * Classify a single directory entry against the cutoff. Safe-direction bias: * anything whose age is not derivable from an eligible filename is KEPT + * surfaced, never deleted (auto-deleting an un-ageable file is worse than * letting it linger — mmnto-ai/totem-strategy#700 by-design). Non-file entries * are checked FIRST so a directory named `*.md` can never reach a delete. */ export declare function classifyEntry(entry: DirEntryLike, cutoff: string): PruneClass; /** The classification plan for a whole directory listing. */ export interface PrunePlan { prune: string[]; kept: number; skipped: { file: string; reason: string; }[]; } /** * Pure prune-plan classification: given a directory listing + a cutoff key, * partition entries into prune / keep / skip. Deterministic order (filename * sort) so the reported lists are stable across platforms. */ export declare function planPrune(entries: DirEntryLike[], cutoff: string): PrunePlan; /** * Programmatic entry point. Resolves the single self-agent, validates inputs, * scans that agent's OWN outbox, and (under `--apply`) deletes the aged * dispatches. Returns a structured `EclGcResult`. * * Throws ONLY on usage errors (unresolvable/ambiguous self, unsafe agent-id, * invalid `--retain-days`), and always BEFORE any directory scan or deletion. * Filesystem failures are NEVER thrown — a per-file delete failure is captured * into `result.failed` (janitorial sensor, not a gate — Tenet 13). */ export declare function eclGc(opts?: EclGcOptions): EclGcResult; /** * Render an `EclGcResult`. With `json`, the structured result goes to stdout * (hook-friendly clean stream). Otherwise a human summary goes to stderr via * the standard CLI logger (agent, mode, pruned/kept counts, skipped-with- * reasons, failed count) — mirrors `mail.ts`'s stderr `log` usage. */ export declare function eclGcCommand(result: EclGcResult, json: boolean): Promise; export interface EclCompactOptions { /** Actually delete inert marks (default: dry-run — list would-collect only). */ apply?: boolean; /** * Override the self-resolved agent-id (visiting/orchestrator case). The * single-writer compaction target; NOT used by the signoff step (self-resolves). */ agentId?: string; /** * Walk-START directory (default: `process.cwd()`), not the definitive root: * the effective repo root is derived by walking up to the nearest * `.totem`/`.git` marker (mmnto-ai/totem#2312); a marker-less start is used * as-is. Test injection point. */ repoRoot?: string; /** Env override (default: `process.env`). Test injection point. */ env?: Record; /** Workspace override (default: `TOTEM_WORKSPACE` env, else parent of repoRoot). */ workspace?: string; /** Scan cap override (default: mail's `MAX_SCAN`). Injection point for the * A2.2 truncation abort arm — exercised with small fixtures. */ maxScan?: number; /** * Declared expected cohort repo roster for the A2.2 completeness gate — the * yardstick the workspace glob is checked against. Highest-precedence source * (mmnto-ai/totem#2310): an explicit value here (programmatic callers / tests) * WINS over `config.ecl.cohortRepos`. When BOTH are absent the roster is * undeclared: compaction HARD-ABORTS (fail-loud, exit 3), never "assume * complete" and never a silent no-op — completeness is unprovable without a * declared expectation (strategy#828 no-roster corollary). Tests inject * explicitly; the CLI action injects `config` (below) instead. */ expectedRepos?: string[]; /** * Loaded consumer config, the roster's second-precedence source * (mmnto-ai/totem#2310): when `expectedRepos` is absent the gate reads * `config.ecl.cohortRepos`. Injected by the CLI action (which loads config at * the process boundary) so `eclCompact` stays a pure, synchronously-testable * function — most tests inject `expectedRepos` directly and never touch this. * A declared-but-EMPTY `cohortRepos` never reaches here: it is a Zod `.min(1)` * violation caught loud at config load (a config bug ≠ an undeclared roster). */ config?: TotemConfig; /** * Operator escape (`--force-incomplete`): proceed with compaction even when * the workspace glob is a strict subset of the declared roster (a cohort repo * is absent). UNSAFE — a live mark in an unscanned repo can be collected as a * false-unread; use only when you know the absent repo holds no inbound for * this seat. Bypasses ONLY the roster-presence check; scan warnings and * truncation remain hard aborts, and an undeclared (empty) roster still * hard-aborts (there is no presence to force when nothing was declared). */ forceIncomplete?: boolean; } /** * Structured compaction result. Also the `--json` payload. `collectable` is the * would-collect set in dry-run (or the eligible set pre-delete under apply); * `collected` is what actually got deleted (gate-green only); `resurfaced` is * the A2.4 falsifier output — MUST be empty (a non-empty set means a live mark * was collected and the completeness gate was too weak). */ export interface EclCompactResult { agent: string; dryRun: boolean; workspace: string; expectedRepos: string[]; /** * Whether a non-empty roster was declared. `false` = the undeclared case, * folded INTO the A2.2 gate: `gateComplete` is also false and compaction * HARD-ABORTS (exit 3), because completeness cannot be proven without a * declared expectation (strategy#828). Retained only to frame the specific * operator message ("no cohort roster declared" vs a missing/unscannable repo). */ rosterDeclared: boolean; /** A2.2 gate: true iff full roster present (or forced) AND zero scan warnings AND not truncated. */ gateComplete: boolean; /** Why the gate is red (missing repos, scan/read/parse warnings, truncation); empty iff complete. */ gateReasons: string[]; /** Count of raw addressed-inbound basenames discovered (pre-dedupe; to:S ∪ broadcast). */ rawInbound: number; /** Total own processed marks examined (direct ∪ broadcast). */ marks: number; /** Marks whose dispatch is absent from raw inbound — the would-collect set (dry-run / pre-delete). */ collectable: string[]; /** Marks actually deleted (⊆ collectable; gate-green apply only). */ collected: string[]; /** Marks left in place after the run. */ retained: number; failed: { file: string; error: string; }[]; /** A2.4 falsifier: previously-handled dispatches that re-surfaced as unread post-compact (MUST be []). */ resurfaced: string[]; /** * A2.4 self-check trustworthiness: `false` iff the post-compact re-poll was * itself truncated or warned, so a resurfaced dispatch beyond its horizon * could read as clean. An untrustworthy verify is a hard failure (we deleted * marks and cannot confirm no resurface) — maps to the abort exit code. */ verifyComplete: boolean; warnings: string[]; } /** * Resolve the A2.2 completeness roster by precedence (mmnto-ai/totem#2310): * explicit `expectedRepos` (programmatic callers / tests) → `config.ecl.cohortRepos` * (consumer-declared) → `undefined` (the honest UNDECLARED state, which the gate * maps to a hard-abort). Pure so the precedence is unit-testable in isolation. * A declared-but-EMPTY array never reaches here: it is a Zod `.min(1)` violation * caught loud at config load (`loadEclConfig`), so a config bug is never aliased * into an undeclared roster. */ export declare function resolveExpectedRoster(explicit: string[] | undefined, config: TotemConfig | undefined): string[] | undefined; /** * Load the consumer's Totem config for the compaction roster read * (mmnto-ai/totem#2310). A MISSING config file is the honest "undeclared" state — * returns `undefined`, and the gate hard-aborts (exit 3) downstream. A * PRESENT-but-INVALID config (e.g. `ecl.cohortRepos: []` → Zod `.min(1)`, or any * other schema/parse error) throws LOUD: it is a config BUG and must NOT be * caught into honest-absent, which would mask the bug as a normal undeclared * roster (Tenet 4). Mirrors `orient`'s config read (`resolveProjectNumber`) but * DELIBERATELY narrows the swallow to `CONFIG_MISSING` only — orient's catch-all * would degrade an invalid config into "no board", the exact aliasing this gate * must avoid. */ export declare function loadEclConfig(cwd: string): Promise; /** * Programmatic entry point for cursor-coupled processed-mark compaction. For a * single resolved seat, deletes `processed/` marks whose inbound dispatch is * absent from the RAW addressed-inbound set (A2.1) — but ONLY when discovery is * provably complete (A2.2), and then self-verifies (A2.4). * * Throws ONLY on usage errors (unresolvable/ambiguous self, unsafe agent-id), * always BEFORE any scan or deletion (exit-2 class, parity with `eclGc`). Every * safety failure (incomplete roster, scan warning, truncation) is a STRUCTURED * gate-red result with zero deletes, never a throw — the caller maps it to the * compaction-abort exit code. */ export declare function eclCompact(opts?: EclCompactOptions): EclCompactResult; /** * Render an `EclCompactResult`. `--json` → structured stdout; otherwise a human * summary to stderr. A red gate reports the reasons + zero deletes; a tripped * A2.4 falsifier and per-mark delete failures are surfaced via the error logger. */ export declare function eclCompactCommand(result: EclCompactResult, json: boolean): Promise; /** * Combined prune+compact exit-code contract (codex panel, mmnto-ai/totem#2307). * Pure so the precedence is unit-testable independent of the CLI wrapper. The * usage code `2` is NOT modeled here — it is the thrown-error path the wrapper * catches around this call. Precedence: `3` (compaction abort — A2.2 gate red or * A2.4 falsifier tripped) outranks `1` (partial janitorial delete failure, from * either phase), which outranks `0` (clean). So a prune-partial + compact-abort * is `3`, with the prune count still carried in the structured result. */ export declare function resolveEclGcExitCode(prune: Pick, compact?: Pick): 0 | 1 | 3; //# sourceMappingURL=ecl-gc.d.ts.map