/** * `totem mail` — canonical cross-repo outbox poll (ADR-106 § 3 / ADR-107). * * Senders write to their own `/.totem/orchestration//outbox/*.md` * with `to: ` (or `to: broadcast`) in the frontmatter. * Recipients invoke this command at session-start (typically via a * vendor-specific hook in `.claude/hooks/` or `.gemini/hooks/`) to surface * unread mail addressed to themselves. * * SELF_AGENTS resolution flows through `resolveSelfAgents` from * `@mmnto/totem` (env > config.json > seat dirs ∪ basename map — the dirs ARE * the registration, mmnto-ai/totem#2141). Workspace defaults to the parent * directory of the calling repo, overridable via the `TOTEM_WORKSPACE` env * var or `--workspace` flag. * * Resolution is lifecycle-BLIND; the declared seat lifecycle * (`.totem/orchestration//lifecycle.json`, mmnto-ai/totem#2511) filters * only the BROADCAST denominator, and only through a successfully-parsed * marker. A suspended seat keeps every byte of its directed-mail behavior — * visibility and processed-mark subtraction alike — and the poll annotates the * held obligation rather than hiding it. * * Ports the strategy-side reference implementation * (`mmnto-ai/totem-strategy:.claude/hooks/SessionStart.cjs:pollInboundOutboxes`, * merged via mmnto-ai/totem-strategy#373) into a cohort-portable command * surface per the ADR-107 § Consequences direction. */ import { type SelfAgentResolution } from '@mmnto/totem'; /** A single piece of mail surfaced by the poll. */ export interface MailEntry { /** Outbox-relative filename (e.g. `2026-05-18T1734Z-strategy-claude.md`). */ file: string; /** Repo basename where the outbox lives. */ repo: string; /** Sender agent-id (from frontmatter `from:` or outbox-dir name as fallback). */ from: string; /** Recipient (from frontmatter `to:`, preserved verbatim). */ to: string; /** ISO timestamp from frontmatter `timestamp:` (ADR-098 v0.4 canonical), falling back to legacy `date:`; null if absent. */ date: string | null; /** Subject line from frontmatter, or `(no subject)` if absent. */ subject: string; /** Absolute path to the outbox file (useful for `--json` consumers). */ filePath: string; } /** Aggregate result of a single poll. */ export interface MailPollResult { /** Resolution metadata describing how SELF_AGENTS was determined. `source` * is single-sourced from core's `SelfAgentResolution` (Greptile P2 on * mmnto-ai/totem#2160 — a manual copy would compile-error in the wrong * place if core ever narrowed a literal). */ selfAgents: { agents: string[]; source: SelfAgentResolution['source']; }; /** Mail addressed to any SELF_AGENT or to `broadcast`, sorted newest-first. */ mail: MailEntry[]; /** Total files actually opened during the scan (≤ `MAX_SCAN`). */ scanned: number; /** True iff the scan hit `MAX_SCAN` before exhausting the workspace. */ truncated: boolean; /** Workspace directory walked (absolute). */ workspace: string; /** Per-source repo failure messages — never throws, surfaces via this. * GATE-ARMED accounting channel: three consumers treat a non-empty array as * "the scan/subtraction is not trustworthy" — the `ecl-gc --compact` A2.2 * completeness gate and A2.4 post-delete verify (mmnto-ai/totem#2309), and * the #2516 `INCOMPLETE` verdict token. Only scan/subtraction-integrity * anomalies belong here; informational senses ride `notices`. */ warnings: string[]; /** Informational lifecycle senses (mmnto-ai/totem#2511) — NEVER gate-armed. * Rendered to humans as `Note:` lines; every `warnings.length` consumer * ignores this channel by design, so a weeks-long suspension can never red * mark-compaction or pin the verdict to INCOMPLETE (the falsification-round * regression this channel exists to prevent). */ notices: string[]; /** Present iff the identity gate fired (mmnto-ai/totem#2204): a MULTI-seat * non-env resolution was polled without an explicit selector, so directed * dispatches addressed to union seats were withheld from `mail[]`. COUNT * only, deliberately — the listing (subjects included) is the exposure * surface the gate exists to close, so no per-item detail may leak here. */ seatGate?: { withheldDirected: number; }; } export interface MailCommandOptions { /** Emit JSON instead of human-readable text. */ json?: boolean; /** Use the recursive variant of the ADR-106 § 3 glob (default: single-level). */ recursive?: boolean; /** Workspace override (default: `TOTEM_WORKSPACE` env, else parent-of-cwd). */ workspace?: 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; /** Scan cap override (default: `MAX_SCAN`). Test injection point — cap * MECHANICS are exercised with small fixtures instead of 5000-file trees. */ maxScan?: number; /** * Return the RAW addressed-inbound set — every dispatch addressed to a * SELF_AGENT or `broadcast`, WITHOUT subtracting `processed/` marks. The * pre-dedupe discovery `ecl-gc` compaction consumes (ADR-106 § A2.1): the * cursor-GC key is `processed ∩ raw-addressed-inbound`, and feeding back the * default `inbound − processed` list would read every handled dispatch as * absent and delete the marks it must retain (the false-unread bomb A2.1 * names). Default `false` preserves the reader's `unread = inbound − * processed` contract; the two callers stay single-homed on one scan. */ includeProcessed?: boolean; /** * Serve exactly this seat's mail (`--as `, mmnto-ai/totem#2204). * Consumed by `mailCommand`, which validates it against the RESOLVED union * (a foreign-anchored poll answers "ever addressed", never "unread" — * lesson-2923d5e0) and re-enters `pollMail` via an env-injected * `TOTEM_SELF_AGENT`, so resolution flows through the existing resolver. * `pollMail` itself never reads this field. */ asSeat?: string; /** * Serve the full multi-seat union by NAME (`--all-seats`) — the repo * dashboard view the union serve was built for. Bypasses the identity gate * (mmnto-ai/totem#2204); mutually exclusive with `asSeat`. */ allSeats?: boolean; } /** * Normalize an ECL dispatch basename to its portable, NTFS-safe form by * STRIPPING COLONS — the compact `...T0510Z...` shape the send actuator already * emits (mmnto-ai/totem#2431). A colon in a filename is an NTFS Alternate Data * Stream separator: `fs.writeFileSync` to `...T05:10Z-x.md` "succeeds" but * writes a 0-byte base file (`...T05`) with the real bytes in an invisible * `:10Z-x.md` stream that `fs.readdirSync` NEVER lists — so a colon-bearing * `processed/` mark is unreadable and its dispatch reports UNREAD forever * (agy root-cause, live-verified with `Get-Item -Stream *`). Applied on ALL * platforms (not just win32) so marks stay portable across checkouts: a mark * written on a colon-legal filesystem must still be found on Windows. This is * the single normalization seam — the writer (`markSource`) stores under the * sanitized name, and the reader normalizes BOTH inbound outbox basenames AND * mark basenames through it before comparison, so sanitized marks subtract * colon-bearing inbound names and pre-existing corrupted 0-byte marks stop * mattering once a healed mark lands. Idempotent — a colon-free basename passes * through unchanged. Colons ONLY: broadening to other characters would perturb * the reader's positional-token bucketing (which keys on the raw filename). */ export declare function sanitizeEclBasename(name: string): string; /** * Programmatic entry point. Returns a structured `MailPollResult` for * consumers that want to render their own output (hooks, MCP audits, * future surfaces). The CLI wrapper calls this then formats the result * for human consumption. * * Never throws — filesystem failures degrade to warnings on the result. */ export declare function pollMail(opts?: MailCommandOptions): MailPollResult; /** * Exit-code contract for `totem mail` (mmnto-ai/totem#2312). Pure so the class * is unit-testable independent of the CLI wrapper, mirroring * `resolveEclGcExitCode`. An UNRESOLVED self (`source: 'none'`, agents `[]`) is * a NOT-DERIVED verdict, never a clean inbox: every directed dispatch is * filtered out so "no unread" asserts nothing (the false-clean class). The * plain poll must not be softer than its `totem ecl-gc` sibling, whose * unresolvable-self is exit 2 — so this arm is exit 2 too. A resolved self * (genuine clean inbox OR a real unread list) is exit 0. * * The identity-gated arm (mmnto-ai/totem#2204) joins the SAME NOT-DERIVED * family: a multi-seat non-env poll without a selector withholds the directed * listing, so the per-seat inbox verdict the caller asked for cannot be * asserted — exit 0 there would be the reassuring-lead class (#2516). The * named `--all-seats` union view never carries `seatGate` and stays exit 0. */ export declare function resolveMailExitCode(result: MailPollResult): 0 | 2; export declare function formatTextResult(result: MailPollResult): string; export declare function mailCommand(opts?: MailCommandOptions): Promise<{ result: MailPollResult; exitCode: 0 | 2; }>; /** * Structurally-complete dispatch header. ADR-098 v0.4 compliance is enforced * *by construction*: you cannot build this object without `schema` / `from` / * `to` / `timestamp` / `subject` / `expectedAction`, so a structurally invalid * dispatch is unrepresentable rather than rejected after the fact — the * strongest form of "enforce via substrate" (inv2 realized). The content * predicates that CANNOT be guaranteed at construction time (is the recipient * a known agent? do refs resolve?) are the validator's job, and warn rather * than block (inv6). */ export interface DispatchHeader { schema: string; from: string; to: string; /** Full RFC3339 UTC, e.g. `2026-06-09T17:34:37.127Z` (ADR-098 v0.4). */ timestamp: string; subject: string; /** ADR-098 v0.4 mandatory; the `none` literal for informational dispatches. */ expectedAction: string; inReplyTo?: string; priority?: string; related?: string[]; } export interface MailSendOptions { /** Recipient agent-id (or `broadcast`). */ to: string; /** Subject line (the cohort convention carries the gist here). */ subject: string; /** Sender agent-id; default resolves from self, erroring if ambiguous. */ from?: string; /** Read the dispatch body from this file (hard error if unreadable). */ bodyFile?: string; /** Direct body text (test/stdin seam); `bodyFile` overrides when both set. */ body?: string; /** `in-reply-to:` frontmatter — the source dispatch path. */ inReplyTo?: string; /** `priority:` frontmatter. */ priority?: string; /** `related-issues:` frontmatter list. */ related?: string[]; /** `expected-action:` frontmatter; defaults to the `none` literal. */ expectedAction?: string; /** Filename slug override; default derived from the subject. */ slug?: string; /** * Workspace for dir-derived known-recipient validation (default: * `TOTEM_WORKSPACE` env, else parent of repoRoot — the same resolution as * `pollMail`). Advisory only (inv6): widens the known set so a * dir-registered seat is not warned as unknown (mmnto-ai/totem#2141). */ workspace?: string; /** Repo root (default: cwd). Test injection point. */ repoRoot?: string; /** Env override (default: process.env). Test injection point. */ env?: Record; /** Clock injection for deterministic timestamps/filenames in tests. */ now?: () => Date; /** Known-recipient set override (default: `knownCohortAgents()`). */ knownAgents?: readonly string[]; } export interface MailSendResult { /** Absolute path of the written dispatch. */ filePath: string; /** Basename of the written dispatch. */ fileName: string; /** The composed (structurally-valid-by-construction) header. */ header: DispatchHeader; /** Content-class warnings surfaced at emit-time; dispatch still written. */ warnings: string[]; /** * Present only on a `mailReply` that atomically marked the source dispatch * consumed (ADR-106 § A1.4; mmnto-ai/totem#2396). Absent on `mailSend` and on * a `--no-mark` (stage-only) reply. */ mark?: MailMarkResult; } /** * Serialize a dispatch header + body to ADR-098 v0.4 markdown. Pure + * deterministic — the round-trip anchor: its output MUST parse back through * `parseHeader` (the sensor↔actuator "one enumeration, two readers" pairing). * Frontmatter keys are kebab-case wire form, the surface the reader greps. */ export declare function composeDispatch(header: DispatchHeader, body: string): string; /** * Content-class validation (inv1: exact predicates only — set membership and * non-emptiness, never judgment). NEVER throws, NEVER blocks: returns warnings * the caller surfaces at emit-time and writes anyway (inv6). The headline check * is the unknown-recipient typo class (strategy-claude 2026-06-09): a typo'd * recipient writes under a wrong name and is undelivered-but-not-errored unless * the sender is told loudly. */ export declare function validateDispatchContent(header: { to: string; related?: string[]; }, knownAgents: readonly string[]): string[]; /** * Resolve the single sender identity for an outbound dispatch. Unlike the * reader (which resolves a SET of self-agents to filter by), send must pick * ONE. Precedence: explicit `--from` > unambiguous `resolveSelfAgents` > error. * A >1 ambiguous map (e.g. totem hosts both totem-claude + totem-gemini) is a * hard usage error — never silently pick one (it would mis-attribute the * dispatch). Zero is a hard error too — never write to `.../undefined/outbox`. */ export declare function resolveSelfSender(repoRoot: string, env: Record, explicitFrom?: string): string; /** * Compose + validate + write an outbound dispatch to the sender's own outbox. * Structural validity is by construction; content warnings are returned (the * CLI wrapper surfaces them loudly) and never block the write. The only HARD * failures are usage errors (missing to/subject, unresolvable/ambiguous self, * unreadable body-file) and actuation failure (a write that didn't land — * fail-loud, Tenet 4, the opposite of the inv6 content case). */ export declare function mailSend(opts: MailSendOptions): MailSendResult; /** * `totem mail reply ` — syntactic sugar over `mailSend`. Reads the * source dispatch (HARD error if missing/unparseable — reply structurally needs * it to infer the recipient + subject), then sends with `to = source.from` * (falling back to the source's outbox-dir agent, reader parity), * `subject = "Re: "`, and `in-reply-to` set to the source's * repo-relative wire form. Any field can still be overridden via opts. */ export declare function mailReply(source: string, opts?: Omit & { to?: string; subject?: string; /** * Skip the atomic consume-mark (ADR-106 § A1.4; mmnto-ai/totem#2396). * Default (undefined/false) copies the source into the replying seat's own * `processed/` cursor the moment the reply lands — reply IS consumption, so * the mark is a side-effect of the read tool (A1.3 "never a separate * ritual"), never the dropped-copy step that re-surfaces as phantom-unread. * `--no-mark` opts out for stage-only reply workflows. */ noMark?: boolean; }): MailSendResult; export interface MailMarkOptions { /** * Seat whose `processed/` cursor receives the mark. Precedence mirrors the * `ecl-gc` single-seat self-resolver (`resolveSelfSender`): explicit * `--agent-id` > unambiguous resolved self > error. Ambiguous/zero self is a * hard usage error — a seat may only mark into its OWN subtree (§ A2.3 * single-writer), so the command never guesses which seat consumed a dispatch. */ agentId?: string; /** Walk-start for the repo-root resolver (default: cwd — same contract as * `pollMail`/`eclCompact`). Test injection point. */ repoRoot?: string; /** Env override (default: process.env). Test injection point. */ env?: Record; } export interface MailMarkResult { /** Absolute path of the mark (written, or the pre-existing one on a no-op). */ markPath: string; /** Basename of the mark — matches the source dispatch (the reader's dedupe key). */ fileName: string; /** The seat whose `processed/` cursor now holds the mark. */ agent: string; /** True iff the mark landed under `processed/_broadcast/` (source is a broadcast). */ broadcast: boolean; /** True iff a same-basename mark already existed — the operation was an idempotent no-op. */ alreadyMarked: boolean; } /** * `totem mail mark ` — copy a consumed dispatch into the consuming * seat's own `processed/` cursor WITHOUT replying (read-and-acted-elsewhere). * The binary-guaranteed path for the marking obligation ADR-106 § A1.3 puts on * the recipient. Shared by the standalone command and `mailReply`'s atomic mark. * * HARD-errors (fail-loud, Tenet 4 — a dropped mark is the phantom-unread class * this closes) on: an unreadable/unparseable source, an unresolvable/ambiguous * seat, an unsafe agent-id, or a write that did not land. Idempotent: a * same-basename mark already present is a no-op (safe to run twice). */ export declare function markSource(source: string, opts?: MailMarkOptions): MailMarkResult; /** * CLI wrapper for `mail send` / `mail reply`. Surfaces content warnings LOUDLY * on stderr at emit-time (inv6: the dispatch still wrote — this is the typo * backstop, not a block), then confirms the written path. */ export declare function mailSendCommand(result: MailSendResult): Promise; /** * CLI wrapper for `mail mark`. Confirms the mark (or reports the idempotent * no-op) on stderr — the informational, non-primary-data path the mail surface * uses throughout. */ export declare function mailMarkCommand(result: MailMarkResult): Promise; //# sourceMappingURL=mail.d.ts.map