import { Database } from 'better-sqlite3'; type CtlState = "busy" | "idle"; /** * pi is waiting on a blocking extension dialog (ui_prompt_start without * its ui_prompt_end): a confirm, select, input, editor or custom prompt. * `kind` and `title` are the innermost open dialog's; `since` is when the * first one opened (ms epoch). */ type CtlBlocked = { kind: string; title?: string; since: number; }; /** Session commands the `command` op runs: `/new`, `/reload`, `/compact`. */ declare const CTL_COMMANDS: readonly ["new", "reload", "compact"]; type CtlCommandName = (typeof CTL_COMMANDS)[number]; /** The tool pi is executing (innermost, latest started); `since` is ms epoch. */ type CtlTool = { name: string; since: number; }; type RuntimeState = "busy" | "needs_input" | "needs_permission" | "unknown"; type StateSource = "murmur" | "herdr" | "ctl" | "none"; /** How a pi agent's control socket answered; "n/a" for non-pi agents. */ type CtlLink = "ok" | "missing" | "refused" | "n/a"; interface StateReading { state: RuntimeState; source: StateSource; since: number | null; alive: boolean; reason?: string; /** Set for agents that expect a control socket. */ ctl?: Exclude; /** ctl only: pi holds queued messages, so an idle reading is about to * turn busy. */ pending?: boolean; /** ctl only: pi's settled-run count (`mu agent wait --after-runs`). */ runs?: number; /** ctl only: the pi process's incarnation string; absent from older extensions. */ incarnation?: string; /** ctl only: the extension dialog pi is waiting on (state needs_permission). */ blocked?: CtlBlocked; /** ctl only: the running tool, null when none; absent from older extensions (unknown). */ tool?: CtlTool | null; } interface StateAgentRef { name: string; workstreamName: string; paneId: string; /** When set and it names pi, the control socket is the state source. */ cli?: string; /** A remote command gate (see src/agents/gate.ts): no control socket, whatever `cli` says. */ gateToken?: string | null; } /** ctl's idle maps to needs_input, the same as murmur's idle; an open * extension dialog (`blocked`) maps to needs_permission. */ declare function ctlRuntimeState(state: CtlState, blocked?: CtlBlocked): RuntimeState; declare const UNKNOWN_REASON: { readonly murmurMissing: "murmur not installed"; readonly extensionMissing: "murmur pi extension not linked"; readonly noRow: "murmur has no row"; readonly stale: "remote snapshot stale"; readonly herdrNone: "herdr reports no state"; readonly paneGone: "pane gone"; readonly ctlMissing: "ctl missing"; readonly ctlRefused: "ctl refused"; }; declare function agentKey(a: { name: string; workstreamName: string; }): string; declare function murmurAvailable(): boolean; /** * Read the runtime state of each agent. Order per agent: control socket * (pi agents) → murmur pane option → murmur remote → herdr. A pi agent * whose socket does not answer reads `unknown` with reason `ctl missing` * or `ctl refused`, never a murmur reading, so the gap stays visible. * `stateDir` roots the derived socket path (spawn uses the DB's directory). * A remote command gate has neither: it reads from its pane (see `readGatePanes`). */ declare function readAgentStates(agents: readonly StateAgentRef[], opts?: { now?: number; stateDir?: string; }): Promise>; /** * Resolve the canonical mu state directory: * MU_STATE_DIR > $XDG_STATE_HOME/mu-next (absolute only) > ~/.local/state/mu-next * * `mu-next`, not `mu`: 3.x owns `/mu`, and a shared default DB, * socket dir or lock dir would let the two builds corrupt each other. */ declare function defaultStateDir(): string; type Db = Database; interface OpenDbOptions { /** * Absolute path to the SQLite file. Defaults to MU_DB_PATH env var or * the XDG state path (see `defaultDbPath`). Use a per-test temp path * in tests. */ path?: string; /** * If true, opens the DB read-only. Used by `mu sql` and similar read-only * surfaces to enforce no-mutation guarantees at the connection level. */ readonly?: boolean; } /** * Resolve the canonical DB path: * MU_DB_PATH > /mu.db */ declare function defaultDbPath(): string; /** * Open the mu database. Creates the parent directory and applies the schema * idempotently when it is missing or differs from this build (an * up-to-date DB opens without a write). Safe to call from many short-lived processes * concurrently — WAL mode handles cross-process writes. */ declare function openDb(options?: OpenDbOptions): Db; declare class WorkstreamNotFoundError extends Error { readonly workstream: string; readonly name: string; /** `near` are live workstreams with a similar name; `init` says `mu workstream init` is the fix * (the name is valid and not reserved). Both only shape the message. */ constructor(workstream: string, hints?: { near?: readonly string[]; init?: boolean; }); } /** A verb named a **torn down** workstream (`workstreams.torn_down_at` * set). It is a kind of not-found (exit 3), but says why and how to get * the name back: `workstream init` previews, then `--if ` reuses it. */ declare class WorkstreamTornDownError extends WorkstreamNotFoundError { readonly tornDownAt: string; readonly name: string; constructor(workstream: string, tornDownAt: string); } /** * Thrown by openDb when the on-disk DB is older than the current * schema. There is no in-place migration ladder; the old DB is left * untouched. Maps to exit code 4 (conflict) in cli.ts handle(). */ declare class SchemaTooOldError extends Error { readonly detectedVersion: number; readonly requiredVersion: number; readonly name = "SchemaTooOldError"; constructor(detectedVersion: number, requiredVersion: number); } /** * Thrown by openDb when the on-disk DB was written by a NEWER mu. * The DB is left untouched. Maps to exit code 4 (conflict). */ declare class SchemaTooNewError extends Error { readonly detectedVersion: number; readonly supportedVersion: number; /** Which stamp was newer: the table schema, or the capture triggers. */ readonly stamp: "schema" | "capture"; readonly name = "SchemaTooNewError"; constructor(detectedVersion: number, supportedVersion: number, /** Which stamp was newer: the table schema, or the capture triggers. */ stamp?: "schema" | "capture"); } /** * Thrown by openDb when the linked SQLite is older than 3.42: the capture * triggers mint HLCs with `unixepoch('subsec')`, which older versions * return NULL for. Maps to exit code 4 (conflict). Nothing was touched. */ declare class SqliteTooOldError extends Error { readonly sqliteVersion: string; readonly name = "SqliteTooOldError"; constructor(sqliteVersion: string); } /** The task kinds (`tasks.kind`): `work` is the default, `delegate` a * task-backed delegate call, `human` one only a person can close. Synced * per field; a value this build does not know is dropped by the fold, so * a newer peer's kind never trips the CHECK during ingest. */ declare const TASK_KINDS: readonly ["work", "delegate", "human"]; type TaskKind = (typeof TASK_KINDS)[number]; /** The states of a delegate call (`delegate_calls.state`). */ declare const DELEGATE_CALL_STATES: readonly ["queued", "starting", "running", "recording", "done", "failed", "refused", "cancelled"]; /** The states of a stall action (`wait_action_receipts.state`). */ declare const WAIT_ACTION_STATES: readonly ["reserved", "sending", "sent", "unknown", "refused", "released"]; type WaitActionState = (typeof WAIT_ACTION_STATES)[number]; /** The stall actions a wait may take (`wait_action_receipts.action`). */ declare const WAIT_ACTIONS: readonly ["steer", "release"]; type WaitAction = (typeof WAIT_ACTIONS)[number]; /** The states of a task-bound spawn attempt (`spawn_receipts.state`). */ declare const SPAWN_RECEIPT_STATES: readonly ["reserved", "pane", "claimed", "sending", "sent", "unknown", "failed"]; type SpawnReceiptState = (typeof SPAWN_RECEIPT_STATES)[number]; /** The schema version a fresh DB starts at. v12 adds the kernel columns * (torn_down_at, cwd, base, scored, dispatch_host, kind), task_auto and * the blockers view on top of v11's task substates; v13 adds the loop's * storage (tasks.kind/wave, agents.spawner/host/collected_head and four * machine-local tables). See CHANGELOG.md. */ declare const CURRENT_SCHEMA_VERSION = 13; /** The note kinds (`task_notes.kind`). Set once at INSERT, never changed: * a local UPDATE of it raises, and a peer's update cannot match a row. */ declare const NOTE_KINDS: readonly ["brief", "report", "verdict", "close", "note"]; type NoteKind = (typeof NOTE_KINDS)[number]; /** Tables a healthy DB must contain. Single source of truth so * `mu doctor` and any other consumer don't drift. Adding a new table * = one new entry here AND a CREATE TABLE in CURRENT_SCHEMA, plus a * CURRENT_SCHEMA_VERSION bump (sync_fingerprints went in without one: * additive, and an older mu ignores it). Sorted; exactly 26 entries. */ declare const EXPECTED_TABLES: readonly string[]; /** Op entities that cross machines. Everything else is machine-local. * Readonly tuple (not `string[]`) so `SyncedEntity` is a real union * and downstream code gets compile-time checking, not raw strings. */ declare const SYNCED_ENTITIES: readonly ["workstream", "task", "edge", "note", "message"]; /** One of the op entities that sync. Derived from the tuple, so * adding an entity is a one-line change with no type to keep in step. */ type SyncedEntity = (typeof SYNCED_ENTITIES)[number]; /** Op entities this build KNOWS are machine-local: their payloads name * a pane id or an absolute path, so a peer sending one is a real bug * and `applyOp` rejects it loudly. * * WHY THIS LIST EXISTS SEPARATELY FROM "not in SYNCED_ENTITIES" * ------------------------------------------------------------ * "Unknown" and "known-local" are different failures and were * conflated, which wedged a real fleet. A peer running a LATER (or * earlier) mu wrote `entity:"marker"` ops — legal on the writer, whose * SYNCED_ENTITIES included it. The reader treated every non-synced * entity as a bad peer, so `ingestSegment` recorded a defect and * stopped at that line FOREVER: 87% of a 20,305-line segment never * applied, and `mu sync --repair` (which only resets the watermark) * marched straight back into the same wall. * * So the rule is asymmetric on purpose: an entity we know must never * travel is a defect; an entity we simply do not recognise is tolerated * forward-compatibly — recorded in `ops`, projected nowhere, exactly * like 'message'. Reader vocabulary may lag writer vocabulary; that is * a fact of a mixed fleet, not a corruption. */ declare const MACHINE_LOCAL_ENTITIES: readonly ["agent", "workspace", "event", "broadcast"]; type MachineLocalEntity = (typeof MACHINE_LOCAL_ENTITIES)[number]; /** **Portable** tables: their rows mean the same thing on any machine, * so their ops ship. Mirrors docs/VOCABULARY.md § portable exactly. */ declare const PORTABLE_TABLES: readonly ["task_edges", "task_notes", "tasks", "workstreams"]; type PortableTable = (typeof PORTABLE_TABLES)[number]; /** **Machine-local** tables. Never bulk-copied to a peer, because a * row's meaning does not survive the trip: * * agents holds `pane_id` ('%17') — meaningless elsewhere. * worktrees holds absolute paths — /home/... vs /Users/... * on a mixed macOS/Linux fleet. * machine_identity IS the per-machine identity. * op_ctx the capture context row; defaults at rest. * op_dying capture's delete-key stash; empty at rest. * schema_version local bookkeeping. * sync_peers local bookkeeping (per-peer watermarks). * sync_fingerprints local cache of what the last clean ingest saw. * task_substates seeded identically on every machine from * TASK_SUBSTATE_ROWS; code, not data. * task_auto per-task auto-dispatch state (cli, runs, last * spawn): about THIS machine's agents. * delegate_calls one row per task-backed delegate call: the caller, * call_id and pane-side state of THIS machine. * wait_cursors per caller and ref, the last wait result consumed. * wait_registrations per caller, the refs a wait still owes it, and * whether one is in flight (generation CAS), plus the * unconsumed run baselines of its agent refs. human_needs which caller recorded a `human` task (`mu human-needed`). Rebuild cannot restore it (UNREBUILDABLE_TABLES). * agent_settles settled runs already applied (settle replay). * host_profiles ssh-free command templates per remote host. * host_channels the connection slots a host's `max_sessions` counts: * one row per live holder (invocation pid or agent pane). * wait_resumes one row per `mu wait` result that left something owed: * the full condition set behind `mu wait --resume `. * wait_action_receipts one row per stall action a wait took on a task * (task id, owner registry id, ctl incarnation, action): * the exclusion that keeps a nudge to ONE and the record * of a release. No FK: it must outlive the task and agent. * spawn_receipts one row per task-bound spawn attempt: the replay and * crash-recovery journal (reservation, pane identity, * claim, send state). No FK to `agents`: it must * outlive the agent it names. * remote_recovery a deleted remote worker row's worktree, written by * triggers (src/db-recovery.ts). No FK; rebuild loses it. * ops see below — the carrier, not cargo. * * `ops` is listed here deliberately rather than omitted. It is not * **portable** in the copy-the-table sense: the table is never * wholesale-copied. Individual op ROWS ship, one at a time, filtered * by SYNCED_ENTITIES and carried by per-machine **segments** — and * `seq` is a local-only append cursor that means nothing on a peer. * So for the only question this list answers — "is this table's * content copied across machines?" — the answer for `ops` is no. * * Consequence that falls out with no special case: `tasks.owner_id` * is an FK into `agents`, and `agents` is machine-local. Therefore * OWNERSHIP DOES NOT SYNC. The deleted db-sync.ts reached the same * conclusion via an `includeOwners` flag; here it is structural. */ declare const MACHINE_LOCAL_TABLES: readonly ["agent_settles", "agents", "delegate_calls", "host_channels", "host_profiles", "human_needs", "machine_identity", "op_ctx", "op_dying", "ops", "remote_recovery", "schema_version", "spawn_receipts", "sync_fingerprints", "sync_peers", "task_auto", "task_substates", "wait_action_receipts", "wait_cursors", "wait_registrations", "wait_resumes", "worktrees"]; type MachineLocalTable = (typeof MACHINE_LOCAL_TABLES)[number]; /** The multiplexers mu can drive. */ type MuxBackendName = "herdr" | "tmux"; /** Runtime states a mux backend can report directly for one pane. */ type MuxPaneStatus = "busy" | "needs_input" | "needs_permission"; interface MuxPane { /** Backend-specific stable pane id (tmux `%15`). Never a volatile index. */ paneId: string; /** Current foreground command (e.g. "claude", "node", "bash"). */ command: string; /** Window this pane lives in. */ windowId?: string; } /** Name of the placeholder shell window every `mu-` session keeps, * so the session outlives its last agent pane. */ declare const MU_PLACEHOLDER_WINDOW = "_mu"; /** What `ensureSession` did. */ interface EnsureSessionResult { /** The session did not exist and was created. */ created: boolean; /** The session existed but its `_mu` window was missing and was re-added. */ repaired: boolean; } /** * What `spawnAgent` needs. mu has already resolved the command it would * run; a backend that starts agents itself (herdr) needs both that string * and the `--cli` key it came from, because those select different routes. */ interface SpawnAgentPaneOptions { /** Mux session (tmux session / herdr workspace label), e.g. `mu-auth`. */ session: string; /** Window (tmux window / herdr tab) the agent's pane lives in: created * when missing, otherwise the pane is split into it. */ window: string; /** mu's agent name. Backends with their own agent registry use it as * the mux-level handle too. */ name: string; /** mu's `--cli` key, e.g. "pi". A backend that classifies agent kinds * natively maps this onto its own kind vocabulary. */ cli: string; /** Fully resolved command string mu would run. */ command: string; /** * Where `command` came from. A backend that resolves the executable * ITSELF must not silently discard an operator's explicit choice, and * needs to know which one it is so the diagnostic can name the right * thing to change: * * "cli-key" — just the `--cli` value; nothing to honour, proceed. * "env" — a `MU__COMMAND` override. * "explicit" — an explicit `--command "…"`. */ commandSource: CommandSource; /** Initial working directory for the pane. */ cwd?: string; /** Identity and state env for the pane (tmux `-e`, herdr `--env`). */ env: Record; /** * Called as soon as the pane exists, before any readiness wait. The * caller records the pane id under its topology lock and lets the lock * go while a slow readiness wait (herdr: up to 30s) is still running. * The returned promise settles when the agent is started. */ onPaneCreated?: (paneId: string) => void; /** * Keep the pane (its row and scrollback) after its command ends, armed * BEFORE the command can run, so an immediate exit is still readable with * `paneResult`. Honoured only by a backend with `paneResult`; the pane then * lives until `killPane`. Never changes a session or global default. */ retainOnExit?: boolean; } /** * A pane's terminal result as the mux itself reports it. `exited` carries the * LOCAL command's status (a remote command's status is not this: see * src/agents/gate.ts); `code` xor `signal` is set. */ type PaneResult = { state: "running"; } | { state: "exited"; code: number | null; signal: number | null; } | { state: "absent"; }; /** Where an attach should land the user. */ interface AttachTarget { /** Mux session name, e.g. `mu-auth`. */ session: string; /** Window inside that session (the agent's `tab`, or its name). */ window?: string; /** True when the caller is already inside a client of this mux, which * on tmux means `switch-client` rather than `attach-session`. */ inside?: boolean; /** True when the steps are for a shell OUTSIDE any client of this mux (the line a * dashboard copies): they must open a client, whatever the ambient env says. * Wins over `inside` and over a backend's own env detection. */ outside?: boolean; } /** One argv the caller may execute to hand the terminal to the mux. */ /** Where a spawn's command came from: the cli key itself, `MU__COMMAND`, or `--command`. */ type CommandSource = "cli-key" | "env" | "explicit"; interface MuxCommand { command: string; args: readonly string[]; /** Best-effort step: a non-zero exit is not a failure of the attach * as a whole (tmux's post-attach `select-window`, for instance). */ optional?: boolean; /** Why an `optional` step may fail (shown when it does). */ reason?: string; /** A client deliberately nested inside another tmux server's client: run it with * `$TMUX` removed (tmux refuses to nest otherwise). The shell line spells it `env -u TMUX`. */ nested?: boolean; } /** What a backend can say about its own health. Deliberately DATA, not * prose: `mu doctor` owns all rendering (human and --json). */ interface MuxHealth { /** Backend name, echoed so doctor can label the row. */ name: MuxBackendName; /** True iff the backend answered a version probe. */ ok: boolean; /** Version string as the backend reports it, or null when unreachable. */ version: string | null; /** Ambient env facts this backend cares about, in display order * (tmux: $TMUX / $TMUX_PANE). Values are null when unset. */ env: readonly { name: string; value: string | null; }[]; /** Remediation line shown when `ok` is false. */ remediation: string; } interface SendOptions { /** 0 drops herdr's `--wait` (submit and return); any other value is ignored. */ readinessMs?: number; /** Called when the send could not be confirmed as submitted. */ onUndelivered?: (warning: SendWarning) => void; } /** Why a send could not be confirmed as submitted. */ interface SendWarning { paneId: string; /** 'paste-vanished' — pane looked calm, but the text never registered. */ reason: "paste-vanished"; message: string; } interface CaptureOptions { /** * Number of trailing lines to capture. Omitted = full scrollback. * 0 = visible pane only. */ lines?: number; } /** * One copy-pasteable shell line for `steps`, joined with `&&` so a * failed required step stops everything after it. An `optional` step is * rendered `{ step || true; }`: its own failure never fails the line * and never stops what follows. Callers catch what `attachCommands` * throws (herdr: no such workspace) and omit the line. */ declare function attachShellLine(steps: readonly MuxCommand[]): string; /** * Base class for "the multiplexer itself failed". Each backend * subclasses it (tmux → `TmuxError`) so `handle()` can map the whole * family to exit code 5 (substrate unavailable) with one `instanceof`. */ declare class MuxError extends Error { name: string; } /** * Thrown when a verb references a pane id that doesn't exist on the * running multiplexer. Distinct from `MuxError` (which wraps any mux * command failure) so callers can map it to a specific exit code * (`mu` maps it to 5 alongside other mux issues, but the message is * more actionable than raw backend stderr). * * The backend-specific half of the remediation comes from the backend * that raised it: showing a herdr user `tmux info | head` would be * worse than showing no hint at all. Callers that construct this error * already hold the active backend, so passing it is free; the * no-backend form degrades to mu's own verbs only. */ declare class PaneNotFoundError extends Error { readonly paneId: string; readonly name = "PaneNotFoundError"; constructor(paneId: string, backend?: MuxDiagnostics); } /** The slice of a backend `PaneNotFoundError` needs. Structural so the * error type doesn't depend on the whole backend contract. */ interface MuxDiagnostics { readonly name: MuxBackendName; } /** * Thrown by `detectMux()` when no supported multiplexer is available. * Maps to exit 5 (substrate unavailable) — the same bucket as "tmux is * installed but the server is unreachable", because from the caller's * point of view both mean "mu cannot reach a pane right now". */ declare class NoMultiplexerError extends Error { readonly tried: readonly string[]; readonly name = "NoMultiplexerError"; constructor(tried: readonly string[]); } /** * One multiplexer implementation. Methods mirror the operations mu * actually performs; nothing is here speculatively. * * Implementations must be stateless value objects (a frozen record of * functions), so swapping the active backend is a pointer assignment * and tests can install a fake without teardown ordering hazards. */ interface MuxBackend { readonly name: MuxBackendName; /** True when `spawnAgent` returns only once the MUX considers the agent * ready for input (herdr's `agent start` blocks until it detects the * agent). spawn.ts then skips its own liveness/readiness polling. A * capability flag, never a backend-name check. */ readonly spawnAgentWaitsReady: boolean; /** True when `spawnAgent` runs `opts.command` as a shell command line in * the pane (tmux). herdr starts a known agent kind itself, so it cannot * run a host profile's shell template (`spawn --host`). A capability * flag, never a backend-name check. */ readonly runsShellCommand: boolean; /** True iff this backend can drive panes on this machine right now. * Consulted by `detectMux()` in precedence order. */ available(): Promise; isValidPaneId(s: string): boolean; assertValidPaneId(s: string): void; /** Create `session` with its placeholder `_mu` window when missing; * re-add a missing `_mu` window to an existing one. Load-bearing: * mux errors propagate. */ ensureSession(session: string): Promise; /** Idempotent. herdr closes the workspace and refuses (surfacing * `HerdrWorkspaceGroupCloseError`) when it has linked worktree * workspaces mu did not create. */ killSession(session: string): Promise; /** Create the agent's pane (new window, or a split of the existing * `opts.window`; the session is created when missing) and run the * agent in it. Returns the pane id. tmux: one step running * `opts.command`. herdr: a bare shell pane, then `agent start --kind` * with a busy retry; refuses `commandSource` ≠ "cli-key" with * `HerdrCommandOverrideError` before touching herdr and closes the * bare pane when the start fails. */ spawnAgent(opts: SpawnAgentPaneOptions): Promise; /** Every pane in `session`. `[]` when the session (or server) is gone; * any other failure throws, so a caller that prunes on absence never * reads an unreachable mux as "zero panes". */ listPanes(session: string): Promise; /** False only when the mux reports the pane (or server) gone; throws * when the probe itself fails, so liveness gates fail closed. */ paneAlive(paneId: string): Promise; killPane(paneId: string): Promise; capture(paneId: string, opts?: CaptureOptions): Promise; /** Ordered argv steps that land the user on `target` (some optional, * see `MuxCommand.reason`). May throw when the target cannot be * resolved (herdr: no workspace carries the label). Join with * `attachShellLine` for a printable line. */ attachCommands(target: AttachTarget): Promise; /** The pane's runtime state as the mux itself reports it, or undefined * when the pane is gone or unclassified. herdr only: for non-pi agents. */ paneStatus?(paneId: string): Promise; /** tmux only: whether the pane runs, exited (a retained pane: `retainOnExit`) * or is gone. Unlike `paneAlive` (handle existence), an exited pane is NOT * running here. Throws when the probe itself fails: unknown, never a result. */ paneResult?(paneId: string): Promise; /** herdr only: atomic `agent prompt`. tmux has no reliable paste (cut in * 4.0), so callers branch on the method's presence, never on * `mux.name`, and refuse when it is absent. */ sendToPane?(paneId: string, text: string, opts?: SendOptions): Promise; /** The argv steps that type pi's `/reload` into `paneId` from outside, for * an agent whose extension cannot take `/reload` over ctl. herdr only * (its `agent prompt`); tmux has no reliable paste, so it omits this and * callers branch on presence. */ reloadCommands?(paneId: string): readonly MuxCommand[]; /** * Name of the session the CALLER is running inside, or undefined when * outside one. * Suggestion only (the no-scope hint, the TUI start workstream); never routes a verb. */ currentSessionName(): Promise; /** Version + ambient facts for `mu doctor`. Never throws: an * unreachable backend reports `ok: false`. */ healthCheck(): Promise; } /** Look up a backend by name. Throws on unknown name. Backs `MU_MUX`. */ declare function muxByName(name: MuxBackendName): MuxBackend; /** * Resolve the active multiplexer backend. * * The ladder, in order: * * 1. `MU_MUX=` — explicit override. Also the test seam. An * unknown value throws rather than silently falling through: a * typo'd backend name should fail loud, not quietly run on tmux. * 2. Ambient signal — an env var proving the CALLER is already * inside a managed pane of that mux ($HERDR_ENV for herdr, $TMUX * or $TMUX_PANE for tmux). The most specific signal wins, since a * mux can run nested inside another: herdr panes routinely host a * tmux server, so BOTH sets of vars can be present at once and * $HERDR_ENV — the narrower claim — is checked first. * 3. Availability — whichever backend's binary actually runs. * Ties break in BACKENDS order (tmux is the incumbent). * 4. Throw `NoMultiplexerError`. * * Note rungs 2 and 3 are separate on purpose. `$TMUX` says "the caller * is in a tmux pane"; `tmux -V` says "tmux works here". mu can spawn a * detached session from a plain shell, so rung 3 alone is sufficient to * operate — rung 2 exists only to pick the RIGHT mux when several are * installed. */ declare function detectMux(): Promise; /** * The active backend, resolved once per process and memoized. * * Memoized because detection can shell out (`tmux -V`) and mu is a * short-lived CLI that touches the mux many times per invocation; the * answer cannot change mid-process in any way that mu should react to. */ declare function activeMux(): Promise; /** * Install a backend directly, bypassing detection. Returns the previous * value so tests can restore it. Mirrors `setTmuxExecutor`. * * Production code never calls this — use `activeMux()`. */ declare function setMuxForTests(backend: MuxBackend | undefined): MuxBackend | undefined; /** Drop the memoized backend so the next `activeMux()` re-detects. */ declare function resetMux(): void; /** * "herdr itself failed" — the server returned a JSON error, or the * binary could not be reached. A `MuxError`, so `handle()` maps the * whole family to exit 5 with one `instanceof`. Mirrors `TmuxError`. */ declare class HerdrError extends MuxError { readonly args: readonly string[]; readonly stderr: string; readonly stdout: string; readonly exitCode: number | null; /** herdr's machine-readable error code (e.g. `pane_not_found`), when * the stderr payload parsed as the documented JSON envelope. */ readonly code?: string | undefined; constructor(args: readonly string[], stderr: string, stdout: string, exitCode: number | null, /** herdr's machine-readable error code (e.g. `pane_not_found`), when * the stderr payload parsed as the documented JSON envelope. */ code?: string | undefined); } /** * herdr rejected our ARGUMENTS (exit 2). Deliberately not a `MuxError`: * the substrate is healthy and mu asked it something ungrammatical. * That is a bug in this file — most likely CLI drift after a herdr * upgrade — and must surface as one instead of being swallowed into * the "herdr is down" bucket, where it would waste hours. */ declare class HerdrSyntaxError extends Error { readonly args: readonly string[]; readonly output: string; readonly name = "HerdrSyntaxError"; constructor(args: readonly string[], output: string); } /** * `mu agent spawn --cli ` named something herdr does not recognise * as an interactive agent kind. * * A REFUSAL, not a fallback. herdr's `pane run ` would happily * start the binary, but herdr would then never classify that pane, and * on this backend herdr's report is mu's runtime state source. A pane * mu can start but never observe is the same family of failure as a pane mu records * with nothing running in it: it looks fine and lies forever. * * Not a `MuxError`: the substrate is healthy and answered precisely. * This is an operator asking for something the active backend cannot * do, which `handle()` maps to exit 2 alongside the usage family. * * herdr is the AUTHORITY on the kind list — mu does not hardcode one, * it forwards `--cli` and translates herdr's rejection. A herdr release * that adds a kind therefore works without a mu change. */ declare class HerdrUnsupportedCliError extends Error { readonly cli: string; readonly detail: string; readonly name = "HerdrUnsupportedCliError"; constructor(cli: string, detail: string); } /** * The operator pinned an exact command line — `--command "…"` or * `MU__COMMAND` — and herdr cannot honour it. * * `herdr agent start --kind ` resolves the canonical executable * ITSELF; there is no "use this binary instead" flag. Args after `--` * are passed to the agent, NOT used to choose it (verified against * 0.8.0: `agent start x --kind pi -- --model m` reports `argv:["pi"]`; * 0.9.0 still exposes no override flag), so forwarding an override * there would append a binary * NAME as an argument and start the wrong thing while reporting success. * * Accepting the override and not honouring it is the one outcome ruled * out, so mu refuses and names the variable. */ declare class HerdrCommandOverrideError extends Error { readonly cli: string; readonly command: string; /** The `MU__COMMAND` var, when that is where the override * came from; undefined for an explicit `--command`. */ readonly envVar?: string | undefined; readonly name = "HerdrCommandOverrideError"; constructor(cli: string, command: string, /** The `MU__COMMAND` var, when that is where the override * came from; undefined for an explicit `--command`. */ envVar?: string | undefined); } /** * `mu workstream destroy` asked herdr to close the workstream's * workspace, and herdr refused because worktree workspaces are linked * to it (herdr 0.9.0+). * * mu does NOT retry with `--group`. Those sibling workspaces are not * mu's: they were created by `herdr worktree`, they host panes mu never * spawned, and mu has no way to know whether the user still wants them. * Closing them to satisfy a teardown would destroy unsaved work with no * undo, which is the one outcome ruled out. * * Not a `MuxError`: the substrate is healthy and answered precisely. * This is a decision only the operator can make, so it lands in the * usage lane (exit 2) beside the other herdr refusals. Thrown BEFORE * any DB rows are deleted — `teardownWorkstream` kills the mux session * first for exactly this reason — so the workstream is left intact and * the command is safe to re-run. */ declare class HerdrWorkspaceGroupCloseError extends Error { readonly session: string; readonly workspaceId: string; readonly detail: string; readonly name = "HerdrWorkspaceGroupCloseError"; constructor(session: string, workspaceId: string, detail: string); } interface HerdrExecResult { stdout: string; stderr: string; exitCode: number | null; } type HerdrExecutor = (args: readonly string[]) => Promise; /** * Install a custom executor (for tests). Returns the previous executor * so tests can restore it cleanly. Production code never calls this. * Required: the fast test tier may not shell out. */ declare function setHerdrExecutor(executor: HerdrExecutor): HerdrExecutor; /** Restore the real (child_process-backed) executor. */ declare function resetHerdrExecutor(): void; /** * True iff a `herdr status` payload describes a server mu can drive. * * Pure, and EXPORTED because test/_mux.ts gates the real-herdr * integration tier on the same question — one parser, not two that * drift (they already had, across the 0.8 → 0.9 rename below). * * WHICH COMPATIBILITY LINE. herdr 0.9.0 split the old single * `compatible: yes|no` into two, and they do NOT mean the same thing: * * endpoint_compatible — the stable public API generation. * `no` means the server predates * endpoint generation 1 and needs a * one-time upgrade before ANY verb * works. Load-bearing. * private_protocol_compatible — the internal client↔server protocol. * Since 0.9.0 a mismatch here only * disables the affected action and * leaves running agents untouched, so * it must NOT gate availability: a * client one release ahead of its * server still drives panes fine. * * The bare `compatible:` line is herdr ≤0.8.x and is still honoured, so * mu does not silently start trusting an old incompatible server. It * cannot collide with the two 0.9 lines: `^\s*` will not consume the * `endpoint_` / `private_protocol_` prefixes. * * A MISSING line is fine in every case — absence of evidence is not * incompatibility, and herdr omits these entirely when no server runs * (which the `status: running` check has already rejected). */ declare function isHerdrStatusUsable(stdout: string): boolean; /** * The herdr implementation of `MuxBackend`. A frozen record of the * module's functions — no state of its own, so swapping backends is a * pointer assignment (see ./detect.ts). */ declare const herdrBackend: MuxBackend; /** Back-compat alias: tmux-specific name predating the MuxBackend extraction. */ type TmuxPane = MuxPane; declare class TmuxError extends MuxError { readonly args: readonly string[]; readonly stderr: string; readonly stdout: string; readonly exitCode: number | null; constructor(args: readonly string[], stderr: string, stdout: string, exitCode: number | null); } /** * Stable tmux pane IDs are of the form `%N` (e.g. "%15"). They never change * for the lifetime of the pane. **Pane indexes** (0, 1, 2…) are volatile and * shift when other panes close — never store or pass them. */ declare const PANE_ID_RE: RegExp; declare function isValidPaneId(s: string): boolean; declare function assertValidPaneId(s: string): void; interface TmuxExecResult { stdout: string; stderr: string; exitCode: number | null; } type TmuxExecutor = (args: readonly string[]) => Promise; /** * Install a custom executor (for tests). Returns the previous executor so * tests can restore it cleanly. Production code should never call this. */ declare function setTmuxExecutor(executor: TmuxExecutor): TmuxExecutor; /** Restore the real (child_process-backed) executor. */ declare function resetTmuxExecutor(): void; /** * Run an arbitrary tmux command. The single point of contact with the * tmux binary; every higher-level operation in this module goes through it. * * Throws `TmuxError` on non-zero exit. Returns stdout on success. */ declare function tmux$1(args: readonly string[]): Promise; declare function setSleepForTests(impl: (ms: number) => Promise): (ms: number) => Promise; declare function resetSleep(): void; /** Test-aware sleep — honours `setSleepForTests`. Public so other modules * (notably `agents.ts` for spawn liveness polling) get free no-op-ing in * tests without re-implementing the swap. */ declare function sleep(ms: number): Promise; /** * Create `session` with its placeholder `_mu` window when it is missing, * and re-add the window when the session exists without it (the operator * killed it, or the session predates it). Without `_mu`, closing the last * agent kills the last window and tmux destroys the session. */ declare function ensureSession(session: string): Promise; /** * Idempotent: succeeds even if the session is already gone. * * Four swallowed shapes: * - "can't find session: " — session never existed. * - "session not found" — alternate phrasing on some tmux builds. * - "no server running on " (or "error connecting to (No * such file or directory)", see NO_SERVER_RE) — the tmux server itself has exited * (typical when the test suite runs against a private `tmux -L * ` server and the just-killed session was its last; tmux * quietly shuts the server down). Without this, killSession would * throw on the very next idempotent call — only visible under * the test harness's private socket. * - "no current target" — the server is UP but has zero * sessions, so `-t ` has no session list to resolve against * and tmux never gets as far as "can't find session". Reachable * whenever the server outlives its last session: the suite's * private server (`exit-empty off`, see test/_global-teardown.ts) * and any user whose ~/.tmux.conf sets `exit-empty off`. Same * meaning as "can't find session" for our purposes — the session * we were asked to kill is gone. */ declare function killSession(name: string): Promise; /** * Create the agent's pane and run `opts.command` in it, one tmux call: * - session doesn't exist → new session+window+pane * - session exists, no such window → new window for this agent * - session and window both exist → split the window to add a pane * Returns the pane's stable id (`%15`). */ declare function spawnAgent$1(opts: SpawnAgentPaneOptions): Promise; /** * List ALL panes in a tmux session (across every window). * * Note `list-panes -t ` (no -s) lists panes in the current * *window* of that session, not the whole session — a common gotcha. * `-s` is the flag that says "all panes in this session." * * Returns `[]` (not throws) when the session doesn't exist or has no * panes. tmux destroys a session as soon as its last pane closes, so the * "session was just here a moment ago" case is normal during reconcile. * tmux's error wording in this case varies ("can't find session", * "can't find window", or "no current target" when the server is up * with zero sessions), so we match any of them. Every other failure * throws. */ declare function listPanes(session: string): Promise; /** Idempotent: succeeds even if the pane is already gone. */ declare function killPane(paneId: string): Promise; declare function paneAlive(paneId: string): Promise; /** * The pane's run state: running, exited (a retained pane keeps its row, with * `#{pane_dead_status}` / `#{pane_dead_signal}`) or absent. Only "the pane or * server is gone" is absent; any other failure throws, as `paneAlive` does. */ declare function paneResult(paneId: string): Promise; /** * Name of the tmux session this process is running inside, or * undefined outside tmux. Gated on `$TMUX` so a call from a plain * shell doesn't get whatever session the server happens to consider * current. A `$TMUX` naming another server (an outer tmux around mu's * private one) is outside too. */ declare function currentSessionName(): Promise; /** * Read pane scrollback as plain text (no ANSI escapes). * * - No options: full scrollback (`-S - -E -`) * - `lines: 0`: visible pane only * - `lines: N`: last N lines, ending at the last non-blank row * * `-S -N` counts from the TOP of the visible screen, so tmux returns the * N rows above the screen plus every visible row, blank rows below the * cursor included. That range always holds the last N non-blank-ended * lines, so drop the trailing blank rows and keep the last N. */ declare function capture(paneId: string, opts?: CaptureOptions): Promise; /** How the caller gets back after an attach from here: `$TMUX` on mu's server (`switch`), * on another one (`nested`: the outer tmux needs a doubled prefix), or none (`plain`). */ declare function tmuxAttachMode(): "switch" | "nested" | "plain"; declare function attachCommands(target: AttachTarget): Promise; /** * Version probe + the two ambient env facts tmux exposes. Returns DATA; * `mu doctor` owns every string the user sees, so a second backend * reporting different env vars needs no doctor change. */ declare function healthCheck(): Promise; /** * The tmux implementation of `MuxBackend`. A frozen record of the * module's functions — no state of its own, so swapping backends is a * pointer assignment (see ./detect.ts). */ declare const tmuxBackend: MuxBackend; type mux_AttachTarget = AttachTarget; type mux_CaptureOptions = CaptureOptions; type mux_EnsureSessionResult = EnsureSessionResult; type mux_HerdrCommandOverrideError = HerdrCommandOverrideError; declare const mux_HerdrCommandOverrideError: typeof HerdrCommandOverrideError; type mux_HerdrError = HerdrError; declare const mux_HerdrError: typeof HerdrError; type mux_HerdrSyntaxError = HerdrSyntaxError; declare const mux_HerdrSyntaxError: typeof HerdrSyntaxError; type mux_HerdrUnsupportedCliError = HerdrUnsupportedCliError; declare const mux_HerdrUnsupportedCliError: typeof HerdrUnsupportedCliError; type mux_HerdrWorkspaceGroupCloseError = HerdrWorkspaceGroupCloseError; declare const mux_HerdrWorkspaceGroupCloseError: typeof HerdrWorkspaceGroupCloseError; declare const mux_MU_PLACEHOLDER_WINDOW: typeof MU_PLACEHOLDER_WINDOW; type mux_MuxBackend = MuxBackend; type mux_MuxBackendName = MuxBackendName; type mux_MuxCommand = MuxCommand; type mux_MuxDiagnostics = MuxDiagnostics; type mux_MuxError = MuxError; declare const mux_MuxError: typeof MuxError; type mux_MuxHealth = MuxHealth; type mux_MuxPane = MuxPane; type mux_MuxPaneStatus = MuxPaneStatus; type mux_NoMultiplexerError = NoMultiplexerError; declare const mux_NoMultiplexerError: typeof NoMultiplexerError; type mux_PaneNotFoundError = PaneNotFoundError; declare const mux_PaneNotFoundError: typeof PaneNotFoundError; type mux_PaneResult = PaneResult; type mux_SendOptions = SendOptions; type mux_SendWarning = SendWarning; type mux_SpawnAgentPaneOptions = SpawnAgentPaneOptions; declare const mux_activeMux: typeof activeMux; declare const mux_attachShellLine: typeof attachShellLine; declare const mux_detectMux: typeof detectMux; declare const mux_herdrBackend: typeof herdrBackend; declare const mux_isHerdrStatusUsable: typeof isHerdrStatusUsable; declare const mux_muxByName: typeof muxByName; declare const mux_resetHerdrExecutor: typeof resetHerdrExecutor; declare const mux_resetMux: typeof resetMux; declare const mux_setHerdrExecutor: typeof setHerdrExecutor; declare const mux_setMuxForTests: typeof setMuxForTests; declare const mux_tmuxAttachMode: typeof tmuxAttachMode; declare const mux_tmuxBackend: typeof tmuxBackend; declare namespace mux { export { type mux_AttachTarget as AttachTarget, type mux_CaptureOptions as CaptureOptions, type mux_EnsureSessionResult as EnsureSessionResult, mux_HerdrCommandOverrideError as HerdrCommandOverrideError, mux_HerdrError as HerdrError, mux_HerdrSyntaxError as HerdrSyntaxError, mux_HerdrUnsupportedCliError as HerdrUnsupportedCliError, mux_HerdrWorkspaceGroupCloseError as HerdrWorkspaceGroupCloseError, mux_MU_PLACEHOLDER_WINDOW as MU_PLACEHOLDER_WINDOW, type mux_MuxBackend as MuxBackend, type mux_MuxBackendName as MuxBackendName, type mux_MuxCommand as MuxCommand, type mux_MuxDiagnostics as MuxDiagnostics, mux_MuxError as MuxError, type mux_MuxHealth as MuxHealth, type mux_MuxPane as MuxPane, type mux_MuxPaneStatus as MuxPaneStatus, mux_NoMultiplexerError as NoMultiplexerError, mux_PaneNotFoundError as PaneNotFoundError, type mux_PaneResult as PaneResult, type mux_SendOptions as SendOptions, type mux_SendWarning as SendWarning, type mux_SpawnAgentPaneOptions as SpawnAgentPaneOptions, mux_activeMux as activeMux, mux_attachShellLine as attachShellLine, mux_detectMux as detectMux, mux_herdrBackend as herdrBackend, mux_isHerdrStatusUsable as isHerdrStatusUsable, mux_muxByName as muxByName, mux_resetHerdrExecutor as resetHerdrExecutor, mux_resetMux as resetMux, mux_setHerdrExecutor as setHerdrExecutor, mux_setMuxForTests as setMuxForTests, mux_tmuxAttachMode as tmuxAttachMode, mux_tmuxBackend as tmuxBackend }; } /** * What kind of reconciliation pass to run. * * "full" Default for `mu state` and `mu agent list`. Prunes * ghosts (deleting the registry row, which fires the * deleteAgent reaper that flips IN_PROGRESS tasks back * to OPEN with [reaper] notes), surfaces orphans. * * "report-only" Pure observation. Counts would-be-pruned ghosts * without deleting (no DB writes); surfaces orphans * (pure read). Used by `mu doctor` (read-only * diagnostic). * * Mid-spawn placeholders are protected directly in the prune loop, so read * paths no longer need a separate mode just to avoid racing spawn's worktree * pre-stage. */ type ReconcileMode = "full" | "report-only"; interface ReconcileOptions { /** The workstream whose registry rows we're reconciling. */ workstream: string; /** * Override the tmux session name. Defaults to `mu-`. Useful * for tests and for the rare case where a workstream's tmux session was * created with a non-default name. */ tmuxSession?: string; /** * Which kind of pass to run. Default is `"full"` (the documented * mutating behaviour `mu agent list` has always had). See * `ReconcileMode` for the full per-mode contract. * * BREAKING: this replaces the previous `dryRun?: boolean` flag. * Migration: `dryRun: true` → `mode: "report-only"`; default * (`dryRun: false` / unset) → `mode: "full"`. */ mode?: ReconcileMode; } interface ReconcileReport { /** Number of registry rows whose pane was gone. In `report-only` mode * this is the count of rows that WOULD have been pruned; in `full` * mode it's the count actually deleted. */ prunedGhosts: number; /** Panes in the workstream's tmux session that look like agents but * aren't in the registry. Never auto-registered. */ orphans: MuxPane[]; /** Which mode this report was generated in. Lets callers switch their * output text ("agents pruned" vs "would-be-pruned (suppressed)") * without re-deriving from options. */ mode: ReconcileMode; } declare function reconcile(db: Db, opts: ReconcileOptions): Promise; type AbortResult = { agent: string; workstream: string; before: CtlState; after: CtlState; /** True when pi settled (or was never busy). */ settled: boolean; /** False when the agent was not busy, so no abort was sent. */ aborted: boolean; /** pi had queued messages before the abort (pi returns them to the editor). */ pending: boolean; elapsedMs: number; }; type AbortAgentOptions = { workstream: string; timeoutMs?: number; /** Control socket path. Default: derived from the agent's identity. */ socket?: string; /** interrupt only: the ctl incarnation the caller saw; another pi is refused (AgentReplacedError). */ expectIncarnation?: string; }; declare function abortAgent(db: Db, name: string, opts: AbortAgentOptions): Promise; type InterruptResult = { /** pi was busy, so its turn was aborted before the send. */ wasBusy: boolean; /** pi had queued messages before the abort; pi put them back in the pane's editor. */ pending: boolean; /** * pi's run count after the aborted run settled and before the send: the * `mu agent wait --after-runs` baseline for the run the text starts. */ runs?: number; /** The id the interrupt carried: `mu agent check ` asks whether it landed. */ msgId: string; /** pi's incarnation: the `--inc` for `mu agent wait`. */ incarnation?: string; /** Set when the reply never arrived and `status {msgId}` confirmed the interrupt landed. */ receipt?: "confirmed-after-timeout"; }; /** * Make a pi agent act on `text` now: abort a running turn, wait for pi to * settle (AgentAbortTimeoutError after timeoutMs, text not sent), then * send `text` as a new run. Never pastes: non-pi agents get * AgentAbortNeedsCtlError. */ declare function interruptAgent(db: Db, name: string, text: string, opts: AbortAgentOptions): Promise; /** * Everything already upstream of the worker: `base` (recorded at spawn), * the root's HEAD, `origin/HEAD` when it resolves, and the root's * `@{upstream}` when set. The worker's own commits are * `git rev-list HEAD --not `, so a manual rebase onto advanced * local main or a fetched remote does not count the base's commits as the * worker's. Unresolved refs are skipped; the result is deduped SHAs and * always starts with `base`. */ declare function upstreamRefs(root: string, base: string): Promise; /** `--worktree` was asked of a root that is not inside a git repo. Exit 4. */ declare class WorktreeVcsRequiredError extends Error { readonly root: string; readonly name = "WorktreeVcsRequiredError"; constructor(root: string); } /** * `agent close` (or teardown) refused: the worktree has uncommitted work. * `--discard` overrides. Exit 4. */ declare class WorktreeDirtyError extends Error { readonly agent: string; readonly workstream: string; readonly path: string; readonly files: readonly string[]; readonly name = "WorktreeDirtyError"; constructor(agent: string, workstream: string, path: string, files: readonly string[]); } /** * Spawn found a worktree dir at the agent's path that is not its own clean * leftover (dirty, new commits, or not made by mu). Exit 4. */ declare class WorktreePathNotEmptyError extends Error { readonly agent: string; readonly workstream: string; readonly path: string; readonly why: string; readonly name = "WorktreePathNotEmptyError"; constructor(agent: string, workstream: string, path: string, why: string); } /** * Close would keep unmerged commits on `mu//-`, but a branch * of that name already points at another commit. It is never moved; the * worktree and its registry row are kept. Exit 4. */ declare class WorktreeBranchExistsError extends Error { readonly agent: string; readonly workstream: string; readonly path: string; readonly branch: string; readonly name = "WorktreeBranchExistsError"; constructor(agent: string, workstream: string, path: string, branch: string); } /** * `agent close` refused to remove a worktree something may still use: a * process has a file in it open for writing (`holder`), or no scan could * prove there is none (`holder` undefined). `--force` overrides either. The * tree, its registry row and the agent row are kept. Exit 4. */ declare class WorktreeHeldError extends Error { readonly agent: string; readonly workstream: string; readonly path: string; readonly holder: WorktreeHolder | undefined; readonly name = "WorktreeHeldError"; constructor(agent: string, workstream: string, path: string, holder: WorktreeHolder | undefined); /** The writer's pid; undefined when the refusal is an unprovable scan. */ get pid(): number | undefined; } /** `/worktrees`: where mu puts the worktrees it creates. */ declare function worktreesRoot(): string; /** The canonical path of an agent's worktree. */ declare function worktreePath(workstream: string, agent: string): string; /** * `git -C root worktree add --detach path [from]`. Returns the commit the * worktree is at. The caller registers it. */ declare function createWorktree(opts: { root: string; path: string; from?: string; signal?: AbortSignal; }): Promise<{ base: string; }>; /** Remove a worktree. `force` also drops uncommitted work. */ declare function removeWorktree(root: string, path: string, opts?: { force?: boolean; }): Promise; type WorktreeState = "dirty" | "clean-merged" | "clean-unmerged" | "missing"; /** * Where a worktree stands, for the close rule: * missing the dir is gone * dirty uncommitted work, or git cannot say (unverifiable counts as dirty) * clean-merged clean and every commit is already upstream: HEAD is `base`, * or reachable from the root's HEAD / origin/HEAD / @{upstream} * clean-unmerged clean with commits of its own * `base` may be empty (a row migrated without one): then only the root's refs count. */ declare function worktreeState(path: string, base: string, root: string): Promise; interface WorktreeRow { path: string; agent: string; workstream: string; /** The checkout the worktree was added from ("" when unknown: migrated rows). */ root: string; /** The commit the worktree was created at ("" when unknown). */ base: string; /** HEAD last brought back by a collect; reuse and removal accept it like `base`. */ lastCollectedHead: string | null; createdAt: string; } declare function registerWorktree(db: Db, row: Omit): void; declare function forgetWorktree(db: Db, path: string): void; declare function getWorktreeAt(db: Db, path: string): WorktreeRow | undefined; /** The registry row of an agent's worktree (newest when a name was reused). */ declare function getWorktreeFor(db: Db, workstream: string, agent: string): WorktreeRow | undefined; declare function listWorktrees(db: Db, workstream?: string): WorktreeRow[]; /** * Make `path` free for a new worktree of `agent`. Nothing on disk: fine (a * stale registry row is forgotten). A worktree there is removed only if mu * made it, it is clean, and HEAD is the base (or the last collected head): * its own unused leftover. Anything else refuses with one line: no branch * is made on reuse. The close rule's scan runs first: a process writing in * the tree refuses (`mu agent close ... --force` removes it); a cwd or reader * there, or a scan that cannot run, is returned as a warning (the removal is * git's unforced one, which still refuses dirt). */ declare function reclaimWorktreePath(db: Db, opts: { agent: string; workstream: string; path: string; }): Promise; interface ReleasedWorktree { path: string; /** What happened on disk. `gone`: the dir was already missing. */ action: "removed" | "kept-branch" | "discarded" | "gone"; /** The branch made to keep unmerged commits. */ branch?: string; /** Processes still in the tree when it was removed (their cwd, a read-only file; or, under `force`, a writer). */ warnings?: string[]; } /** * A process found in a worktree. `writer`: it has a file there open for * writing (the one kind whose work a removal can lose); else its cwd or a * read-only file is there, which a close reports and goes past. */ interface WorktreeHolder { pid: number; /** Its command name (`/proc//comm`, lsof's `c`), "" when unknown. */ command: string; writer: boolean; } /** * The one close rule (README § Worktrees): * dirty refuse (WorktreeDirtyError) unless `discard`, which removes it * clean, merged remove * clean, unmerged make `mu//-` at HEAD in the root repo, then remove * missing forget the row * The registry row is forgotten once the dir is handled. Re-checks the state * itself: a write between a caller's pre-check and this call still refuses. * * Quiescence before any branch or removal, the remote close rule's local * twin: the tree is parked at `asideOfLocal(path)` (a writer addressing the * old path now fails), `worktreeHolders` must prove no process has its cwd or * an open file in it, and the state is judged again under the park. A file * open for writing refuses (`WorktreeHeldError`), as does a scan that cannot * run; `force` overrides both. A process with only its cwd or a read-only * file there (a shell cd'd in, a reader, a dashboard) loses nothing: it is * named in `warnings` and the close goes on. Any refusal moves the tree back. A park a crash left is moved * back first, then the rule runs again. */ declare function releaseWorktree(db: Db, row: WorktreeRow, opts?: { discard?: boolean; force?: boolean; probe?: { proc: string; lsof: string; }; }): Promise; interface CloseAgentOptions { /** * Throw the agent's worktree away even when it has uncommitted work. * Lossy; never makes a branch. Without it a dirty worktree refuses * (WorktreeDirtyError) before anything is killed. */ discard?: boolean; /** * A remote worktree whose HEAD was never collected: keep it as the remote * branch `mu//-` and remove the worktree. Never overrides * uncommitted work, and `discard` does not stand in for it. A local * worktree always keeps its branch; the flag is ignored there. */ keepBranch?: boolean; /** * The `revision` of `previewClose`. Checked under the agent lock before * anything is killed (`RevisionMismatchError`, exit 4, nothing done). An * agent that no longer exists is a no-op whatever the value: nothing is * left to destroy. Absent: close without a preview. */ ifRevision?: number; /** * Close the remote worktree a REMOVED agent left (`remote_recovery`, the * original registry incarnation it names), not a live agent. Required when * several records share the name; without it exactly one selects itself. * Refuses while an agent holds the name or the path. */ recoveryId?: number; /** * Close past a refusal the caller owns: the pane's liveness could not be * checked (the probe failed, or no multiplexer answers), a process has a * file in the worktree open for writing, or no scan could prove none has * (no /proc, no lsof). Never overrides a pane that answers alive, nor * uncommitted work. Without it these refuse (`AgentPaneStillAliveError` * state `unknown`, `WorktreeHeldError`) with everything kept. */ force?: boolean; } interface CloseAgentResult { killedPane: boolean; deletedRow: boolean; /** What the close rule did to the agent's worktree (a remote one included); null when it had none. */ worktree: ReleasedWorktree | null; /** Set when a remote recovery record was closed: its id and whether the record was removed. */ recovery?: { id: number; deleted: boolean; }; } /** What `agent close` would do, read only. */ interface ClosePreview { agentName: string; workstream: string; /** `agent`: a registry row; `orphan-worktree`: only a registered worktree * is left (a failed spawn or an earlier close); `recovery`: the agent row * is gone but `remote_recovery` kept its remote worktree; `none`: nothing. */ kind: "agent" | "orphan-worktree" | "recovery" | "none"; /** `agentRevision`: the value for `--if`. 0 when `kind` is `none`. */ revision: number; /** The registry incarnation (`AgentRow.incarnation`); null without a row. */ incarnation: number | null; paneId: string | null; /** Tasks the row owns, any workstream. IN_PROGRESS ones go back to OPEN. */ ownedTasks: { ref: string; status: string; reverts: boolean; }[]; /** The worktree the close rule would handle; null when none is registered. */ worktree: { path: string; state: WorktreeState; } | null; /** * The remote worktree of a `--host --worktree` agent that the close removes * under the remote rule (`closeRemoteWorktree`), read from the row alone: * a preview runs no remote command (at `--max-sessions 1` the live pane * holds the only channel). Null for a local or `--cwd`-only agent. */ remote: { host: string; path: string; base: string; collectedHead: string | null; } | null; /** * The selected recovery record (`kind` `recovery`), under its own id: the * ORIGINAL registry incarnation, never `incarnation`. `refusal` is why an * apply would refuse right now (a current agent, a changed profile), null * when it would go on. Null otherwise. */ recovery: { id: number; aside: string; reason: string; preservedAt: string; refusal: string | null; } | null; /** True when the close would refuse for uncommitted work (no `discard`). */ refusesDirty: boolean; discard: boolean; keepBranch: boolean; } /** Preview a close: nothing is written, nothing is killed. The one read an * apply is derived from, for orphans and torn-down workstreams too. * * Identity and revision come from ONE read transaction, taken after the * slow worktree check: the agent row, its worktree row, the owned tasks * and the revision are all of the same snapshot, also against another * process's writes. The check awaits git, so * the name may be replaced meanwhile; the snapshot taken before it is then * stale and the read is retried, and a name that will not hold still is * refused (`AgentReplacedError`) rather than described half and half. */ declare function previewClose(db: Db, name: string, opts: { workstream: string; discard?: boolean; keepBranch?: boolean; recoveryId?: number; }): Promise; /** * Close an agent: kill its pane, apply the worktree close rule, remove its * DB row. Idempotent: an unknown agent is a no-op, a gone pane is fine. * * The worktree rule (src/worktree.ts `releaseWorktree`): dirty refuses * unless `discard`; clean and merged is removed; clean with unmerged * commits keeps a branch `mu//-` and is removed. The * refusal happens BEFORE the pane is killed. With no agent row but a * registered worktree (an earlier close or rollback left it) the same rule * runs on the leftover, so `agent close ` is the one way out. * * Fenced: see the header. Throws `AgentLockBusyError` (nothing done), * `RevisionMismatchError` (nothing done), `AgentReplacedError` (the * replacement is untouched). */ declare function closeAgent(db: Db, name: string, opts: CloseAgentOptions & { workstream: string; }): Promise; interface SettledCloseInput { /** The agent's workstream, and the workstream of the task it may close. */ workstream: string; agent: string; agentId: number; incarnation: string; /** The settled run's count from ctl (not a wait baseline). */ runs: number; lastText: string; /** Ctl `lastTextTruncated`: the head was cut, so the block cannot be trusted. */ lastTextTruncated?: boolean; /** ctl `ended === "settled"`; a blocked or unknown observation is false. */ settled: boolean; lastError?: string; } /** Who an observation is about. */ type ObservedAgent = Pick; interface AgentObservation { workstream: string; name: string; /** The registry row id (`AgentRow.incarnation`): a number, not pi's string. */ agentId: number; paneId: string; /** Runtime state; null = the pane is gone (dead). */ status: RuntimeState | null; /** Where the reading came from. */ source: StateSource; /** When the state began (ms epoch), when the source says. */ since: number | null; /** Why the state is unknown or blocked, when the source said. */ reason?: string; /** How the control socket answered; absent for a non-pi agent. */ ctl?: Exclude; /** A remote command gate that ended: the command's own numeric exit status (`status` is then null). */ exit?: number; /** pi's process incarnation (ctl only; absent from older extensions). */ incarnation?: string; /** pi's settled-run count: of the SETTLED run when `settled`, else the current count. */ runs?: number; /** ctl only: pi holds queued messages. */ pending?: boolean; /** True only for a ctl `wait` reply that ended `settled`: the run is over and its facts below are its own. */ settled: boolean; /** The extension dialog pi is waiting on (status needs_permission). */ blocked?: CtlBlocked; /** The settled run's final assistant text (tail-capped by the extension). */ lastText?: string; /** True when `lastText` lost its head to the cap: a CLOSE block in it cannot be trusted. */ lastTextTruncated?: boolean; /** The error that ended the settled run. */ lastError?: string; /** The pi session file holding the run's full transcript. */ sessionFile?: string; } /** A status read as an observation. `reading` undefined = the reader had no row. */ declare function observationFromReading(agent: ObservedAgent, reading: StateReading | undefined): AgentObservation; /** One `readAgentStates` pass over `agents`, keyed `/`; a gate is read from its pane and result record instead. */ declare function observeAgentStates(db: Db, agents: readonly AgentRow[]): Promise>; /** The facts of an observation that decide a settled close. */ type SettleFacts = Pick; /** * `applySettledClose`'s input for an observation, or undefined when it is * not one: it must be a ctl-settled run (`settled`), alive, with pi's * incarnation and the settled run count. Idle, blocked, unknown and * older-extension replies never qualify: a settle is never inferred. */ declare function settledCloseInput(obs: SettleFacts): SettledCloseInput | undefined; interface ObservedWatch { /** The agent's state when the watch started. */ initial: AgentObservation; /** The run count the watch waits past (ctl `afterRuns`). */ afterRuns: number; /** Resolves when the run ends. A REJECTION (AgentRestartedError) is fatal: * the incarnation the run belonged to is gone. */ settled: Promise; } /** * Watch a pi agent's current or next run end over its control socket: one * ctl `wait` that the extension answers on `agent_settled`. `afterRuns` * (else a status read taken BEFORE the wait: a followUp queued while busy * runs in the same pi run) is the baseline; `inc` pins the pi process. * * undefined when the agent has no control socket or it does not answer * (the caller polls it instead). `signal` aborts the wait (`settled` then * resolves unknown, never dead and never settled). */ declare function watchAgentRun(agent: ObservedAgent, sock: string, signal: AbortSignal, opts: { afterRuns?: number; inc?: string; }): Promise; /** The verdict labels a check may answer. Anything else is UNVERIFIED. */ declare const VERDICT_LABELS: readonly ["CONFIRMED", "REFUTED", "UNVERIFIED"]; type VerdictLabel = (typeof VERDICT_LABELS)[number]; /** The label on the first `VERDICT:` line (the first known label after the * colon), or undefined when there is no verdict line or no known label. */ declare function parseVerdict(answer: string): VerdictLabel | undefined; type DelegateState = (typeof DELEGATE_CALL_STATES)[number]; /** What a call needs to start again from nothing: the prompt and the spawn options. */ interface DelegateRequest { /** The whole prompt sent to the delegate (standing brief and task); also the task's brief note. */ text: string; label?: string; cli?: string; model?: string; cwd?: string; workspace?: boolean; timeoutS?: number; keep?: boolean; } interface RecordRef { workstream: string; task: string; } interface DelegateRow { callId: string; /** The delegate task's local id, in `workstream`. */ task: string; workstream: string; caller: string; state: DelegateState; wave: string | null; record: (RecordRef & { label: string | null; }) | null; request: DelegateRequest; /** The agent that ran it, once running. */ agent: string | null; /** The parsed verdict label of a done answer; null otherwise. */ verdict: VerdictLabel | null; error: string | null; updatedAt: string; } declare class DelegateCallConflictError extends Error { readonly callId: string; readonly field: string; readonly name = "DelegateCallConflictError"; constructor(callId: string, field: string); } declare class DelegateStateError extends Error { readonly callId: string; readonly from: DelegateState; readonly to: string; readonly name = "DelegateStateError"; constructor(callId: string, from: DelegateState, to: string); } declare class DelegateNotFoundError extends Error { readonly callId: string; readonly name = "DelegateNotFoundError"; constructor(callId: string); } declare function getDelegateCall(db: Db, callId: string): DelegateRow | undefined; /** Calls of `caller` (all callers when undefined), optionally in one workstream and wave, oldest first. */ declare function listDelegates(db: Db, caller?: string, workstream?: string, opts?: { wave?: string; states?: readonly DelegateState[]; /** Only calls whose task has one of these statuses. */ taskStatuses?: readonly string[]; /** Only calls updated after this ISO time. */ updatedAfter?: string; }): DelegateRow[]; /** Calls of any of `callers` (the union, each call once, oldest first), optionally in one workstream and wave. `[]` is no calls. */ declare function listDelegatesOf(db: Db, callers: readonly string[], workstream?: string, opts?: { wave?: string; }): DelegateRow[]; interface RegisterDelegateOptions { callId: string; caller: string; /** Where the task (and the delegate agent) lives. The extension's default is `scratch`. */ workstream: string; request: DelegateRequest; wave?: string; /** The task the answer's verdict is written onto; must exist. */ record?: RecordRef; /** Admission: `starting` (a slot is free), `queued` (waits for one), `refused` (past the queue). */ state?: "queued" | "starting" | "refused"; } /** * Create the call: a `kind=delegate` task, its brief note and the call row, * in ONE transaction. A queued or refused task is parked (outside * `next`/`ready`); a starting one is todo. The same call id with the same * request returns the existing call; a different one throws * {@link DelegateCallConflictError} and writes nothing. */ declare function registerDelegateCall(db: Db, opts: RegisterDelegateOptions): { call: DelegateRow; created: boolean; }; /** * One admission step, as a compare-and-set on the current state: two runtimes * cannot both start a call. `starting` unparks the task; `refused` parks it. * `queued` from `refused` is the reissue: nothing drains a durable queued row * but the runtime that holds it, so only that runtime may make one. * `done`, `failed`, `cancelled` and `recording` belong to {@link finishDelegate}. */ declare function markDelegate(db: Db, callId: string, to: "queued" | "starting" | "running" | "refused", extra?: { agent?: string; error?: string; }): DelegateRow; interface FinishDelegateOptions { /** `done`, or why there is no answer: `timeout`, `died`, `error`, `empty`, `blocked`, `cancelled`, ... */ outcome: string; /** The answer (done) or what is known of the run (the partial text, a pane tail). */ text?: string; elapsedMs?: number; /** Who writes: the delegate's name, or the caller for a cancel. */ author: string; /** False: leave the judged task alone (a spawn that never ran has no verdict to give). */ record?: boolean; } interface FinishDelegateResult { /** False for a duplicate callback: nothing was written. */ changed: boolean; state: DelegateState; task: string; record: RecordRef | null; /** Whether the verdict note landed on the judged task. */ recorded: boolean; verdict: VerdictLabel | null; error?: string; } /** * The recorder. 1) `recording` is committed. 2) ONE op group: the verdict note * on the judged task (when there is a record target), then the delegate's * report and close, then the final state. A throw in 2 rolls the group back; * the call then becomes `failed` with the answer kept as its report: it is * never `done` without its verdict landed. A call already terminal is a no-op, * so a duplicate callback writes one verdict, one report and one close. */ declare function finishDelegate(db: Db, callId: string, opts: FinishDelegateOptions): FinishDelegateResult; /** The tally of one wave: see {@link verdictTally}. */ interface VerdictTally { findings: { id: string; confirmed: number; refuted: number; unverified: number; pending: number; }[]; queued: number; running: number; /** Call ids refused past the cap, still to reissue. */ refused: string[]; } /** * One wave's results, grouped by the judged task (`/`, or the * delegate's own task for a call with no record target). One result per call: * a done answer counts its parsed label (none: UNVERIFIED); failed and * cancelled calls are UNVERIFIED; queued, starting, running, recording and * refused are pending. Reads only the call rows, never notes. `callers` * restricts the wave to those callers' calls (a SET: managed ws/name and * pi:session; `[]` is no calls): two callers may use the same wave name in one * workstream. `undefined` reads every caller (a human's view). */ declare function verdictTally(db: Db, /** The delegates' workstream; undefined = the wave in every workstream. */ workstream: string | undefined, wave: string, /** Only these callers' calls (`mu state` never mixes in another session's identically named wave); `[]` is none. */ callers?: readonly string[]): VerdictTally; /** A blank reason or caller: nothing is recorded (exit 2). */ declare class HumanNeededInvalidError extends Error { readonly name = "HumanNeededInvalidError"; } interface HumanNeededOptions { workstream: string; /** Who is stopping: `spawnerFromEnv()` (managed `/` or `pi:`). */ caller: string; reason: string; } interface HumanNeeded { workstream: string; id: string; reason: string; } /** * Record (or extend) the caller's open human need in a workstream, in ONE * transaction. Same caller + workstream with an open need: the same reason * is a no-op (`changed: false`), a different one appends a brief to it. */ declare function recordHumanNeeded(db: Db, opts: HumanNeededOptions): { id: string; changed: boolean; }; /** The caller's OPEN human needs, oldest first, across workstreams. Another * caller's need never appears. Reasons of an extended need join with "; ". */ declare function openHumanNeeds(db: Db, caller: string): HumanNeeded[]; type TaskStatus = "OPEN" | "IN_PROGRESS" | "CLOSED"; /** Every legal task status, in canonical order (matches the seeded * task_substates rows). Exported so CLI surfaces (`--status` validators, * --help text, error messages) name them all in one place; missing * one used to silently lie about the supported set. */ declare const TASK_STATUSES: readonly TaskStatus[]; declare function isTaskStatus(s: string): s is TaskStatus; /** Pipe-separated list of every legal status, e.g. * 'OPEN | IN_PROGRESS | CLOSED'. Single source of truth for * --help text and error messages so adding a new status doesn't * leave stale lists rotting in the CLI surface. */ declare const TASK_STATUS_LIST: string; /** Legal substates per status. `substate` qualifies `status` and never * touches edge semantics: only `status === "CLOSED"` satisfies a * `blocks` edge. Open: `parked` = set aside on purpose; `triage` = a * proposed task (e.g. a review finding) not yet accepted as work. Both * stay out of `ready` and `claim`. Closed: `rejected` = the proposal * was declined; `wontfix` = valid, but not worth doing. */ declare const TASK_SUBSTATES: { readonly OPEN: readonly ["todo", "parked", "triage"]; readonly IN_PROGRESS: readonly ["active"]; readonly CLOSED: readonly ["done", "rejected", "wontfix", "duplicate", "superseded"]; }; type TaskSubstate = (typeof TASK_SUBSTATES)[TaskStatus][number]; /** The substate a status takes when none is given. Never null: an * absent value must not carry meaning. */ declare const DEFAULT_SUBSTATE: { readonly [S in TaskStatus]: (typeof TASK_SUBSTATES)[S][number]; }; interface TaskPair { status: TaskStatus; substate: TaskSubstate; } /** Rows for seeding task_substates: [status, substate, isDefault]. */ declare const TASK_SUBSTATE_ROWS: ReadonlyArray; declare function isValidPair(status: string, substate: string): boolean; /** REJECTED -> {CLOSED, rejected}; DEFERRED -> {OPEN, parked}; else null. */ declare function mapLegacyStatus(value: string): TaskPair | null; /** Map a stored/remote pair onto a legal one. Legacy status -> mapped pair * (ignores substate arg). Unknown status -> null. Known status with * unknown/missing/mismatched substate -> {status, DEFAULT_SUBSTATE[status]}. */ declare function resolvePair(status: string, substate: unknown): TaskPair | null; /** "OPEN" for default pairs, "OPEN/parked" otherwise. Shared by CLI + TUI. */ declare function formatPair(pair: TaskPair): string; interface CursorToken { incarnation: string; runs: number; } type CursorKind = "task" | "agent" | "delegate"; declare function cursorKey(kind: CursorKind, workstream: string, id: string): string; /** The closed-task token: `noteId` is the task's newest note (0 for none). */ declare const closedToken: (noteId: number) => CursorToken; declare const DELEGATE_TOKEN: CursorToken; /** Has this caller already consumed `token`'s event (or a later one)? */ declare function isConsumed(cursor: CursorToken | undefined, token: CursorToken): boolean; /** Every consumed token of `caller`, by key. Rows with no token are skipped. */ declare function readCursors(db: Db, caller: string): Map; /** * Record consumed tokens, monotonically: within one incarnation `runs` * never goes down, and a different incarnation replaces the row. One short * write transaction; callable inside a larger one. */ declare function recordCursors(db: Db, caller: string, entries: readonly { key: string; token: CursorToken; }[]): void; declare const WAIT_REF_KINDS: readonly ["task", "agent", "delegate"]; type WaitRefKind = (typeof WAIT_REF_KINDS)[number]; /** One owed ref: a task or agent of a workstream, or a delegate call id. */ interface RegisteredRef { kind: WaitRefKind; workstream: string; id: string; } /** * The UNCONSUMED run baseline of an agent ref: what a producer (`send`, * `spawn`, `claim`) saw BEFORE its effect: the ctl run count, pi's process * incarnation and the registry id. A wait counts the agent's runs from here, so * a send that settles before any wait starts is still caught. Distinct from a * consumed cursor (`wait_cursors`), which says what a wait already delivered. */ interface RefBaseline { afterRuns: number; /** * The LATEST fence armed for this process, when later than `afterRuns` (the earliest unconsumed * one): a send armed after the first still owes the run that follows it, so a wait that delivers * a run at or below `latest` leaves the ref owed (`endWait`). */ latest?: number; incarnation?: string; agentId?: number; } /** A ref to register, with its baseline when it is an agent. */ type RegisterRef = RegisteredRef & { baseline?: RefBaseline; }; declare const WAIT_REGISTRATION_STATES: readonly ["armed", "waiting"]; type WaitRegistrationState = (typeof WAIT_REGISTRATION_STATES)[number]; interface WaitRegistration { caller: string; /** Canonical order: kind, workstream, id. */ refs: RegisteredRef[]; state: WaitRegistrationState; /** Bumped by every `beginWait`; never decreases. */ generation: number; /** Unconsumed baselines of agent refs, by `cursorKey("agent", ws, name)`. */ baselines: Record; updatedAt: string; } /** The most refs one caller may owe at once. */ declare const WAIT_REGISTRATION_MAX_REFS = 256; /** A caller, ref or registration mu refuses or cannot read. */ declare class WaitRegistrationError extends Error { readonly name = "WaitRegistrationError"; } /** The caller's registration, or undefined when it never registered anything. */ declare function readWaitRegistration(db: Db, caller: string): WaitRegistration | undefined; /** * Add `refs` to what `caller` owes a wait. Merges with the refs already * owed (deduped, canonical order) and keeps `state` and `generation`, so * registering while a wait is in flight neither disturbs it nor is lost * when it ends. Every ref must exist now (checked in the same transaction * that writes); a ref whose entity is deleted LATER stays owed. Refuses the * whole call (nothing stored) on a bad caller, a bad or missing ref, an * empty list, or more than * WAIT_REGISTRATION_MAX_REFS refs owed in total. */ declare function registerOwnedWait(db: Db, caller: string, refs: readonly RegisterRef[]): WaitRegistration; /** * Start a wait for `caller`: bump the generation, mark the row `waiting`, * return the generation and the refs owed now. undefined (and nothing * written) when the caller owes nothing. An earlier wait still marked * `waiting` is superseded: its `endWait` will no longer match. */ declare function beginWait(db: Db, caller: string): { generation: number; refs: RegisteredRef[]; baselines: Record; } | undefined; /** The run an agent ref was delivered at, when it was a settled run (`endWait`). */ interface DeliveredRun { runs?: number; incarnation?: string; } /** * Finish the wait that `beginWait` numbered `generation`: drop the * `delivered` refs from what is owed and re-arm the row. A compare-and-set: * `applied: false` (and nothing written) when the row is gone, a newer wait * started, or this generation already ended. Refs registered since * `beginWait`, and refs not delivered, stay owed. The row is kept even when * nothing is left, so generations never restart. */ declare function endWait(db: Db, caller: string, generation: number, delivered: readonly (RegisteredRef & DeliveredRun)[]): { applied: boolean; }; /** The heartbeat cap: no wait outlives it; a requested 0 means this. */ declare const WAIT_HEARTBEAT_CAP_MS = 1500000; /** Default `stuckAfterMs`: how long an IN_PROGRESS task's owner may sit needs_input before the wait reports a stall. */ declare const DEFAULT_STUCK_AFTER_MS = 120000; /** The same for an owner read over a working control socket: ctl's idle is exact, so only an `orchestrator send` racing the settle needs the grace. */ declare const DEFAULT_CTL_STUCK_AFTER_MS = 5000; interface WaitRef { kind: WaitRefKind; workstream: string; /** Task or agent local id; a delegate's call id. */ id: string; /** Agent: ctl settled-run baseline (a `send` reply's `runs`). */ afterRuns?: number; /** Agent: pin pi's process incarnation; another one is AgentRestartedError. */ incarnation?: string; } type WaitEventKind = "closed" | "settled" | "input" | "dead" | "stall" | "delegate" | "pane-exit" /** A remote command gate's command ended with a numeric status (`WaitResult.exit`, `snapshot.exit`). */ | "exited" /** An agent with no control socket: its pane tail matched `until.text`. Never a settled run. */ | "pane-text" | "heartbeat" | "interrupted"; interface WaitEvent { kind: WaitEventKind; ref: WaitRef | null; incarnation?: string; runs?: number; } interface CloseOutcome { applied: boolean; task: string | null; reason: string; } /** The facts of one ref at the moment the wait returned. */ interface WaitSnapshot { ref: WaitRef; /** Task: its status. Agent: runtime state (null = pane gone). Delegate: the call's state. */ status: string | null; substate?: string; owner?: string | null; ownerWorkstream?: string; incarnation?: string; runs?: number; lastText?: string; lastError?: string; sessionFile?: string; blocked?: CtlBlocked; unknownReason?: string; /** Agent: the registry row id (a number; never pi's incarnation string). */ agentId?: number; paneId?: string; /** Agent: true only for a ctl run that ended `settled`. */ settled?: boolean; /** Agent: `lastText` lost its head to the cap, so a CLOSE block in it cannot be trusted. */ lastTextTruncated?: boolean; /** Agent: `pane` when `lastText` is a non-pi pane's tail (no ctl text exists for it). */ textSource?: "pane"; /** Agent: the ctl run count this wait counted from. */ afterRuns?: number; /** Agent: observed busy during this wait. */ wasBusy?: boolean; /** Agent: owed with no fence and no cursor, so counted from the run count probed now: an earlier run is NOT reported as completion. */ unknownBaseline?: boolean; /** Delegate: the parsed verdict label of a done answer. */ verdict?: string | null; /** Delegate: its own task's local id (the answer is that task's report note), the `/` it judges, its wave. */ callTask?: string; record?: string; wave?: string; /** Task: its owner has been stalled long enough to report (IN_PROGRESS, owner needs_input or ctl broken). */ stuck?: boolean; /** Task, stuck: why (`needs_input`, or the broken control socket's reason) and how long, in seconds. */ stuckWhy?: string; stuckSecs?: number; /** Task, stuck: the owner's ctl settled-run count, when its socket answered (pi owners). */ ownerRuns?: number; /** Agent (a remote command gate): the command's own numeric exit status, authoritative. */ exit?: number; /** The ref's completion condition is met (task at target, agent settled, delegate terminal). */ fired: boolean; dead: boolean; /** What applying the settled run's CLOSE block did (agent refs; absent when none was tried). On a stuck task: the owner's refused CLOSE. */ close?: CloseOutcome; } /** What a handled stall did; `disposition` says whether the wait goes on. */ interface StallActionResult { /** `continue`: keep waiting (the ref is not consumed). `return`: end the wait with the stall. */ disposition: "continue" | "return"; action: "steer" | "release"; state: "sent" | "released" | "refused" | "unknown" | "budget-exhausted"; msgId?: string; incarnation?: string; /** The ctl run baseline after a send: the wait counts the owner's next run from here. */ afterRuns?: number; reason?: string; } /** What the engine knows when it hands a stall to `handleStall`. The hook owns policy and effects. */ interface WaitStallContext { /** The caller that owns the stalled ref (the first of the set when none does). */ caller: string; /** That caller's registration generation; null for an unregistered explicit wait (not a bypass of ownership). */ generation: number | null; /** The stalled TASK (its owner is `owner`). */ ref: WaitRef; /** `tasks.id` of the stalled task. */ taskId: number; /** The task's revision when the stall was seen. */ revision: number; /** The task's owner: registry id, pane and the authoritative ctl facts. */ owner: AgentObservation; /** Stable for one observation of one stall: task, owner id, ctl incarnation, runs, since. */ stallToken: string; /** Milliseconds left to the wait's deadline when the hook was called. */ deadlineMs: number; /** Aborts when the wait aborts or returns: a not-yet-written action must not be written. */ signal: AbortSignal; } interface WaitResult { /** The event that ended the wait; `heartbeat` on a timeout, `interrupted` on an abort. */ event: WaitEvent; /** Targets in input order, then the delivered refs that are not targets. */ refs: WaitSnapshot[]; timedOut: boolean; interrupted: boolean; /** 0 fired or timed out, 6 dead, 7 input or stall, 130 interrupted; an `exited` gate's own status (23 stays 23), the first nonzero of the gates that ended. */ exit: number; /** * The refs this wait's event actually handed over as COMPLETED (closed, settled, delegate, ...): * what a resume may drop. Never an input or stall (still owed its completion) and never a target * an early urgent return did not name. */ delivered: WaitRef[]; /** The callers' consumed tokens after this wait (empty without callers). */ cursor: Record; /** Open human needs of the callers (never another caller's). */ humanNeeds: HumanNeeded[]; /** The wave's tally over the callers' calls, when `wave` was given. */ tally?: VerdictTally; /** What `handleStall` did for a returned stall. */ action?: StallActionResult; /** True when a newer wait of the same caller superseded this one: nothing was consumed. */ superseded?: boolean; /** Everything watched is done and was already delivered: nothing is owed. */ drained?: boolean; /** The reaper-flip anchor this wait used: a resume of it keeps it. */ sinceSeq?: number; } /** What an agent read or watch reports; an AgentObservation satisfies it. */ interface AgentReading { status: RuntimeState | null; /** A remote command gate that ended: its numeric exit status (status is then null; `exit` decides, not "dead"). */ exit?: number; settled?: boolean; reason?: string; incarnation?: string; runs?: number; pending?: boolean; blocked?: CtlBlocked; lastText?: string; lastTextTruncated?: boolean; /** `pane`: `lastText` is the tail of a NON-pi pane (it has no control socket, so no ctl text): matched by `until.text` only, never a CLOSE source. */ textSource?: "pane"; lastError?: string; sessionFile?: string; agentId?: number; paneId?: string; /** When the state began (ms epoch); stall detection reads it. */ since?: number | null; source?: StateReading["source"]; /** How the control socket answered (pi agents). */ ctl?: StateReading["ctl"]; } /** The unconsumed run baseline a producer registered for an agent ref. */ interface AgentBaseline { afterRuns: number; incarnation?: string; agentId?: number; } interface AgentWatchHandle { initial: AgentReading; afterRuns?: number; /** Set when an owed ref had no fence and no cursor: the baseline the watch probed and counted from. */ unknownBaseline?: AgentBaseline; /** Resolves when the run ends. A REJECTION is fatal (the agent restarted). */ settled: Promise; } interface WaitOptions { /** * Who owns this wait: a SET, because one orchestrator is known under two * names: the managed `/` (spawned workers, human needs) and * `pi:` (delegate calls). Owned refs, delegates, human needs and * the wave tally are the union over the set. `[]` is an explicit-refs wait: * no registration, no cursors, no owned refs, never an all-caller fallback. * See `currentCallers`. */ callers: readonly string[]; /** `first` (default): the first event ends the wait. `all`: every target must be done. */ mode?: "first" | "all"; /** `owned` (default): the callers' owned refs also end the wait. `refs`: only the listed refs do. */ eventScope?: "owned" | "refs"; /** Delegate calls of this wave (the callers' only, never narrowed by `scope`) are targets; the result carries their tally. A name none of their calls carry throws WaveNotFoundError. */ wave?: { workstream?: string; name: string; }; /** The open transitive blockers of this task are targets, re-read each iteration. */ blockersOf?: { workstream: string; id: string; }; /** Task target status (default CLOSED). */ status?: TaskStatus; /** `idle` (default): a run ending. `text`: a bounded regex over a settled run's lastText or a done task's report. */ until?: "idle" | { text: string; }; /** Default and cap WAIT_HEARTBEAT_CAP_MS; 0 means the cap. */ timeoutMs?: number; /** Ops seq a reaper flip must come after to be a death this wait reports (default: the ops head at start; a resume passes the stored one). */ sinceSeq?: number; pollMs?: number; signal?: AbortSignal; /** * How long an IN_PROGRESS work task's owner may sit needs_input (or with a * dead control socket) before the wait reports a `stall`. Default 120 s, * 5 s for an owner read over a working ctl socket; an explicit value * applies to both; 0 disables detection. */ stuckAfterMs?: number; /** * `exit` (default): a stall ends the wait. `warn`: the stall is flagged * (`stuck` on the snapshot, an `agent.stall` event) and the wait goes on. * `steer`/`release`: call `handleStall` (no handler: same as exit). */ onStall?: "exit" | "warn" | "steer" | "release"; /** * Narrow what the callers OWN to one workstream: registered and discovered * refs by their workstream, delegate calls by their own or their record's. * Explicit refs, a wave and `blockersOf` are never narrowed. */ scope?: string; /** Raced with the loop, never awaited serially; called once per `stallToken`. */ handleStall?: (ctx: WaitStallContext) => Promise; /** Called before every iteration; a throw propagates. */ beforePoll?: () => Promise; /** One batched, non-blocking read per iteration, keyed `/`. */ readAgents?: (refs: readonly WaitRef[], signal: AbortSignal) => Promise>; /** * The blocking read of a future settle (the ctl `wait`): raced, never * awaited serially. undefined = poll the agent instead. `ctx.owed` marks a * ref the callers have registered (explicit or not): its unconsumed runs * still count, from the fence, else the cursor, else the probed run count. * `ctx.baseline` is the producer's registered unconsumed baseline (already * checked against the registry id; the hook checks the incarnation) and * `ctx.cursor` the consumed token. */ watchAgent?: (ref: WaitRef, signal: AbortSignal, ctx: { owed: boolean; cursor?: CursorToken; baseline?: AgentBaseline; }) => Promise; /** Apply settled CLOSE blocks (default true). Needs ctl facts: a polled reading never closes. */ applyClose?: boolean; /** Ingest peers every iteration and flush before returning (default true). */ sync?: boolean; /** Called every iteration with the snapshots of everything the wait is watching (the CLI's heartbeat and stall warning). */ onPicture?: (snaps: readonly WaitSnapshot[]) => void; /** Called once when every watched agent target reads unknown on the first iteration. */ onInitialUnknown?: (agents: readonly WaitSnapshot[]) => void; /** Poll sleep seam. Default: an abortable timer. */ sleep?: (ms: number) => Promise; /** `mu agent wait`: timeoutMs 0 means forever and there is no heartbeat cap. */ uncapped?: boolean; } /** A wait with nothing to wait for. */ declare class WaitNothingError extends Error { readonly name = "WaitNothingError"; constructor(); } /** `--wave ` that none of the callers' delegate calls carry (a typo, or another session's wave). */ declare class WaveNotFoundError extends Error { readonly wave: string; readonly name = "WaveNotFoundError"; constructor(wave: string, yours: readonly (string | null)[]); } /** A ref the caller may not wait on (another caller's delegate call). */ declare class WaitRefError extends Error { readonly name = "WaitRefError"; } /** Wait for the next owned event (or all targets). See the file comment. */ declare function waitForEvents(db: Db, input: readonly WaitRef[], opts: WaitOptions): Promise; /** * Pre-flight failure: the command mu would have spawned in the new * pane doesn't resolve to a binary on PATH (and isn't an absolute / * relative path that exists + is executable). Thrown by `spawnAgent` * BEFORE the worktree is created so a typo in `--cli` never leaves * an orphan worktree behind. * * Live * dogfood report: `mu agent spawn worker-1 --cli pi-meta` on a host * where the `pi-meta` binary wasn't on PATH printed `Spawned worker-1 * (pi-meta)` and the pane immediately died with `command not found`; * the existing 1.5s liveness check sometimes missed it (the shell * stays alive after the failed exec). Pre-flighting the PATH lookup * surfaces the typo before any side effects (worktree, pane, DB row). * * Distinct from `AgentSpawnStartupError` (pane alive but parked at an * error prompt) and `AgentDiedOnSpawnError` (pane vanished within the * liveness window). All three carry different remediation hints, so * they're separate types. */ declare class AgentSpawnCliNotFoundError extends Error { readonly cli: string; /** First whitespace-separated token of the resolved command — the * thing actually missing on PATH. Surfaced verbatim in the * message so the operator sees what mu searched for (which may * differ from `cli` when `$MU__COMMAND` rewrites it). */ readonly binary: string; /** Name of the env var that mu consulted before falling back to * the bare `cli` value (e.g. `MU_PI_META_COMMAND`). Always set * to the conventional name so the message can name it. */ readonly envVarChecked: string; readonly name = "AgentSpawnCliNotFoundError"; constructor(cli: string, /** First whitespace-separated token of the resolved command — the * thing actually missing on PATH. Surfaced verbatim in the * message so the operator sees what mu searched for (which may * differ from `cli` when `$MU__COMMAND` rewrites it). */ binary: string, /** Name of the env var that mu consulted before falling back to * the bare `cli` value (e.g. `MU_PI_META_COMMAND`). Always set * to the conventional name so the message can name it. */ envVarChecked: string); } declare class AgentExistsError extends Error { readonly agentName: string; readonly name = "AgentExistsError"; constructor(agentName: string); } /** `spawn --next-free` found no free valid name near the asked one. */ declare class AgentNameExhaustedError extends Error { readonly agentName: string; readonly name = "AgentNameExhaustedError"; constructor(agentName: string, reason: string); } declare class AgentNotFoundError extends Error { readonly agentName: string; /** Optional workstream context. When set, the message is enriched * with `(in workstream )` so the verb that hit the miss * (e.g. `mu agent close -w `) doesn't leave the * operator guessing which scope was searched. Optional so existing * call sites that only know the agent name keep their original * one-line message. */ readonly workstream?: string | undefined; readonly name = "AgentNotFoundError"; constructor(agentName: string, /** Optional workstream context. When set, the message is enriched * with `(in workstream )` so the verb that hit the miss * (e.g. `mu agent close -w `) doesn't leave the * operator guessing which scope was searched. Optional so existing * call sites that only know the agent name keep their original * one-line message. */ workstream?: string | undefined); } /** * Thrown when an entity-targeted verb is invoked with `-w/--workstream * ` but the named agent lives in a different workstream. * Mirrors `TaskNotInWorkstreamError`. Maps to exit code 4 (conflict / * wrong scope). Distinguishes "the user typo'd the workstream" from * "the agent doesn't exist anywhere" (which surfaces as * `AgentNotFoundError`). */ declare class AgentNotInWorkstreamError extends Error { readonly agentName: string; readonly expectedWorkstream: string; readonly actualWorkstream: string; readonly name = "AgentNotInWorkstreamError"; constructor(agentName: string, expectedWorkstream: string, actualWorkstream: string); } /** * Thrown when an agent's pane is created and titled successfully but the * spawned process exits within the liveness window (default 1500ms; * configurable via `MU_SPAWN_LIVENESS_MS`). The most common cause is the * underlying CLI failing fast: a wrapper CLI blocking on a single-instance * lock, `claude` rejecting an invalid API key, etc. The agent's last * scrollback (when capturable) is attached to help diagnose. */ declare class AgentDiedOnSpawnError extends Error { readonly agentName: string; readonly paneId: string; readonly scrollback: string | undefined; /** The command the pane ran; an `ssh ...` command gets remote hints. */ readonly command?: string | undefined; readonly name = "AgentDiedOnSpawnError"; /** The pane's transport reaches another machine (a host profile, or a hand-written ssh hop): remote causes, not wrapper locks. */ readonly remote: boolean; /** The host profile the pane ran, when it was a `--host` spawn. */ readonly host: string | undefined; constructor(agentName: string, paneId: string, scrollback: string | undefined, /** The command the pane ran; an `ssh ...` command gets remote hints. */ command?: string | undefined, /** When it died; default `within ms of spawn`. */ when?: string, host?: string); } /** * Thrown when an agent's pane is alive AND staying alive after the * liveness window, but its first burst of output matches a known * provider-startup-failure pattern (missing API key, auth rejected, …). * Source: feedback ws task `agent_spawn_model_auth_failure_counts_as_live`. * Live dogfood report: `pi-meta --no-solo --model sonnet:high` printed * `Error: No API key found for amazon-bedrock` and parked at a prompt. * The pane stayed alive (1.5s liveness check passed) but the worker * could never do work — the orchestrator only discovered this when * `mu task wait` stalled minutes later. * * Distinct from `AgentDiedOnSpawnError`: * - `AgentDiedOnSpawnError` → pane vanished within the liveness window * (CLI exited fast). * - `AgentSpawnStartupError` → pane alive, but the captured scrollback * tail contains a curated provider-auth-failure pattern. * The two carry different remediation hints (CLI override vs. fix the * env var), so they're separate types instead of one with a flag. * * The pattern list is curated and short to keep false-positive risk low * — the scan only looks at the last ~30 lines of the 50-line capture * taken right after the liveness sleep, so matches naturally come from * the CLI's first ~1.5s of output (not arbitrary later prompts the * agent might type into). */ declare class AgentSpawnStartupError extends Error { readonly agentName: string; readonly paneId: string; /** The single scrollback line that matched a known startup-error * pattern. Surfaced verbatim in the message so the operator sees * what mu saw. */ readonly matchedLine: string; /** Full captured scrollback (tail-trimmed already by * awaitSpawnLiveness). Attached to the message for context. */ readonly scrollback: string; readonly name = "AgentSpawnStartupError"; constructor(agentName: string, paneId: string, /** The single scrollback line that matched a known startup-error * pattern. Surfaced verbatim in the message so the operator sees * what mu saw. */ matchedLine: string, /** Full captured scrollback (tail-trimmed already by * awaitSpawnLiveness). Attached to the message for context. */ scrollback: string, /** When it was seen; default `within ms of spawn`. */ when?: string); } /** * The agent's pane and pi are up, but nothing answers on its control * socket (`MU_CTL_SOCK`): the mu pi extension is not loaded, or, for a * remote agent, the ssh command lacks the `-L` forward. Spawn does not * throw it — the agent is usable by hand — but prints it as a warning; * verbs that need the socket throw it. */ declare class AgentCtlUnreachableError extends Error { readonly agentName: string; readonly workstream: string; readonly socket: string; readonly kind: "missing" | "refused"; /** The host profile the agent runs on, when remote: its forward is the usual cause. */ readonly host?: string | null | undefined; readonly name = "AgentCtlUnreachableError"; constructor(agentName: string, workstream: string, socket: string, kind: "missing" | "refused", /** The host profile the agent runs on, when remote: its forward is the usual cause. */ host?: string | null | undefined); } /** * A side-effecting ctl request (send, fresh, interrupt, command) was * written to a live agent's socket, then no reply arrived (timeout or * close). pi may have queued the prompt, so a blind resend can duplicate it. */ declare class AgentSendIndeterminateError extends Error { readonly agentName: string; readonly workstream: string; readonly op: string; readonly reason: string; /** The receipt id the send carried: `mu agent check ` asks whether it landed. */ readonly msgId?: string | undefined; /** The pi process the send was written to (hello's), when the extension reports one. */ readonly incarnation?: string | undefined; /** The registry row the send went to (`AgentRow.incarnation`): its receipt check is offered only while that row stands. */ readonly agentId?: number | undefined; readonly name = "AgentSendIndeterminateError"; constructor(agentName: string, workstream: string, op: string, reason: string, /** The receipt id the send carried: `mu agent check ` asks whether it landed. */ msgId?: string | undefined, /** The pi process the send was written to (hello's), when the extension reports one. */ incarnation?: string | undefined, /** The registry row the send went to (`AgentRow.incarnation`): its receipt check is offered only while that row stands. */ agentId?: number | undefined); } /** * `mu agent wait --inc ` found another pi process behind the socket: * the agent restarted, and the run the caller waits on died with the old * process. Waiting more cannot recover it. */ declare class AgentRestartedError extends Error { readonly agentName: string; readonly workstream: string; readonly expected: string; readonly actual?: string | undefined; readonly name = "AgentRestartedError"; constructor(agentName: string, workstream: string, expected: string, actual?: string | undefined); } /** * `agent close` killed the pane and it is STILL alive (moved out from under * the kill, or the mux refused), or its liveness could not be checked (the * probe failed, or no multiplexer answers: `unknown`, with `reason`). The * row, its claims and its worktree stay: nothing is freed while the pane may * run. Exit 4; the message names the retry (`--force` for `unknown` only). */ declare class AgentPaneStillAliveError extends Error { readonly agentName: string; readonly workstream: string; readonly paneId: string; readonly state: true | "unknown"; readonly reason?: string | undefined; readonly name = "AgentPaneStillAliveError"; constructor(agentName: string, workstream: string, paneId: string, state: true | "unknown", reason?: string | undefined); } /** * The name now belongs to another incarnation (a respawn, or the same pane * id reused) than the one this step meant, so nothing more was done to it. * `agent close`: whatever it already did to the ORIGINAL (a pane kill) * stands. `agent spawn --send` / `--task`: the prompt was NOT written. In * both the replacement's row, socket, worktree and tasks are untouched. * Exit 4. */ declare class AgentReplacedError extends Error { readonly agentName: string; readonly workstream: string; readonly name = "AgentReplacedError"; constructor(agentName: string, workstream: string, during?: "closed" | "sent to" | "collected"); readonly during: "closed" | "sent to" | "collected"; } /** * Another mu process holds this agent's lock (a close, or a `--worktree` * spawn staging the name) and did not release it in time. Nothing was * done: a destructive agent step never runs unlocked. Exit 4. */ declare class AgentLockBusyError extends Error { readonly agentName: string; readonly workstream: string; readonly name = "AgentLockBusyError"; constructor(agentName: string, workstream: string); } /** * A ctl verb (send, abort, ...) found the agent's control socket silent * AND its pane gone: the process exited (or the pane was closed behind * mu's back), so there is nothing to link or forward. Raised * instead of AgentCtlUnreachableError, which is for a live pane. */ declare class AgentPaneDeadError extends Error { readonly agentName: string; readonly workstream: string; readonly paneId: string; /** Set when a send/fresh WAS written before the pane died: it may have landed. */ readonly msgId?: string | undefined; readonly name = "AgentPaneDeadError"; constructor(agentName: string, workstream: string, paneId: string, /** Set when a send/fresh WAS written before the pane died: it may have landed. */ msgId?: string | undefined); } /** * `mu agent abort` on an agent that does not run pi: there is no control * socket to carry the abort, and mu does not silently signal the pane. */ declare class AgentAbortNeedsCtlError extends Error { readonly agentName: string; readonly workstream: string; readonly cli: string; readonly name = "AgentAbortNeedsCtlError"; constructor(agentName: string, workstream: string, cli: string); } /** The abort was delivered but pi did not settle within the timeout. */ declare class AgentAbortTimeoutError extends Error { readonly agentName: string; readonly workstream: string; readonly timeoutMs: number; readonly name = "AgentAbortTimeoutError"; constructor(agentName: string, workstream: string, timeoutMs: number); } /** * `mu agent send --fresh` on an agent without a control socket (non-pi * CLI): there is no one-operation new session. */ declare class AgentFreshNeedsCtlError extends Error { readonly agentName: string; readonly workstream: string; readonly cli: string; readonly name = "AgentFreshNeedsCtlError"; constructor(agentName: string, workstream: string, cli: string); } /** * The agent's pi serves the control socket with an extension it loaded * before mu learned `op` (e.g. `fresh`): running pi processes keep the * extension code they started with. pi's `/reload` re-imports the * extension through the pi package and keeps the socket; a * respawn always works. An extension that serves op `command` takes * `/reload` over ctl; an older one cannot be reloaded by mu: respawn it. */ declare class AgentExtensionOutdatedError extends Error { readonly agentName: string; readonly workstream: string; /** The op it answered "unknown op" to, or the feature (msgId, incarnation) its hello lacks. */ readonly op: string; readonly extVersion?: string | undefined; /** Ops the extension serves (from its "unknown op" reply or hello); absent from old extensions. */ readonly ops?: readonly string[] | undefined; /** The recovery inputs only the caller can compute (the mux and the agent row). */ readonly recovery: { /** A shell line that types /reload into the pane through the mux (herdr). */ muxReload?: string; /** The agent's original spawn command, rebuilt from its row. */ respawn?: string; /** `op` is a feature of the hello reply, not an op. */ feature?: boolean; }; readonly name = "AgentExtensionOutdatedError"; constructor(agentName: string, workstream: string, /** The op it answered "unknown op" to, or the feature (msgId, incarnation) its hello lacks. */ op: string, extVersion?: string | undefined, /** Ops the extension serves (from its "unknown op" reply or hello); absent from old extensions. */ ops?: readonly string[] | undefined, /** The recovery inputs only the caller can compute (the mux and the agent row). */ recovery?: { /** A shell line that types /reload into the pane through the mux (herdr). */ muxReload?: string; /** The agent's original spawn command, rebuilt from its row. */ respawn?: string; /** `op` is a feature of the hello reply, not an op. */ feature?: boolean; }); } /** * `send --fresh` or a session command (`/new`, `/reload`, `/compact`) * refused: pi is mid-turn, and running it would abandon the turn. */ declare class AgentBusyError extends Error { readonly agentName: string; readonly workstream: string; /** The session command refused, e.g. "/new". Absent: `--fresh`. */ readonly command?: string | undefined; readonly name = "AgentBusyError"; constructor(agentName: string, workstream: string, /** The session command refused, e.g. "/new". Absent: `--fresh`. */ command?: string | undefined); } /** * A slash command other than `/new`, `/reload`, `/compact` sent to a pi * agent. The control socket cannot run it (pi.sendUserMessage would * hand it to the model as text), and mu has no paste into a pi pane: * the operator types it in the pane. */ declare class AgentSlashCommandUnsupportedError extends Error { readonly agentName: string; readonly workstream: string; readonly text: string; readonly supported: readonly string[]; readonly name = "AgentSlashCommandUnsupportedError"; constructor(agentName: string, workstream: string, text: string, supported: readonly string[]); } /** * An empty message sent to a pi agent over the control socket. * pi.sendUserMessage("") is meaningless; a bare Enter is a pane * keystroke, which mu does not send. */ declare class AgentEmptySendError extends Error { readonly agentName: string; readonly workstream: string; readonly name = "AgentEmptySendError"; constructor(agentName: string, workstream: string); } /** * `mu agent send` to a non-pi agent on a backend with no `sendToPane` * (tmux): its bracketed paste cannot be confirmed, so mu refuses. Maps to * exit code 4 (conflict). */ declare class AgentSendUnsupportedError extends Error { readonly agentName: string; readonly workstream: string; readonly cli: string; readonly name = "AgentSendUnsupportedError"; constructor(agentName: string, workstream: string, cli: string); } /** * `agent spawn --task`: another mu process is running this same attempt * (task + attempt key). Nothing was done; retry once it finishes and the * replay returns its outcome. Exit 4. */ declare class SpawnAttemptBusyError extends Error { readonly task: string; readonly attemptKey: string; readonly name = "SpawnAttemptBusyError"; constructor(task: string, attemptKey: string); } /** * `mu next`: an executor attempt (`auto:`) cannot be run or resumed: it * failed before anything was sent (its run stays charged), was never * reserved, or its saved identity is gone. It is never reset; the next * attempt is a new key and a new charge. Exit 4. */ declare class SpawnAttemptFailedError extends Error { readonly task: string; readonly attemptKey: string; readonly name = "SpawnAttemptFailedError"; constructor(task: string, attemptKey: string, reason: string); } /** * `mu next`: permission, readiness or the revision changed between the * reservation and an effect (revoked `mu task auto`, another host * designated, the task claimed or closed). Nothing was spawned or sent past * this point; a reservation made earlier stays resumable. Exit 4. */ declare class SpawnAttemptGuardError extends Error { readonly task: string; readonly attemptKey: string; readonly name = "SpawnAttemptGuardError"; constructor(task: string, attemptKey: string, reason: string); } /** * `agent spawn --task`: the task is not claimable by this attempt any more * (closed, or owned by someone else since the claim). Nothing was sent; the * attempt's own pane is rolled back. Exit 4. */ declare class SpawnTaskStateError extends Error { readonly task: string; readonly name = "SpawnTaskStateError"; constructor(task: string, reason: string); } /** * `agent spawn --task` replayed an attempt key with a different request * (name, text or --next-free): a second prompt must be a new attempt. Exit 4. */ declare class SpawnReceiptConflictError extends Error { readonly task: string; readonly attemptKey: string; readonly savedName: string; readonly name = "SpawnReceiptConflictError"; constructor(task: string, attemptKey: string, savedName: string); } /** * `agent spawn --task`: the agent came up but cannot take the prompt (Claude * is parked at its folder-trust dialog). Nothing was sent; the spawn was * rolled back. Exit 1. */ declare class SpawnTrustDialogError extends Error { readonly agentName: string; readonly name = "SpawnTrustDialogError"; constructor(agentName: string); } /** Run `fn` holding the agent's lock; throws `AgentLockBusyError` instead * of running when it cannot be taken. */ declare function withAgentLock(db: Db, workstream: string, name: string, fn: () => Promise, opts?: { acquireTimeoutMs?: number; }): Promise; interface SpawnReceipt { taskId: number; /** `manual:`: the namespace of a manual spawn (the executor's attempts are `task_auto.last_spawn`). */ attemptKey: string; name: string; workstream: string; /** Hash of the request (name, --next-free, text): a replay with another one conflicts. */ requestSha: string; /** The id the send carries on the control socket (`mu agent check`). Never resent. */ msgId: string; agentId: number | null; paneId: string | null; /** How the prompt went: `ctl` (a receipt id was carried) or `mux` (no receipt). */ transport: "ctl" | "mux" | null; /** pi's incarnation, once a reply (or a receipt check) named it. */ incarnation: string | null; runs: number | null; state: SpawnReceiptState; detail: string | null; createdAt: string; updatedAt: string; } /** A profile field mu refuses (name, cap, repo, template). Exit 2. */ declare class HostProfileError extends Error { readonly name: string; } /** A template mu refuses to expand. Exit 2. */ declare class HostTemplateError extends HostProfileError { readonly name: string; } declare class HostNotFoundError extends Error { readonly host: string; readonly name = "HostNotFoundError"; constructor(host: string); } declare class HostExistsError extends Error { readonly host: string; readonly name = "HostExistsError"; constructor(host: string); } /** The host is at `max_sessions`, or the holder already holds a channel. One line, names the holder. */ declare class HostChannelBusyError extends Error { readonly host: string; readonly holders: readonly string[]; readonly name = "HostChannelBusyError"; constructor(host: string, holders: readonly string[], message: string); } /** `host rm` of a host that still holds a channel. */ declare class HostInUseError extends Error { readonly host: string; readonly holders: readonly string[]; readonly name = "HostInUseError"; constructor(host: string, holders: readonly string[]); } /** The active multiplexer cannot run a host profile's shell template. One line, exit 2. */ declare class HostSpawnUnsupportedError extends Error { readonly host: string; readonly mux: string; readonly name = "HostSpawnUnsupportedError"; constructor(host: string, mux: string); } /** `host check` found a failing step. The report rides on `failureWithData`. */ declare class HostCheckFailedError extends Error { readonly host: string; readonly failed: readonly string[]; readonly name = "HostCheckFailedError"; constructor(host: string, failed: readonly string[]); } interface HostProfile { name: string; shell: string; exec: string; fetch: string; /** Default remote repo for a spawn without `--root`; null: none. */ repo: string | null; max_sessions: number | null; } declare function getHost(db: Db, name: string): HostProfile; declare function listHosts(db: Db): HostProfile[]; /** Store a profile. Validates the name, cap, default repo and every template first; stores nothing on refusal. */ declare function addHost(db: Db, p: HostProfile): HostProfile; /** * Remove a profile; false when there was none. Refuses while a channel is * held, after reclaiming proven-dead transient ones. An attached channel is * freed by closing its agent, never here. */ declare function removeHost(db: Db, name: string): boolean; /** * The starter for `host init --ssh`: printed for the user to edit, * never run. The shell template is a command for the remote login shell * (inside quotes, so each value asks for `:sh`, quoting for that shell): cd, env, `$SHELL -ilc`. */ declare function hostStarter(host: string): { shell: string; exec: string; fetch: string; }; declare const HOST_PLACEHOLDERS: readonly ["env", "cmd", "local_sock", "remote_sock", "cwd"]; type HostPlaceholder = (typeof HOST_PLACEHOLDERS)[number]; type HostTemplateValues = Partial>; /** * The only variables a remote worker is given: its identity, never the local * DB, state or sync dir. `MU_REMOTE=1` tells the remote extension there is no * mu DB over there (it skips the close nudge; CLOSE is applied here). */ declare const HOST_ENV_KEYS: readonly ["MU_MANAGED_AGENT", "MU_AGENT_NAME", "MU_WORKSTREAM", "MU_CTL_SOCK", "MU_REMOTE"]; /** `K='v'` words for `{env}`: whitelisted keys, every value shell-quoted (any text survives), nothing else. */ declare function hostEnvWords(env: Record): string; /** Throws HostTemplateError unless `template` is well-formed. */ declare function validateHostTemplate(template: string): void; /** * Expand `template` in one pass. Every value is quoted for the layer it lands * in and stays one word (`{name:sh}` adds the second-shell layer); {env} is * whitelisted assignment words. A placeholder with * no value in this context is an error, never blanked. */ declare function expandHostTemplate(template: string, values: HostTemplateValues): string; interface HostChannel { host: string; holder: string; kind: "transient" | "attached"; token: string; pid: number | null; pid_start: string | null; /** The template's process group (its leader's pid), set before the command starts. */ pgid: number | null; pgid_start: string | null; agent_id: number | null; pane_id: string | null; opened_at: string; } /** Proof of a reservation: release and bind are compare-and-set on `token`. */ interface HostLease { host: string; holder: string; token: string; } interface ChannelOpts { /** Is this pane alive? Throwing or absent means unknown, which keeps the channel. */ paneAlive?: (paneId: string) => Promise; } declare function listHostChannels(db: Db, host?: string): HostChannel[]; /** * Reserve a transient channel for `holder`, or throw HostChannelBusyError * naming who holds the host. One holder never holds two channels, and a * refused call changes nothing (it can never release or steal another's). */ declare function reserveHostChannel(db: Db, host: string, holder: string, opts?: ChannelOpts): Promise; /** * Admission: the profile is read INSIDE the BEGIN IMMEDIATE that counts, so * the cap enforced is the one that exists at admission (a profile removed or * re-added during the awaited probe is seen, or refused as not found, never * an orphan row). The returned profile is that snapshot; run the template * from it, not from a re-read. */ declare function admitHostChannel(db: Db, host: string, holder: string, opts?: ChannelOpts): Promise<{ lease: HostLease; profile: HostProfile; }>; /** Free a channel by token. False when it was already gone or is another's now. */ declare function releaseHostChannel(db: Db, lease: HostLease): boolean; /** Transfer a transient reservation to an agent's pane (registry id + pane). False unless the lease is still live. */ declare function bindHostChannel(db: Db, lease: HostLease, to: { agentId: number; paneId: string; }): boolean; /** Free the attached channels of one agent incarnation: registry id AND pane, never a name. Touches nothing else. */ declare function releaseHostChannelsForAgent(db: Db, agentId: number, paneId: string): number; /** Hold a transient channel for the duration of `run` (given the admission snapshot of the profile and the lease); released in `finally` by token. */ declare function withHostChannel(db: Db, host: string, holder: string, run: (profile: HostProfile, lease: HostLease) => Promise, opts?: ChannelOpts): Promise; interface HostRun { /** null: killed by the timeout or a signal. */ exitCode: number | null; stdout: string; stderr: string; } /** Run the profile's `exec` or `fetch` template under one transient channel. */ declare function runHostTemplate(db: Db, host: string, field: "exec" | "fetch", holder: string, values: HostTemplateValues, opts?: ChannelOpts & { timeoutMs?: number; localCwd?: string; }): Promise; interface HostCheckStep { step: "reachable" | "repo" | "pi" | "extension" | "ctl"; /** null: skipped because the host was not reachable. */ ok: boolean | null; detail: string; } interface HostCheckReport { host: string; ok: boolean; steps: HostCheckStep[]; } interface HostCheckOptions extends ChannelOpts { timeoutMs?: number; /** Also probe this agent's derived local control socket: the forward rides its pane. */ agent?: { name: string; workstream: string; }; /** The remote repo the `repo` step checks (default the profile's); with neither it is skipped. */ root?: string; } /** * Check a host through its own stored templates, under ONE channel: the * host answers `exec`, the repo (`root`, else the profile's default; skipped * with neither) is a git repo, pi runs under the login * shell, `pi list` names mu, and with `agent` the forwarded socket answers * hello. Each failure is reported; none is retried; an unreachable host * skips the rest. The ctl probe only reads an existing forward. */ declare function checkHost(db: Db, host: string, opts?: HostCheckOptions): Promise; /** * Resolve the actual executable to launch in an agent's pane for a given * `cli`. Honours the env var `MU__COMMAND` (e.g. `MU_PI_COMMAND= * pi-alt` makes `--cli pi` actually exec `pi-alt`). Falls back to the cli * name itself, which is what users expect when their `pi` binary is on * `$PATH` under that exact name. * * Used by `spawnAgent` to pick the spawned command, and by reconcile's * orphan detector so externally-spawned panes running the resolved binary * are still recognised as agents. */ declare function resolveCliCommand(cli: string): string; /** * Compute the `MU__COMMAND` env var name mu consults when * resolving `--cli `. Hyphens in the cli key become underscores * (env var names can't contain `-`); this matches the operator-aliases * convention documented in the mu skill (e.g. `--cli pi-meta` → * `MU_PI_META_COMMAND`). */ declare function envVarNameForCli(cli: string): string; /** * Resolve `--cli ` to its actual command string AND tell the * caller whether the resolution came from a `MU__COMMAND` * env var or fell through to the bare cli name. The CLI uses this to * surface env-var attribution in the spawn-success line so config * issues are visible without `mu agent show`. */ declare function resolveCliCommandWithSource(cli: string): { command: string; envVar: string; resolvedFromEnv: boolean; }; interface CommandResolutionResult { ok: boolean; /** First whitespace-separated token of the command — the binary * whose presence on PATH we checked. */ binary: string; /** Absolute path of the resolved binary on PATH, when ok=true. */ resolvedPath?: string; } type CommandResolver = (command: string) => Promise; /** Override the PATH resolver. Tests use this to simulate "binary * absent" / "binary present" without depending on what's actually * installed. Production callers should never touch this. */ declare function setCommandResolverForTests(resolver: CommandResolver): void; /** Restore the default PATH resolver. */ declare function resetCommandResolverForTests(): void; /** * Verify the first token of `command` resolves to a binary on PATH. * Public so tests can call it directly; spawnAgent calls it before * the worktree is created so a bad --cli never creates an orphan worktree. */ declare function checkCommandResolvable(command: string): Promise; interface SpawnAgentOptions { name: string; workstream: string; /** Defaults to "pi". Any CLI is accepted: pi is driven over its control * socket; others run `MU__COMMAND` (or the cli name) and get * text by herdr `agent prompt` (a send to them on tmux is refused). */ cli?: string; /** The actual command to run in the pane. Defaults to the cli value. */ command?: string; /** Window name to group this agent under. Defaults to the agent's name * (so each agent gets its own window). Multiple agents sharing a `tab` * share a window with multiple panes. */ tab?: string; /** "full-access" (default) or "read-only". The schema stores it; today * the role isn't enforced (deferred to a future capabilities pass). */ role?: string; /** Initial working directory of the pane, recorded as `agents.cwd`. * Defaults to the caller's cwd. Ignored when `worktree: true`: the * worktree path is the cwd then. */ cwd?: string; /** Override the tmux session name. Defaults to `mu-`. */ tmuxSession?: string; /** Run the agent in its own `git worktree add --detach` at * `/worktrees//`; its path is the cwd and * its base commit is recorded as `agents.base`. A root outside a git * repo throws WorktreeVcsRequiredError. */ worktree?: boolean; /** Commit / branch the worktree starts at (only with `worktree: true`). */ from?: string; /** Repo the worktree is added to (only with `worktree: true`). Defaults * to the git toplevel of the caller's cwd. */ root?: string; /** Run on this host profile (`mu host add`): the pane runs the profile's * `shell` template, which starts the remote pi and forwards its ctl socket * here; `cwd` and `worktree` name REMOTE paths (the worktree is made by the * profile's `exec`). One channel is held from before the first remote * command until the pane dies. Refused on a mux that cannot run a shell * command (herdr). With `host`, `root` is a REMOTE repo (default the * profile's `repo`); a `worktree` needs one of them. */ host?: string; /** Run the control-socket handshake for pi agents (default true). * `false` skips it and reports `ctl: "skipped"`. */ ctl?: boolean; /** When `name` is taken in the workstream, take the next free one * (see `nextFreeAgentName`) instead of failing with AgentExistsError. */ nextFree?: boolean; /** Abort (the CLI's SIGINT/SIGTERM): kills a running `git worktree add` and * rolls the spawn back like a thrown error, up to its end. */ signal?: AbortSignal; /** * Called synchronously, inside the spawn lock, the moment the agent row * exists for the new pane (nothing awaited between the row write and this * call). A durable journal of the created registry identity (`agents.id` + * pane): a throw rolls the spawn back like a failed finalize. */ onIdentity?: (agent: AgentRow) => void; /** Where a local pi agent keeps its session (`PI_CODING_AGENT_SESSION_DIR` * in the pane env) instead of pi's per-cwd default. Set for delegates, so * their sessions stay out of the caller's `/resume` list. */ piSessionDir?: string; } /** Outcome of the spawn-time control-socket handshake. */ type SpawnCtl = "ok" | "missing" | "refused" | "skipped"; /** What spawnAgent returns: the registered row plus the handshake outcome. */ type SpawnedAgent = AgentRow & { ctl: SpawnCtl; ctlSocket: string; /** * The pi process the handshake's hello/status named. A send right after * the spawn carries it (`expectIncarnation`), so a replacement that binds * the same socket path is refused instead of handed the prompt. Absent * when the handshake did not run or the extension predates incarnations. */ ctlIncarnation?: string; /** Set when the startup scan saw a Claude trust dialog parked in the pane. */ trustDialog?: true; }; /** * Spawn a new agent in its tmux pane and register it in the DB. * * Phases: * 1. Validate name + uniqueness. * 2. If --worktree: prestageWorktree() (placeholder agent row + * worktree dir + registry row). * 3. mux.spawnAgent() in the worktree (or opts.cwd, default the * caller's cwd); the scratch session is ensured first. * 4. finalizeAgentRow() — patch the placeholder to the real pane id, * cwd and base (worktree path), or insert a fresh agent row. * 5. Liveness: awaitSpawnLiveness() (fixed sleep, then pane + scan * check), or for a tmux pi agent the ctl handshake with a pane * check per tick and the scan at the end. A backend with * `spawnAgentWaitsReady` (herdr) is ready when spawnAgent settles. * 6. ctl handshake for herdr pi agents (outside the rollback). * * Failure between any of (3)–(5) calls rollbackSpawn() to undo the * pane + row + worktree. The caller-visible error is preserved. */ declare function spawnAgent(db: Db, opts: SpawnAgentOptions): Promise; /** * Default liveness window in milliseconds. 0 disables the check (useful * for fast tests that don't want to wait). Override via env var * `MU_SPAWN_LIVENESS_MS`. */ declare function defaultSpawnLivenessMs(): number; type Transport = "ctl" | "mux"; type SendResult = { transport: Transport; state?: CtlState; /** * ctl only: pi's completed-run count. For a plain send it is measured * before dispatch (the `mu agent wait --after-runs` baseline); for * `--fresh` it is the new session's count once its run started. * Absent from an extension that predates it. */ runs?: number; /** ctl only: when pi entered `state` (epoch ms), from the same status. */ since?: number; /** ctl only: pi's session file, from the same status. */ sessionFile?: string; /** Set when the text ran as a pi session command over ctl. */ command?: CtlCommandName; /** ctl send/fresh/interrupt: the id this send carried; `mu agent check` asks about it later. */ msgId?: string; /** ctl only: the pi process's incarnation, the `--inc` for `mu agent wait`. */ incarnation?: string; /** Set when the reply never arrived and a `status {msgId}` probe confirmed the send landed. */ receipt?: "confirmed-after-timeout"; }; type TransportSendOptions = SendOptions & { /** How a busy pi queues the message. Default followUp. ctl only. */ mode?: "steer" | "followUp"; /** Control socket path. Default: agentCtlSocket(db, agent). */ socket?: string; /** Start a new pi session and send the text into it, as one ctl op. */ fresh?: boolean; /** With fresh or a session command: abandon a running turn instead of refusing. */ force?: boolean; /** * Internal: a msgId the caller reserved (and journaled) BEFORE the send, so * a crash leaves a receipt a later `status {msgId}` can ask about. ctl * send/fresh only; absent, the send mints its own. Never reuse one that was * already written: a saved id is checked, never resent. */ msgId?: string; /** * Internal: the ctl incarnation the caller's own handshake saw (a spawn's * hello). The send is refused, nothing written, when the socket's pi is * another process (AgentReplacedError); the extension re-checks it * atomically with the dispatch (`expectIncarnation`). ctl send/fresh only. */ expectIncarnation?: string; /** * Internal, opt-in (a stall action): bound the ctl round trips and fence the * write. `signal` aborts the hello and the request (an abort before the * write writes nothing; after it the delivery is indeterminate and probed, * never resent); `timeoutMs` bounds each ctl request; `beforeWrite` runs * synchronously on connect, immediately before the one write: false or a * throw writes nothing and the send throws CtlWriteVetoedError. ctl * send only; a route that cannot honour it (mux, session command) refuses rather than skip it. */ ctlGuard?: CtlGuard; }; /** See TransportSendOptions.ctlGuard. */ interface CtlGuard { signal?: AbortSignal; timeoutMs?: number; beforeWrite?: () => boolean; } /** * True when the agent runs pi, so sends go through its control socket. * The cli key decides when it names pi (or MU__COMMAND here runs * pi). Otherwise the agent's own socket file decides: spawn removes any * stale file before the pane starts, so a file at the derived path was * bound by this agent's extension (or its ssh forward). That covers * `--cli helper --command "pi-meta ..."`, whose command is not stored, * without a schema change. `sock` defaults to the derived path; pass * agentCtlSocket(db, agent) when a DB is at hand. A command gate (`gateToken`) * never speaks ctl: the discriminator beats both rules. */ declare function expectsCtl(agent: Pick & { gateToken?: string | null; }, sock?: string): boolean; declare function sendViaTransport(agent: AgentRow, text: string, opts: TransportSendOptions & { socket: string; }): Promise; interface TaskSpawnRequest { /** The spawn itself (name, workstream, cli, worktree, ...). `onIdentity` is this module's. */ spawn: Omit; /** The task's local id, in `spawn.workstream`. */ task: string; /** The attempt's name. Default: the task id. Stored as `manual:`. */ attempt?: string; /** The first prompt; also the task's pinned brief once it is delivered. */ text: string; } interface TaskSpawnOutcome { /** `sent`: delivered (now or by an earlier run). `unknown`: it may have landed; never resent. */ state: "sent" | "unknown"; /** True when a saved receipt answered and nothing new was spawned or sent. */ replay: boolean; receipt: SpawnReceipt; /** The live agent of THIS attempt; absent when its identity is stale. */ agent?: SpawnedAgent | AgentRow; /** The saved identity no longer holds: the agent was `deleted`, or the name now belongs to another `replaced` it. */ stale?: "deleted" | "replaced"; /** The send's own result; only on the run that sent. */ send?: SendResult; /** On `unknown`: why (the error, or the replay probe's verdict). */ reason?: string; /** A replay's one receipt probe, when it made one. */ checked?: "accepted" | "not-accepted" | "unreachable"; /** Whether the brief note was written, found, or could not be (reconciled on replay). */ brief: "recorded" | "kept" | "pending"; } /** * The executor's `auto:` namespace (src/auto-attempt.ts reserves it and * charges the run). Guarded replay: the receipt must already be reserved, a * failed (or stale-identity) attempt is NEVER reset or reused (it throws * `SpawnAttemptFailedError`; the next attempt is a new key and a new charge), * and `guard` is asked before the pane exists (`spawn`) and again inside the * transaction just before the prompt is written (`send`): a throw there is an * unsent failure, rolled back like any other. */ interface AutoAttemptRequest extends Omit { /** `auto:`. */ key: string; guard: (phase: "spawn" | "send") => void; /** * The attempt is an IN-PLACE retry of a live worker: an unsent failure marks * the receipt failed and nothing else. The worker, its claim and its worktree * are never released, killed or rolled back. */ keepAgent?: boolean; } /** * Run (or replay) one task-bound spawn attempt. Throws before any effect for * a bad request (unknown task, owned task, slash text, busy or conflicting * attempt) and, after an UNSENT failure, the original error once its own * resources are cleaned up. A delivered or unknown outcome is returned. */ declare function spawnForTask(db: Db, req: TaskSpawnRequest): Promise; /** Run or resume one reserved executor attempt (see {@link AutoAttemptRequest}). */ declare function spawnAutoAttempt(db: Db, req: AutoAttemptRequest): Promise; interface AgentRow { /** * The registry incarnation: the row's surrogate id. AUTOINCREMENT never * reuses one, so a name that is deleted and created again (or a pane id * that is reused) is always a NEW incarnation. Every destructive agent * write compares it (`deleteAgent`'s `expected`, `agent close --if`). * Not pi's `incarnation` (a string, the `--inc` of `agent wait`). */ incarnation: number; name: string; /** Foreign-name reference to the owning workstream. */ workstreamName: string; cli: string; paneId: string; role: string; /** Window name; null when the agent has its own window named after itself. */ tab: string | null; /** Directory the pane started in (its worktree under `--worktree`); null when spawned without a cwd. */ cwd: string | null; /** Commit the worktree was created at; null without `--worktree`. */ base: string | null; /** The command the pane was started with (machine-local); null for a row older than this column. */ command: string | null; /** Where `command` came from; null with it. */ commandSource: CommandSource | null; /** Who spawned it: `/` for a managed caller, else * `pi:`; null when unknown (older rows, no identity). */ spawner: string | null; /** The remote host profile it runs on; null for a local agent. */ host: string | null; /** HEAD last collected from its remote worktree; null until one is. */ collectedHead: string | null; /** A remote worktree's repo on its host (absolute, chosen at spawn); null otherwise. */ repo: string | null; /** Set for a remote command gate (`spawn --host` of a non-pi command): its spawn-unique token, the key of the remote result record. Null for every other agent. */ gateToken: string | null; /** ISO 8601 timestamp. */ createdAt: string; /** ISO 8601 timestamp. */ updatedAt: string; } interface LiveAgent extends AgentRow { state: RuntimeState; source: StateSource; /** Control-socket link: ok / missing / refused for pi agents, n/a otherwise. */ ctl?: CtlLink; /** ISO 8601 time when the source entered this state. */ since: string | null; reason?: string; idle?: boolean; /** The running tool of a pi agent (ctl read); null = none, absent = unknown. Not in `AgentOutput`. */ tool?: CtlTool | null; } interface InsertAgentInput { name: string; workstream: string; paneId: string; /** Defaults to "pi" via schema DEFAULT. */ cli?: string; /** Defaults to "full-access" via schema DEFAULT. */ role?: string; tab?: string | null; cwd?: string | null; base?: string | null; /** The resolved command and where it came from, kept so an outdated * extension can be respawned exactly. */ command?: string | null; commandSource?: CommandSource | null; /** Who spawned it (see AgentRow.spawner); never guessed from task ownership. */ spawner?: string | null; host?: string | null; collectedHead?: string | null; repo?: string | null; gateToken?: string | null; } declare function insertAgent(db: Db, input: InsertAgentInput): AgentRow; /** * Look up an agent by its tmux pane id (e.g. `%4`). Returns undefined if * no agent currently owns that pane. Used by `mu me` and friends to * answer "which agent am I?" from `$TMUX_PANE` without the LLM having to * remember its own name. * * Note: `pane_id` is not declared UNIQUE in the schema (a managed agent * could in theory be re-spawned into the same recycled pane id) but in * practice tmux pane ids are unique within a server's lifetime, and * reconcile prunes ghosts. We return the first match. */ declare function getAgentByPane(db: Db, paneId: string): AgentRow | undefined; declare function getAgent(db: Db, name: string, workstream: string): AgentRow | undefined; declare function listAgents(db: Db, opts?: { workstream?: string; }): AgentRow[]; /** * Delete an agent row. Returns true if a row was matched. Idempotent; * deleting an agent that doesn't exist returns false without throwing. * * Reaper side-effect: any task that was IN_PROGRESS owned by this * agent gets flipped back to OPEN with a `[reaper]` task_note, and a * remote worker's worktree is recorded in `remote_recovery` first, all in * one transaction (src/agents/reap.ts). The FK on `tasks.owner` is * `ON DELETE SET NULL` so the owner column resets automatically; the * extra step here is the status revert. Without this an agent that * crashed (or was explicitly closed mid-task) leaves the task graph * in a wrong state — IN_PROGRESS forever, with no owner to release. */ declare function deleteAgent(db: Db, name: string, workstream: string, /** * Compare-and-swap, checked inside the reaper transaction: delete only * if the row still has this pane id (a bare string) and/or this * incarnation. Same name but another pane or incarnation means the agent * was replaced: it is kept (false), with its row, socket and tasks. */ expected?: string | AgentIdentity, /** * `preserve: false` skips the remote recovery record: only for a close that * has itself just removed (or found gone) the worktree. Every other * deleter keeps what the row says about it (src/remote-recovery.ts). */ opts?: { preserve?: boolean; }): boolean; /** Which incarnation of a name a destructive write means. */ interface AgentIdentity { paneId?: string; incarnation?: number; } declare function isValidAgentName(name: string): boolean; /** * Send text to an agent and submit it. A pi agent gets it through its * control socket (AgentCtlUnreachableError when that does not answer — * never a silent paste), `/new` / `/reload` / `/compact` included; a * non-pi CLI gets one atomic herdr `agent prompt`; on tmux it is refused * (AgentSendUnsupportedError). See src/agents/transport.ts. */ declare function sendToAgent(db: Db, name: string, text: string, opts: TransportSendOptions & { workstream: string; /** * The registry id + pane the caller means (a just-spawned agent): the * name now holding another one is refused (AgentReplacedError), the * prompt never goes to a replacement. */ expect?: { incarnation: number; paneId: string; }; }): Promise; /** * Read scrollback from an agent's pane. With no options, returns the full * scrollback (`-S - -E -`); with `lines: N`, returns only the last N lines. */ declare function readAgent(db: Db, name: string, opts: CaptureOptions & { workstream: string; }): Promise; interface ListLiveAgentsOptions { workstream: string; tmuxSession?: string; /** * Which kind of reconciliation pass to run. Forwarded to * `reconcile()`'s same-name option. Default `"full"` (the * documented mutating behaviour `mu agent list` has always had, * now also used by `mu state`). * * `mu doctor` passes `"report-only"`: count drift, mutate nothing. * * Mid-spawn placeholders (pane id `%pending-`) are protected * directly in reconcile's prune loop, independent of mode * (see PENDING_PANE_PREFIX). * * BREAKING: this replaces the previous `dryRun?: boolean` * option. Migration: `dryRun: true` → `mode: "report-only"`; * default (`dryRun: false` / unset) → `mode: "full"`. */ mode?: ReconcileMode; } interface LiveAgentsView { /** All registered agents in the workstream, post-reconcile. */ agents: LiveAgent[]; /** Panes in the tmux session that look like agents but aren't registered. */ orphans: MuxPane[]; /** Diagnostic numbers from the reconcile pass; useful for `mu doctor`. */ report: ReconcileReport; } /** * Return the live, reality-reconciled view of agents in a workstream. * `mu state` and `mu agent list` call this with the default `mode: "full"` * (mutating); the read-only diagnostic (`mu doctor`) calls it with * `mode: "report-only"` to mutate nothing at all. */ declare function listLiveAgents(db: Db, opts: ListLiveAgentsOptions): Promise; /** * The registered agents of a workstream with their computed runtime state * (ctl probe, idle flag), WITHOUT reconciling: no mux read, nothing reaped, * no write. The read-only half of {@link listLiveAgents}; `stateSnapshot` * (and so `mu state --me` and the dashboard) uses it. */ declare function readLiveAgents(db: Db, workstream: string, /** Only the agent of this name (`mu me` reads itself, not its neighbours' sockets). */ only?: string, /** * A reading cache keyed by `agentKey` (the dashboard's slow tier). With `probe: false` * an agent already in the cache is NOT probed (no socket, no subprocess): only agents * missing from it are, and their readings are added. `probe: true` re-reads every agent * and refreshes the cache. Absent: probe every agent, cache nothing. */ states?: { cache: Map; probe: boolean; }): Promise; /** An op as applied. Mirrors the `ops` row shape, minus the local-only * `seq` (meaningless on a peer) and the advisory `created_at`. */ interface Op { /** The ordering key. See docs/VOCABULARY.md § HLC. */ hlc: string; /** Which peer minted it. Half of the (machine_id, hlc) identity. */ machineId: string; /** One user-visible action (the op group). */ groupId: string; /** Free text: agent name, "user", "system". */ actor?: string | null; /** Semantic label, e.g. "task.close". */ intent?: string | null; /** Which kind of thing this op addresses. */ entity: string; /** The NATURAL key. Never a surrogate id. */ key: string; /** 'put' = semantic partial update, 'del' = tombstone. */ op: "put" | "del"; /** JSON object of ONLY the fields this op touched. */ payload: string; } /** Thrown when an op names an entity this build KNOWS is machine-local * (see `MACHINE_LOCAL_ENTITIES`). Loud by design: a peer shipping a * pane id or an absolute path is a real bug we want reported. * * NOT thrown for an entity we merely do not recognise. That is a * reader-behind-writer vocabulary skew in a mixed fleet, and treating * it as a defect froze one peer's watermark permanently — see * `MACHINE_LOCAL_ENTITIES` in src/db.ts for the incident. */ declare class OpEntityNotSyncedError extends Error { readonly entity: string; constructor(entity: string); } /** Thrown when an op's natural key does not parse for its entity. */ declare class OpKeyMalformedError extends Error { readonly entity: string; readonly key: string; constructor(entity: string, key: string); } /** What `applyOp` did, so callers (and tests) can assert on outcomes * rather than re-querying. */ interface ApplyResult { /** True iff the DB changed. False for a dead, superseded or duplicate * op: legitimate and non-exceptional. */ changed: boolean; /** The columns of a task or workstream row the op changed. */ appliedFields: string[]; } /** * Apply one op to the portable tables. * * SYNCHRONOUS, for the same reason `withOpContext` is: the op context is * the shared main-DB `op_ctx` row, so two interleaved async apply scopes * would clobber each other's `applying` flag with no way to tell whose * suppression was in force. The scope is one synchronous transaction; * keeping this sync makes that unrepresentable rather than merely * discouraged. * * Runs inside `withCaptureSuppressed`, which is THE echo guard: without * it, writing a peer's op to `tasks` would fire the capture trigger, * mint a fresh local op, flush that back to the peer, and loop forever. * Runs in its own transaction (a savepoint when nested): one op rewrites * several rows, and they commit together or not at all. * * Does NOT insert the op into `ops` — that is the caller's job (which * owns segment bookkeeping and the (machine_id, hlc) dedupe). The op is * folded in beside the log's rows, so recording it before or after * changes nothing and a replay of ops already in the log is a no-op. * * Idempotent: applying the same op twice makes no second change, which * is what lets `mu sync --repair` be nothing more than "re-read that * peer's segment from zero". */ declare function applyOp(db: Db, op: Op): ApplyResult; /** * Apply many ops in HLC order, inside one transaction. The fold makes the * order irrelevant to the result; sorting keeps the work in log order. */ declare function applyOps(db: Db, ops: readonly Op[]): ApplyResult[]; interface TaskRow { /** Per-workstream-unique TEXT name. The operator-facing identifier. */ name: string; /** Foreign-name reference to the owning workstream. */ workstreamName: string; /** The real `tasks.workstream_id` (output boundary only; never invent from names). */ workstreamId: number; title: string; status: TaskStatus; /** Qualifies status (e.g. OPEN/parked, CLOSED/wontfix). Never null. */ substate: TaskSubstate; impact: number; effortDays: number; /** True once impact/effort were given explicitly (not defaulted). */ scored: boolean; /** Host `mu next` dispatches this task to, if one was set. */ dispatchHost: string | null; /** `work` (default), `delegate` or `human`. */ kind: TaskKind; /** Label grouping the delegate calls of one fan-out; null when none. */ wave: string | null; /** Foreign-name reference to the owning agent (NULL when unowned). */ ownerName: string | null; /** The real `tasks.owner_id` (NULL when unowned). */ ownerId: number | null; createdAt: string; updatedAt: string; } interface TaskNoteRow { /** 1-based position among the task's notes (by insertion). Task-bound and * local to this DB: peers may store different integer ids, so it is a * selector for `--from` / `--note` here, never an identity to sync. */ n: number; kind: NoteKind; author: string | null; content: string; createdAt: string; } /** A refusal that leaves everything unwritten (exit 4). */ declare class TaskAutoRefusedError extends Error { readonly workstream: string; readonly taskId: string; readonly name = "TaskAutoRefusedError"; constructor(message: string, workstream: string, taskId: string); } /** A bad `--cli` / `--max` / empty selection (exit 2). */ declare class TaskAutoArgError extends Error { readonly name = "TaskAutoArgError"; } interface AuthorizeTasksOptions { workstream: string; /** The model key `mu next` spawns with. Ignored by `off`. */ cli: string; worktree: boolean; /** Run ceiling (> 0). Ignored by `off`. */ maxRuns: number; /** Revoke LOCAL permission (keeps the counters) instead of granting it. */ off?: boolean; /** Also select every open transitive blocker of this task (not the task itself). */ blockersOf?: string; /** Attribution for the captured ops. */ actor?: string; } interface AuthorizeTasksResult { /** The tasks acted on, in selection order. */ ids: string[]; /** False when every row and designation was already as asked. */ changed: boolean; } interface AutoConfig { cli: string; worktree: boolean; maxRuns: number; runs: number; lastSpawn: string | null; enabled: boolean; } /** A ready task the executor may act on, with its stored config. */ interface AuthorisedTask extends TaskRow { auto: AutoConfig; } /** The stored config of one task (surrogate id), enabled or not. */ declare function getAutoConfig(db: Db, taskId: number): AutoConfig | undefined; /** * May this machine act on the task (steer, release, dispatch)? True iff an * ENABLED `task_auto` row exists, the task is `kind=work`, and its synced * `dispatch_host` is this machine. Independent of status and of the run * ceiling. Resolve the surrogate id with `taskIdFor`. */ declare function autoPermitted(db: Db, taskId: number): boolean; /** * The tasks `mu next` may dispatch now, in `ready` order: ready (OPEN, * unblocked, not parked/triage) AND permitted AND `runs < max_runs`. * `kind` human/delegate never qualifies; the reserved scratch queue is empty. */ declare function authorisedReady(db: Db, workstream: string): AuthorisedTask[]; /** * The local ids of every OPEN or IN_PROGRESS transitive blocker of an * umbrella, sorted. Walks only the umbrella's own workstream, uncapped * (unlike the display frontier), safe on diamonds and cycles, never walks * through a CLOSED task, never lists the umbrella itself. */ declare function openBlockersClosure(db: Db, workstreamId: number, umbrellaId: number): string[]; /** * Grant (or, with `off`, revoke) this machine's permission to dispatch the * selected tasks. Grant: upsert the local `task_auto` row (keeping `runs` * and `last_spawn`, setting `enabled`) and write the synced `dispatch_host` * = this machine. Off: `enabled = 0`, and `dispatch_host` cleared only when * this machine owns it. The whole batch is validated inside ONE * transaction before any write: a refusal leaves nothing changed. */ declare function authorizeTasks(db: Db, ids: readonly string[], opts: AuthorizeTasksOptions): AuthorizeTasksResult; /** * How a stored attempt ended without a delivery: `abandoned` (nothing left to * do: the task closed, the agent is gone) or `needs_human` (the prompt may * have landed and cannot be confirmed: never resent, parked behind a human * need). Either is terminal: never replayed, never counted again. */ interface AttemptEnd { state: "abandoned" | "needs_human"; reason: string; } /** `task_auto.last_spawn`: the latest attempt, with its exact request. */ interface AttemptRecord { v: 1; attempt: number; /** `auto:`: the `spawn_receipts.attempt_key`. */ key: string; /** The agent name asked for (a taken one is moved on by `nextFree`; the receipt has the real one). */ name: string; cli: string; worktree: boolean; /** The exact prompt, stored at reservation: a replay sends these bytes. */ text: string; /** The previous attempt's worker is gone: the brief says so. */ retry: boolean; /** * A retry IN PLACE: the same live worker (its saved registry id + pane) is * sent the brief again after an API-errored run. No pane is spawned, and no * failure ever removes the worker. */ inPlace?: boolean; /** Set once the attempt reached a terminal state without a delivery. */ end?: AttemptEnd; } interface AttemptPlan extends AttemptRecord { task: string; taskId: number; workstream: string; /** The task's derived revision the plan was made at. */ revision: number; } type PlanCode = "not-found" | "not-permitted" | "in-flight" | "parked" | "delivered" | "ceiling" | "not-ready" | "no-brief" | "changed"; type PlanResult = { ok: true; plan: AttemptPlan; } | { ok: false; code: PlanCode; reason: string; }; /** * Local ids of the permitted tasks of `workstream` whose latest attempt is * in flight (reserved … unknown) and not ended: to be RESUMED before anything * new is allocated, even at their run ceiling. Revoked, foreign and ended * (abandoned / needs_human) attempts are absent. */ declare function pendingAttempts(db: Db, workstream: string): string[]; /** The agent name an attempt asks for: the task id cut to leave room for `-`. */ declare function attemptName(task: string, attempt: number): string; /** * Read-only: may a NEW attempt for `task` be reserved now, and what would it * be? Checks, in order: permission (enabled, work, this host), an attempt * already in flight or delivered, the run ceiling, readiness and the brief. * `revision` (when given) must equal the task's current revision. */ declare function planAttempt(db: Db, workstream: string, task: string, opts?: { revision?: number; }): PlanResult; /** * Charge one run and reserve its attempt, atomically (see the file header). * Re-plans INSIDE the immediate transaction, so a change seen at ingest * between the caller's view and now refuses instead of acting on it. */ declare function reserveAttempt(db: Db, workstream: string, task: string, opts?: { revision?: number; }): PlanResult; /** * The revision of the trigger set stored in the file, stamped in * `schema_version.capture_version`. BUMP IT whenever any generator in * this file changes the SQL it emits: an older stamp makes the next * writable `openDb` replace every capture trigger (one BEGIN IMMEDIATE, * with the stamp), and a newer stamp makes it refuse (SchemaTooNewError) * instead of running triggers it cannot vouch for. * test/capture-persistent.test.ts pins the sha256 of the generated DDL, * so forgetting the bump fails a test. * * Separate from CURRENT_SCHEMA_VERSION on purpose: that number says * "tables and columns are incompatible" and refuses older DBs outright; * a capture revision is upgradeable in place, so it must not be folded * into it (an older schema number is refused before any trigger work). */ declare const CAPTURE_VERSION = 5; /** Every capture trigger as `CREATE TRIGGER …;` statements, in order. */ declare const CAPTURE_TRIGGER_DDL: string; /** True when the stored triggers are byte-for-byte what this build * generates (same names, same SQL). */ declare function captureTriggersCurrent(db: CaptureDb): boolean; /** * Make the capture triggers in this file current. Called once by * `openDb` on every non-readonly open, after the schema is applied. * * stamp == CAPTURE_VERSION nothing: no DDL, no write lock. * stamp > CAPTURE_VERSION CaptureTooNewError; the file is untouched. * stamp < CAPTURE_VERSION (or missing) drop every `_cap_*` trigger, * create the generated set, seed `op_ctx`, * and stamp: ONE BEGIN IMMEDIATE, so no * connection ever sees half a trigger set * and a crash leaves the old set and the old * stamp together. The stamp is re-read under * the lock: a concurrent opener that won the * race makes this a no-op, and a newer one * is still refused. */ declare function installCapture(db: CaptureDb): void; /** The slice of better-sqlite3's Database that capture needs. Declared * structurally rather than importing `Db` from db.ts, because db.ts * imports THIS module — a nominal import would be a cycle, and an * import cycle here silently breaks `node dist/cli.js`. */ interface CaptureDb { readonly inTransaction: boolean; exec(sql: string): unknown; prepare(sql: string): { run(...params: readonly unknown[]): unknown; get(...params: readonly unknown[]): unknown; all(...params: readonly unknown[]): unknown[]; }; } interface SetStatusResult { /** Status before the call. */ previousStatus: TaskStatus; /** Status after the call (== requested status). */ status: TaskStatus; /** Substate before the call. */ previousSubstate: TaskSubstate; /** Substate after the call (== requested substate, or the status default). */ substate: TaskSubstate; /** True iff status OR substate changed. False on idempotent no-op. */ changed: boolean; } /** The substates a task can be closed as (`mu task close --as`). */ type CloseSubstate = (typeof TASK_SUBSTATES)["CLOSED"][number]; /** * Optional evidence string carried on lifecycle verbs (close / open / * claim / release). Lands in the auto-emitted `kind='event'` payload * verbatim, prefixed with `evidence=`. The first inch of distinguishing * "observed" from "claimed" state per an internal critique: the * verb still trusts the caller (it's not a verifier), but the audit * trail records what the caller said it relied on. */ interface EvidenceOption { evidence?: string; } /** Evidence plus the actor its note is attributed to. The CLI passes * `resolveActorIdentity()`; without it the note has no author and * renders as ``. */ interface AttributedEvidence extends EvidenceOption { author?: string; } interface SetStatusOptions extends EvidenceOption { workstream: string; /** Substate to land on; defaults to DEFAULT_SUBSTATE[status]. A pair * outside TASK_SUBSTATES throws InvalidSubstateError before any write. */ substate?: TaskSubstate; } /** * Flip a task's status to any of OPEN / IN_PROGRESS / CLOSED, writing * status and substate in ONE UPDATE (one op, one HLC — spec D7). * Idempotent: setting a task to its current pair is a no-op (returns * `changed: false`) rather than throwing. Owner is unchanged. */ declare function setTaskStatus(db: Db, localId: string, status: TaskStatus, opts: SetStatusOptions): SetStatusResult; /** Result of `closeTask` when called with `ifReady: true` and the * task is NOT yet ready to close (still has at least one OPEN / * IN_PROGRESS blocker). Distinguished from a regular `SetStatusResult` * by the literal `skipped` field; the CLI keys on it to switch * between the "closed" and "waiting" rendering paths. * * A wave umbrella * with N blockers stayed OPEN after every blocker reached a terminal * status. `--if-ready` is the cheap fix: bare `mu task close` is * unchanged (closes regardless), `--if-ready` is a no-op unless every * blocker is CLOSED. */ interface CloseSkippedResult { /** Always 'not_ready' when set; future cause-codes can extend this * without reshaping the JSON payload (the literal-union narrows * safely in the CLI rendering path). */ skipped: "not_ready"; /** Status before the call (always the current status, no change). */ previousStatus: TaskStatus; /** Status after the call (== previousStatus, since we no-op). */ status: TaskStatus; /** Substate before (and after) the call. */ previousSubstate: TaskSubstate; substate: TaskSubstate; /** Always false on a skip (no row mutated). */ changed: false; /** Local ids of every blocker still in OPEN or IN_PROGRESS, sorted * alphabetically for deterministic rendering. Empty list is * impossible on this branch — the no-op only fires when ≥1 * blocker is non-terminal. */ blockingIds: string[]; } interface CloseTaskOptions extends EvidenceOption { workstream: string; /** When true, no-op the close unless every blocker is CLOSED. * Returns a `CloseSkippedResult` carrying the still-blocking ids. * When false / omitted, behaves as bare `closeTask` * (closes regardless of blocker status). */ ifReady?: boolean; /** Optional actor identity attributed to the synthetic `CLOSE: …` * note auto-inserted when `evidence` is non-empty (see closeTask * body). The CLI resolves this via `resolveActorIdentity()` so the * note carries the closing worker's name; SDK callers (tests, * internal use) may omit it (the note then carries no author, same * as a bare `addNote` without `--author`). Surfaced in mufeedback * task_close_evidence_does_not_append_the. */ author?: string; /** Closing substate; default "done" (or, for an already-CLOSED task, * its current substate). Any CLOSED/* satisfies edges. */ as?: CloseSubstate; /** The worker's report, stored verbatim as a `report` note BEFORE the * close notes and the status put, in the same op group. NOT evidence: * a `done` close still needs `evidence`. Skipped on a no-op close. */ report?: string; /** 'new' (default): the close is its own op group. 'inherit': report, * close notes and status put join the CALLER's group, actor and intent * (no new op context), so a caller's group holds the whole close. */ group?: "new" | "inherit"; /** Required (non-empty) unless `as` is "done"; stored as a * `: ` note (kind 'close') in the same op group. */ why?: string; } /** Result of a close that ran (not skipped). */ interface CloseTaskResult extends SetStatusResult { /** Direct same-workstream dependents that entered the `ready` view * because of this close, sorted. Computed only when `as` is not * "done" (spec D3: a non-done close must show what it released); * always [] for a done close. */ unblocked: string[]; } /** Convenience: setTaskStatus(db, id, "CLOSED"). Accepts evidence. * Notes are skipped for the idempotent no-op (already CLOSED with the * same substate; a bare close without `as` keeps the current one) so * retry loops don't accumulate duplicates. * * With `ifReady: true`, returns a `CloseSkippedResult` (no mutation, * no snapshot) when any blocker is still OPEN / IN_PROGRESS. Used by * `mu task close --if-ready` so an orchestrator can fire-and-forget * the umbrella close after every blocker resolves without first * re-querying the graph. */ declare function closeTask(db: Db, localId: string, opts: CloseTaskOptions): CloseTaskResult | CloseSkippedResult; /** Convenience: setTaskStatus(db, id, "OPEN"). Owner intentionally NOT * cleared — use `releaseTask` for that. Accepts evidence. */ declare function openTask(db: Db, localId: string, opts: AttributedEvidence & { workstream: string; }): SetStatusResult; interface ParkTaskOptions extends EvidenceOption { workstream: string; /** Required, non-empty; stored as a `PARKED: ` note. */ why: string; author?: string; } /** * OPEN/todo → OPEN/parked: keep the task out of `ready` / `next` and * make `claim` refuse it without `--force`. Its dependents stay blocked * (parked is still OPEN). Idempotent on OPEN/parked (no second note). * Refuses IN_PROGRESS (release first) and CLOSED (open first) — one * verb, one transition. */ declare function parkTask(db: Db, localId: string, opts: ParkTaskOptions): SetStatusResult; /** * OPEN/triage → OPEN/todo: accept a proposed task (a review finding) as * real work, back in `ready` / `next`. A no-op (`changed: false`) on any * other pair. Declining is `close --as rejected | duplicate --why`. */ declare function acceptTask(db: Db, localId: string, opts: AttributedEvidence & { workstream: string; }): SetStatusResult; /** OPEN/parked → OPEN/todo. A no-op (`changed: false`) on any other pair. */ declare function unparkTask(db: Db, localId: string, opts: AttributedEvidence & { workstream: string; }): SetStatusResult; interface ReleaseResult { /** The previous owner (null if the task was already unowned). */ previousOwnerName: string | null; /** Status before the release. */ previousStatus: TaskStatus; /** Status after the release. */ status: TaskStatus; /** True iff owner OR status actually changed. */ changed: boolean; } interface ReleaseTaskOptions extends AttributedEvidence { /** `--if `: the task's revision from `mu task show`. A stale one * throws `RevisionMismatchError` before any write. */ ifRevision?: number; /** Workstream context for the task (v5: tasks.local_id is * per-workstream unique). */ workstream: string; /** Force `status = OPEN` regardless of the current status. Without * this flag, `IN_PROGRESS` is also flipped to `OPEN` automatically * (so a released task isn't left structurally stranded with * `owner=NULL, status=IN_PROGRESS`); CLOSED is preserved. * `--reopen` is the override for the rarer "un-close and hand * back to the pool" workflow. */ reopen?: boolean; /** * Rollback guard: release only while the task is STILL owned by this * registry agent id (`agents.id`) and not CLOSED, decided inside one * immediate transaction. A task another agent now owns, that was * released, or that was closed meanwhile is left alone (`changed: false`). */ expectedOwnerId?: number; } /** * Release a task: clear `tasks.owner`. * * Status side-effects: * - IN_PROGRESS → OPEN automatically (without it, the task is * stranded: no owner to drive it forward, but `mu task next` * skips it because it's not OPEN). * - OPEN / CLOSED preserved. * - `--reopen` forces OPEN regardless of current status — the * escape hatch for un-closing a CLOSED owned task in one verb. * * Idempotent: releasing an already-unowned task with no `--reopen` and * no IN_PROGRESS status is a no-op (returns `changed: false`). * Throws TaskNotFoundError on missing. */ declare function releaseTask(db: Db, localId: string, opts: ReleaseTaskOptions): ReleaseResult; interface ClaimTaskOptions extends AttributedEvidence { /** `--if `: the task's revision from `mu task show`. A stale one * throws `RevisionMismatchError` inside the claim transaction, before * any write. */ ifRevision?: number; /** Workstream context for both the task and the claiming agent. * v5: agents.name and tasks.local_id are per-workstream unique; * the task lookup AND the agent FK lookup scope to this * workstream so a same-named task or worker elsewhere can't be * silently picked. The CLI always passes this from the resolved * -w or the injected worker scope. */ workstream: string; /** * Override the agent name. If omitted, resolved from the ambient * environment via `resolveWorkerIdentity()`: `$MU_AGENT_NAME` only * (spawn injects it; pane titles are not identity). * * Mutually exclusive with `self: true`. */ agentName?: string; /** * Workstream that the claimer agent lives in. When omitted, defaults * to `opts.workstream` (today's same-workstream behaviour). Set by * the CLI when `mu task claim X -w A --for B/worker-1` qualifies the * `--for` ref with a different workstream prefix * (`task_claim_for_cross_workstream`). * * Cross-workstream ownership is structurally allowed by the schema: * `tasks.owner_id` is an INTEGER FK to `agents.id` with no * workstream qualifier on the agent side. The per-workstream UNIQUE * on `agents(workstream_id, name)` is what previously made the * SDK's name → id lookup scope to one workstream; this option * widens that lookup to a different workstream when the operator * dispatches across a workstream boundary. The agent's own * workstream remains unchanged — only the task's `owner_id` points * out-of-workstream. */ agentWorkstream?: string; /** * The registry id (`agents.id`) the caller created or validated for * `agentName`. Re-checked INSIDE the immediate claim transaction: when the * name now belongs to another incarnation (deleted and recreated), the * claim throws ClaimerNotRegisteredError and the replacement never owns * the task. Absent: the name is resolved as before. */ expectedAgentId?: number; /** * Anonymous claim: write `owner_id = NULL` instead of resolving an agent * name and checking the FK. Use when the actor is the orchestrator * (or a script, or a human) doing direct work in a workstream they * aren't a registered worker in. * * The actor name is still recorded — it ends up in `ops.actor` of * the captured `task.claim` op — so provenance is preserved. * Just not in the FK column. * * Resolution order for the actor name (used as the log source): * 1. `actor` if explicitly passed. * 2. `$MU_AGENT_NAME`. * 3. `$USER`. * 4. The literal string 'orchestrator'. * * Mutually exclusive with `agentName` (the two are alternative * answers to "who's the actor for this claim?"). Passing both is a * usage error. */ self?: boolean; /** * Override the actor name used for the log source when `self: true`. * Ignored when `self: false`. Useful when the orchestrator wants to * attribute the work to a meaningful name rather than the pane * title (e.g. "deploy-bot" rather than "pi-mu"). */ actor?: string; /** * Claim an OPEN/parked task anyway. Without it, a parked task throws * TaskParkedError before any write: parking means "keep out of the * scheduler", so overriding it must be explicit. */ force?: boolean; } interface ClaimResult { /** The agent now owning the task, or null when the claim was anonymous (--self). */ ownerName: string | null; /** The actor recorded in the claim op — the agent name for a * registered-worker claim, or the resolved actor for --self. */ actorName: string; /** The previous owner (null if it was unowned). */ previousOwnerName: string | null; /** The status BEFORE the claim; post-claim is IN_PROGRESS unless was CLOSED. */ previousStatus: TaskStatus; /** The status AFTER the claim. */ status: TaskStatus; } /** * Claim a task. Two modes: * * Worker claim (default): * Resolve an agent name from `opts.agentName`, else $MU_AGENT_NAME, * else fails. The name * MUST exist in the agents table (`tasks.owner_id` is an FK to * `agents.id`). Sets `owner_id` to that agent. This is what mu-spawned * workers do, and what `mu task claim --for ` does for * orchestrator dispatch. * * Anonymous claim (--self): * Skip the name -> agents FK lookup entirely. Sets `owner_id = NULL`. * Records the actor in `ops.actor` instead. This is the * orchestrator-doing-direct-work path — the actor is logged but * not registered as a worker pane. * * Status side-effect: OPEN -> IN_PROGRESS; IN_PROGRESS / CLOSED unchanged. * * Concurrency: the worker-claim path uses a single-statement CAS UPDATE * with `WHERE owner IS NULL OR owner = ?` so two workers racing to * claim the same task can't both win. The anonymous path uses * `WHERE owner IS NULL` (anonymous claims don't 'own' the task in any * exclusive sense; if it's already owned by anyone, the anonymous claim * is a TaskAlreadyOwnedError just like a worker claim would be). */ declare function claimTask(db: Db, localId: string, opts: ClaimTaskOptions): Promise; /** * Resolve the current actor's identity for attribution in task notes, * --self claims, and any other write that wants 'who did this?'. * * Resolution order: * 1. $MU_AGENT_NAME env var (set by mu spawnAgent on every managed * pane; surfaced from the f3d4bdd commit). Authoritative when * present — you're inside a mu-spawned worker, no ambiguity. * 2. $USER (when running outside a mu pane entirely). * 3. The literal 'orchestrator' as a last-resort default. * * The env var is set per-pane at spawn time and is unforgeable from * outside without explicit `--actor` override. Pane titles are not * identity: nothing reads or writes them. */ declare function resolveActorIdentity(): Promise; declare class TaskNotFoundError extends Error { readonly taskId: string; readonly workstream?: string | undefined; /** The id the candidates are matched against, when `taskId` is the * operator's qualified spelling (`ws/id`). Defaults to `taskId`. */ readonly lookupId: string; readonly name = "TaskNotFoundError"; /** Near ids from the SAME workstream (`candidateTaskIds`), read-only * suggestions: filled by `handle()` after the miss, never used to write. */ candidateIds: readonly string[]; /** `workstream` is the partition the exact lookup missed in, when known. */ constructor(taskId: string, workstream?: string | undefined, /** The id the candidates are matched against, when `taskId` is the * operator's qualified spelling (`ws/id`). Defaults to `taskId`. */ lookupId?: string); } /** * Thrown by `addTask` when `localId` violates the schema regex * `/^[a-z][a-z0-9_-]{0,63}$/`. Replaces a bare `TypeError` so the * CLI's `handle()` wrapper can map it to exit code 2 (usage, with the * verb's --help, like WorkstreamNameInvalidError). */ declare class TaskIdInvalidError extends Error { readonly attempted: string; readonly name = "TaskIdInvalidError"; constructor(attempted: string); } declare class TaskExistsError extends Error { readonly taskId: string; readonly name = "TaskExistsError"; constructor(taskId: string); } /** * Thrown when a verb is invoked with `-w/--workstream ` but the * named task lives in a different workstream. Distinguishes "the user * typo'd the workstream" from "the task doesn't exist anywhere" * (which surfaces as `TaskNotFoundError`). Maps to exit code 4 * (conflict / wrong scope). */ declare class TaskNotInWorkstreamError extends Error { readonly taskId: string; readonly expectedWorkstream: string; readonly actualWorkstream: string; readonly name = "TaskNotInWorkstreamError"; constructor(taskId: string, expectedWorkstream: string, actualWorkstream: string); } declare class TaskAlreadyOwnedError extends Error { readonly taskId: string; readonly currentOwner: string; readonly name = "TaskAlreadyOwnedError"; /** `taskId` / `currentOwner` are omitted when a raw `mu sql` write tripped * the guard: the SQL may not name one task, so the hints carry placeholders. */ constructor(taskId?: string, currentOwner?: string); } /** * Thrown when `mu task claim` resolves a claimer agent name (from the * pane title or --for) that has no matching row in the agents table. * * The FK on `tasks.owner_id` references `agents.id`; without this guard * the claim attempt would fail with the unhelpful 'FOREIGN KEY constraint * failed' from SQLite. This typed error gives the user actionable next * steps (work with --self, or use --for to pick a registered agent). * * Maps to exit code 4 (conflict) via the cli.ts handler. */ declare class ClaimerNotRegisteredError extends Error { readonly agentName: string; readonly paneId: string | null; readonly name = "ClaimerNotRegisteredError"; constructor(agentName: string, paneId: string | null); } declare class CycleError extends Error { readonly from: string; readonly to: string; /** Existing path `to -> … -> from` (task names) that the new edge would close. */ readonly path: readonly string[]; readonly workstream?: string | undefined; readonly name = "CycleError"; constructor(from: string, to: string, /** Existing path `to -> … -> from` (task names) that the new edge would close. */ path?: readonly string[], workstream?: string | undefined); } declare class CrossWorkstreamEdgeError extends Error { readonly blocker: string; readonly blockerWorkstream: string; readonly dependent: string; readonly dependentWorkstream: string; readonly name = "CrossWorkstreamEdgeError"; constructor(blocker: string, blockerWorkstream: string, dependent: string, dependentWorkstream: string); } /** * Thrown by `closeTask` (non-`done` `--as`) and `parkTask` when no * non-empty `--why` was given. The reason is stored as a note, so a * classification with no rationale is refused before any write. */ declare class SubstateReasonRequiredError extends Error { readonly verb: "close" | "park"; readonly substate: TaskSubstate; readonly taskId: string; readonly name = "SubstateReasonRequiredError"; constructor(verb: "close" | "park", substate: TaskSubstate, taskId: string); } /** * Thrown when a close lands without a `kind = 'close'` note in its op * group: the local-write guard `_cap_tasks_close_evidence` (src/capture.ts) * refused it. Raised by `closeTask` only; ingest never sees it. */ declare class CloseNeedsEvidenceError extends Error { readonly taskId: string; readonly workstream: string; readonly name = "CloseNeedsEvidenceError"; /** Ids are omitted when a raw `mu sql` write tripped the guard: the SQL * may not name one task, so the message and hint carry placeholders. */ constructor(taskId?: string, workstream?: string); } /** * Thrown when a close to `done` finds a blocker still OPEN or * IN_PROGRESS (guard `_cap_tasks_close_blockers`). Classifying the task * as wontfix / duplicate / rejected / superseded is exempt: those do not * claim the work happened. */ declare class TaskHasOpenBlockersError extends Error { readonly taskId: string; readonly workstream: string; readonly blockerIds: readonly string[]; readonly name = "TaskHasOpenBlockersError"; /** Ids are omitted when a raw `mu sql` write tripped the guard: the SQL * may not name one task, so the message and hint carry placeholders. */ constructor(taskId?: string, workstream?: string, blockerIds?: readonly string[]); } /** * Thrown by `parkTask` on an IN_PROGRESS or CLOSED task. Park is one * transition (OPEN/todo → OPEN/parked); the owner releases, or the task * is reopened, first. */ declare class TaskParkStateError extends Error { readonly taskId: string; readonly status: "IN_PROGRESS" | "CLOSED"; readonly workstream: string; readonly name = "TaskParkStateError"; constructor(taskId: string, status: "IN_PROGRESS" | "CLOSED", workstream: string); } /** * Thrown by `claimTask` on an OPEN/triage task without `force`. A triage * task is a proposal (often a review finding) nobody has accepted as * work yet; accept it, or close it rejected / duplicate. */ declare class TaskInTriageError extends Error { readonly taskId: string; readonly workstream: string; readonly name = "TaskInTriageError"; constructor(taskId: string, workstream: string); } /** * Thrown by `claimTask` on an OPEN/parked task without `force`. Parked * means "keep out of the scheduler"; claiming it anyway is explicit. */ declare class TaskParkedError extends Error { readonly taskId: string; readonly workstream: string; readonly name = "TaskParkedError"; constructor(taskId: string, workstream: string); } /** * Thrown when a verb would resolve a `human` task other than by an explicit * `mu task close`: unpark, claim (even --force) and a CLOSE block. A human * task records a decision only a person can make; nothing autonomous may * make it ready or close it. */ declare class HumanTaskError extends Error { readonly taskId: string; readonly workstream: string; readonly name = "HumanTaskError"; constructor(taskId: string, workstream: string, action: string); } /** * Thrown when a verb is asked for a (status, substate) pair that the * task_substates table does not allow, e.g. `close --as parked`. * Caught here rather than by the deferred FK, which would fail only at * commit with an opaque constraint message. */ declare class InvalidSubstateError extends Error { readonly status: TaskStatus; readonly substate: string; readonly name = "InvalidSubstateError"; constructor(status: TaskStatus, substate: string); } /** A note selector (`--from ` / `--note `) that is not one of the task's * own notes `1..total`. Exact membership: there is no fuzzy match. */ declare class TaskNoteRangeError extends Error { readonly taskId: string; readonly n: number; readonly total: number; readonly name = "TaskNoteRangeError"; constructor(taskId: string, n: number, total: number); } interface TaskEdges { /** Tasks that must close before this one can start (blockers). */ blockers: string[]; /** Tasks that this one blocks (dependents). */ dependents: string[]; } /** One end of an edge with the neighbour's current status attached. * Used by `mu task show` to group blockers/dependents into * "still gating" vs "satisfied" buckets without making the renderer * do a second round-trip to the DB per neighbour. */ interface TaskEdgeWithStatus { name: string; status: TaskStatus; substate: TaskSubstate; } interface TaskEdgesWithStatus { /** Tasks that must close before this one can start (blockers), * carrying each blocker's current status. */ blockers: TaskEdgeWithStatus[]; /** Tasks that this one blocks (dependents), carrying each * dependent's current status. */ dependents: TaskEdgeWithStatus[]; } /** * Direct (one-hop) edges for a task. For transitive prerequisites, use * `getPrerequisites()`; this helper is the immediate-neighbour view used * by `mu task show`. */ declare function getTaskEdges(db: Db, taskLocalId: string, workstream: string): TaskEdges; /** * Same one-hop edge view as `getTaskEdges`, but each neighbour is * returned as `{ name, status }` so callers can group / colour by * status without an N+1 round-trip. Used by `mu task show` to split * "blocked by" (still-gating) from "satisfied" (already-CLOSED) * blockers, and the symmetric split on the dependents side * (task_show_blocked_by_renders_closed). OPEN and IN_PROGRESS stay in * the still-gating bucket; CLOSED is satisfied. */ declare function getTaskEdgesWithStatus(db: Db, taskLocalId: string, workstream: string): TaskEdgesWithStatus; /** * All tasks transitively reachable from `taskId` via reverse-edge * traversal (i.e. the set of tasks that block this one), including the * task itself. */ declare function getPrerequisites(db: Db, taskLocalId: string, workstream: string): Set; interface BlockEdgeResult { /** True iff a row was actually inserted (vs. already present). */ added: boolean; } /** * Add the edge `blocker → blocked` ('blocker blocks blocked'). * Idempotent (existing edge → `added: false`). Validates: * * - both tasks exist * - same workstream (cross-workstream edges forbidden) * - no cycle (the new edge wouldn't form a path blocked → ... → blocker) * - blocker ≠ blocked (no self-reference) */ declare function addBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): BlockEdgeResult; interface RemoveBlockEdgeResult { /** True iff a row was actually deleted (vs. no such edge). */ removed: boolean; } /** * Remove the edge `blocker → blocked`. Idempotent (no edge → * `removed: false`). Does NOT validate task existence — if the * edge is gone there's nothing to do, regardless of whether the * tasks are gone too. */ declare function removeBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): RemoveBlockEdgeResult; interface ReparentTaskResult { /** Edges removed (i.e. all incoming `to_task = taskId` edges). */ removedEdges: number; /** Edges added (after duplicate blockers are canonicalised). */ addedEdges: number; } /** * Atomically replace every incoming edge of `taskId` with new ones * `blocker[i] → taskId`. Pass an empty `blockers` array to clear all * incoming edges (the task becomes ready iff its status allows). * * Validates ALL new blockers up-front (existence + same workstream + * cycle check); if any fails, no DELETE happens — the call is fully * atomic via a single transaction. * * Cycle reasoning: removing the existing incoming edges to `taskId` * doesn't change `taskId`'s OUTGOING reachability, so * `wouldCreateCycle(db, blocker, taskId)` evaluated against the * pre-state gives the right answer for each new edge. */ declare function reparentTask(db: Db, taskLocalId: string, blockers: readonly string[], scope: { workstream: string; }): ReparentTaskResult; /** Thrown when a title has no ASCII letter or digit, so no id can be * derived from it (any non-ASCII script, or pure punctuation). Exit 2 * (usage) with the verb's --help; the fix is the `` positional. * Lives here, not in errors.ts, because errors.ts imports this file. */ declare class TaskTitleSlugEmptyError extends Error { readonly title: string; readonly name = "TaskTitleSlugEmptyError"; constructor(title: string); } declare function isValidTaskId(id: string): boolean; /** * Lowercase title; collapse non-alnum runs into single `_`; trim * leading/trailing `_`; prefix `t_` if the result starts with a digit * (schema requires first char letter); apply the soft cap with * word-boundary trim (cut at the last `_` at-or-before SLUG_SOFT_CAP * when one exists, else hard-truncate). Mirrors `tg`'s `id_from_title` * but adds the soft cap. * * Throws `TaskTitleSlugEmptyError` if `title` yields an empty slug. */ declare function slugifyTitle(title: string): string; /** * Result of `slugifyTitleVerbose`: the slug plus enough metadata for * the CLI to decide whether to warn the user that meaning was lost. * * slug — the same string `slugifyTitle` returns. * strippedLength — length of the post-strip pre-cap slug. When this * exceeds the SLUG_SOFT_CAP the verbose form had to * cut at a word boundary (or hard-truncate); the * cut clauses are gone with no in-band signal. * originalSlug — what the slug WOULD have been without the * SLUG_SOFT_CAP cut: full stripped slug with the * same `t_` digit-prefix correction and the same * SLUG_HARD_CAP ceiling, but no word-boundary * truncation. Equal to `slug` when nothing was * cut. The CLI surfaces this in `mu task add * --json` so scripted callers can detect the * truncation without grepping stderr. * truncated — true iff `slug.length < strippedLength` AFTER the * `t_` digit-prefix correction, i.e. real bytes were * dropped. False for any title that fits under the * soft cap or whose only diff vs the stripped slug * is the `t_` prefix. * * The CLI's `mu task add` uses `truncated` to print a one-line stderr * hint pointing at the `` positional override and (under --json) * to surface `originalSlug` alongside `truncated:true` * (slugifytitle_silently_drops_clauses; task_add_slugify_silently_truncates_ids). */ interface SlugifyResult { slug: string; strippedLength: number; originalSlug: string; truncated: boolean; } /** * Verbose sibling of `slugifyTitle`: returns the slug AND a * `truncated` flag so the CLI can hint to the user when the soft cap * dropped clauses (the meaning-shift hazard documented in * slugifytitle_silently_drops_clauses). * * Algorithm is byte-for-byte identical to `slugifyTitle`; this just * surfaces the metadata that the plain form throws away. */ declare function slugifyTitleVerbose(title: string): SlugifyResult; /** * Generate a unique task id from a title. v5: tasks.local_id is * per-workstream unique, so the collision check scopes to one * workstream. On collision, appends `_2`, `_3`, … until unique. */ declare function idFromTitle(db: Db, workstream: string, title: string): string; /** * Result of `idFromTitleVerbose`: the unique-in-workstream id plus the * truncated flag from the underlying slugify pass. Used by `mu task * add` to decide whether to surface the stderr hint about lost clauses * (slugifytitle_silently_drops_clauses) and to surface the un-truncated * slug in `--json` (task_add_slugify_silently_truncates_ids). * * id — the unique-in-workstream task id. * truncated — true iff the underlying slugify pass cut real * characters (collision-suffixing does NOT flip * this). * originalSlug — what the slug would have been without the * SLUG_SOFT_CAP cut. Equal to `id` when nothing was * cut AND no collision suffix was appended; for * the truncation-detection use case the only thing * the CLI cares about is the lossy-vs-not * comparison surfaced via `truncated`. */ interface IdFromTitleResult { id: string; truncated: boolean; originalSlug: string; } /** * Verbose sibling of `idFromTitle`: returns the unique id, the * `truncated` flag from the slugify pass, and the un-truncated * `originalSlug` for `--json` consumers. Collision-suffixing (`_2`, * `_3`, …) does not flip `truncated` — the underlying slug's lossiness * is what the CLI hint cares about. */ declare function idFromTitleVerbose(db: Db, workstream: string, title: string, /** Ids to treat as taken besides the DB's (explicit ids of the same batch). */ reserved?: ReadonlySet): IdFromTitleResult; interface AddTaskOptions { localId: string; workstream: string; title: string; /** 1..100; enforced by schema CHECK. Omitted = 50, and unless * `effortDays` is given the task is unscored. */ impact?: number; /** > 0; enforced by schema CHECK. Omitted = 1 (see `impact`). */ effortDays?: number; /** * Tasks that block this one. Edges inserted as `blocker -> newTask`. * Each blocker must already exist AND share this task's workstream * (cross-workstream edges are forbidden); cycle check guards each * edge. The CLI surfaces this as `--blocked-by`; the SDK key matches. */ blockedBy?: string[]; /** Start as OPEN/triage (a proposal, e.g. a review finding) instead of OPEN/todo. */ triage?: boolean; /** Task kind (default `work`). `human` starts OPEN/parked, never ready. */ kind?: TaskKind; /** `tasks.wave`: the label grouping one fan-out of delegate calls. */ wave?: string; } /** * Atomically create a task and (optionally) its incoming blocked-by * edges. * * The task insert + every edge insert + cycle check happen inside one * SQLite transaction. If any blocker is missing or any edge would * create a cycle, the entire add rolls back. * * Cycle check for `addTask` is structurally trivial (a fresh task has * no outgoing edges, so `to -> ... -> from` is impossible). It's still * called here so the same primitive is exercised by tests. */ declare function addTask(db: Db, opts: AddTaskOptions): TaskRow; interface AddTasksOptions { workstream: string; /** Umbrellas the NEW tasks block (`mu task add --blocks`): edges * `newTask -> umbrella`, each umbrella same-workstream and cycle-checked. */ blocks?: readonly string[]; /** Tasks that block every NEW task (`--blocked-by`). */ blockedBy?: readonly string[]; triage?: boolean; /** Applied to every task of the batch (see AddTaskOptions). */ kind?: TaskKind; impact?: number; effortDays?: number; /** Author of every `note` (the brief); also the group's actor. */ noteAuthor?: string; } interface AddTasksResult { /** Local ids, in input order. */ ids: string[]; /** Per task, in input order: the id and, for a derived one, whether the * slug was cut (`truncated`) and its un-cut form. */ derived: IdFromTitleResult[]; /** False only for an empty batch. */ changed: boolean; } /** * Create one or more tasks, each with an optional brief (kind 'brief'), * their `blockedBy` edges and their outbound `blocks` edges, in ONE op * group and ONE transaction. A duplicate id, invalid id, missing or * cross-workstream blocker/umbrella or a cycle rolls back every row, edge * and note of the batch. * * An entry without `id` gets one derived from its title: the slug, then * `_2`, `_3`, … the first suffix free in the DB, among the batch's * explicit ids and among the ids already derived. The probe runs inside * the transaction, so it cannot race another writer. */ declare function addTasks(db: Db, input: readonly { id?: string; title: string; note?: string; }[], opts: AddTasksOptions): AddTasksResult; interface AddNoteOptions { /** Free-form author label. Convention: agent name, "user", or "orchestrator". */ author?: string; /** Workstream context (operator-facing name). v5: tasks.local_id is * per-workstream unique, so this is required to disambiguate. */ workstream: string; /** Note kind (`task_notes.kind`), set once at INSERT. Default 'note'. * `closeTask` files its reason/evidence notes as 'close'. */ kind?: NoteKind; /** 'new' (default): the note is its own op group. 'inherit': it joins * the CALLER's group, actor and intent (a verdict written inside a * delegate's record group). Honoured by `addNote`; `insertNote` always * inherits. */ group?: "new" | "inherit"; } interface AddNoteResult { author: string | null; content: string; createdAt: string; kind: NoteKind; } declare function addNote(db: Db, taskLocalId: string, content: string, opts: AddNoteOptions): AddNoteResult; interface DeleteTaskResult { /** True iff the row existed and was deleted. False on a dry-run * (preview) AND on the idempotent missing-row case. */ deleted: boolean; /** Number of `task_edges` rows cascaded out (informational). On a * dry-run, this is the would-be count. */ deletedEdges: number; /** Number of `task_notes` rows cascaded out (informational). On a * dry-run, this is the would-be count. */ deletedNotes: number; /** True iff this was a dry-run (`opts.dryRun: true`). On a * dry-run `deleted` is false and the counts are the would-be * counts; the DB is unchanged. Always false on a commit / on a * missing-row idempotent no-op. */ dryRun: boolean; /** True iff a matching task row was found at the time of the * call. Discriminator for the CLI: a dry-run that found nothing * (`present: false`) renders differently from a dry-run that * found an existing task with zero edges and zero notes * (`present: true, deletedEdges: 0, deletedNotes: 0`). */ present: boolean; /** The op group the delete wrote. Null * when nothing was deleted (dry-run or missing row). */ group: string | null; /** The task's revision when it was found (`taskRevision`): a preview * prints it, `--if ` repeats it. Absent for a missing task. */ revision?: number; } interface DeleteTaskOptions { /** The preview revision (`taskRevision`). A commit whose task changed * since throws `RevisionMismatchError` before any write; checked in the * delete's own transaction. Ignored by a dry-run and by a missing task. */ ifRevision?: number; /** When true, return the cascade preview (would-be edge / note * counts) without mutating. The CLI uses this to power the bare * `mu task delete ` two-phase pattern (mirrors * `mu workstream teardown`). A dogfood report typed * `mu task delete X --yes` (mirroring workstream teardown) and got * 'unknown option --yes' — the verb took no confirmation flag at * all. Two failed deletes left long-named tasks lingering. */ dryRun?: boolean; } /** * Delete a task. FK CASCADE on `task_edges` (from + to) and * `task_notes` cleans the joined rows automatically. Idempotent on * a missing task (returns `deleted: false`). * * Pre-counts the cascade victims for reporting because SQLite's * `changes()` only reports rows directly affected by the DELETE. * * With `opts.dryRun: true`, returns the would-be counts without * touching the DB (no mutation, so no op is written). The CLI bare * `mu task delete ` form uses this; `--yes` calls through with * `dryRun: false`. */ declare function deleteTask(db: Db, localId: string, workstream: string, opts?: DeleteTaskOptions): DeleteTaskResult; interface UpdateTaskOptions { title?: string; /** 1..100; enforced by schema CHECK. */ impact?: number; /** > 0; enforced by schema CHECK. */ effortDays?: number; } interface UpdateTaskResult { /** True iff at least one field actually changed. */ updated: boolean; /** The fields whose values differ post-update (in `UpdateTaskOptions`'s * camelCase shape). Empty when `updated: false`. */ changedFields: string[]; } /** * Update scalar fields on a task. Each option is independently optional; * passing none is a typed no-op (returns `updated: false, changedFields: []`). * Fields whose new value equals the current value are skipped (no row change). * * NOT for status (use `closeTask` / `openTask` / `setTaskStatus`), owner * (use `claimTask` / `releaseTask`), local_id (rename is deferred), or * workstream (cross-workstream moves are deferred). */ interface UpdateTaskScopeOption { workstream: string; } declare function updateTask(db: Db, localId: string, opts: UpdateTaskOptions, scope: UpdateTaskScopeOption): UpdateTaskResult; declare function getTask(db: Db, localId: string, workstream: string): TaskRow | undefined; /** * List tasks. With no `workstream` arg returns every row — used by `mu sql` * and by tests; CLI surfaces always pass a workstream so users only see * their own. */ interface ListTasksOptions { /** Filter to one or more lifecycle statuses. Omitted = all statuses. */ status?: TaskStatus | readonly TaskStatus[]; /** Filter to one or more substates (ANDed with `status`). */ substate?: TaskSubstate | readonly TaskSubstate[]; /** Only tasks whose `owner_id` is this agent (the real FK, so same-named agents never mix). */ ownerId?: number; } declare function listTasks(db: Db, workstream?: string, opts?: ListTasksOptions): TaskRow[]; /** The `ready` view of one workstream: OPEN, not parked or triage, no open blocker. */ declare function listReady(db: Db, workstream: string): TaskRow[]; declare function listBlocked(db: Db, workstream: string): TaskRow[]; declare function listGoals(db: Db, workstream: string): TaskRow[]; /** All IN_PROGRESS tasks in a workstream, most-recently-touched first. * Used by `mu state` to populate its in-progress slice; exposed as a * named SDK helper so CLI renderers don't re-derive the row-shape * conversion. */ declare function listInProgress(db: Db, workstream: string): TaskRow[]; /** CLOSED tasks in a workstream, most recently updated first, capped at * `limit` (default 5). `updated_at` is a proxy for close time: there is * no `closed_at` column, and a later note or edge change on a CLOSED * task bumps `updated_at` (touchTask), moving it up. Used by `mu state` * for its 'recent closed' * slice; exposed as a named SDK helper so the CLI no longer needs the * raw-row type that was duplicating RawTaskRow. */ declare function listRecentClosed(db: Db, workstream: string, limit?: number): TaskRow[]; /** Optional filter knobs for `listNotes`. Default-everything-undefined * preserves the historical "return every note, oldest-first" shape so * every existing caller (cmdTaskShow's notes block, agents.test.ts) * keeps working unchanged. * * Filters compose multiplicatively when both apply (`since` AND * `tail`): the timestamp filter is applied first, then `tail` slices * the last N of what survived. The CLI surface (`mu task notes * --tail / --since / --since-claim`) lives in src/cli/tasks/edit.ts; * the mutex between `--since` and `--since-claim` is a CLI concern, * not enforced here — if both arrive at the SDK, `since` wins (it's * the explicit one) and `sinceClaim` is ignored. The auto-resolve * for `sinceClaim` (look up the most recent `task.claim` op in * the ops log) happens here so the SDK is self-contained for scripted * callers. */ interface ListNotesOptions { /** Print only the last N notes (after any timestamp filter). Must * be a positive integer; a value of 0 returns no rows but is not * an error here — CLI-side validation rejects `--tail 0`. */ tail?: number; /** ISO-8601 cutoff: only notes with `created_at > since` survive. * Any `Date.parse`-able value; it is normalised to `toISOString()` * form (the stored shape) before the text comparison, so offsets * and second-precision cutoffs compare as instants. */ since?: string; /** When true and `since` is unset, look up the `created_at` of the * most recent `task claim` event for this task and use it as the * cutoff. Falls back to no filter when no claim event exists * (equivalent to `--since-beginning`). */ sinceClaim?: boolean; } /** List notes for a task. Operator-facing local_id; resolves to the * surrogate task id via taskIdFor (with optional workstream scope). * * Optional filters: see {@link ListNotesOptions}. Default behaviour * (no opts) is unchanged — every note, oldest-first. */ declare function listNotes(db: Db, taskLocalId: string, workstream: string, opts?: ListNotesOptions): TaskNoteRow[]; /** What `selectTaskNotes` returns: the notes to show (oldest first, full * content), how many were left out, and which of them are pinned. */ interface SelectedNotes { notes: TaskNoteRow[]; /** Notes of the task not in `notes`. */ more: number; /** All notes the task has. */ total: number; /** `n` of every note that is content, never cut: the brief, the report (or * the closer's fallback) and the verdicts. Everything else is ordinary. */ pinned: number[]; } /** * The default notes view, chosen from metadata alone (no content is read for * a note that is not shown): the first `brief`, the last `report`, every * `verdict`, then the last {@link ORDINARY_TAIL} ordinary notes newer than the * report. With no `report` the closer's last `note` stands in for it: the * author of the last `close` note (NULL matches NULL), so late instructions * from anyone else stay ordinary. Decided at read time; nothing is rewritten. * * `all` or `from` return exactly the notes with `n >= from` (`all` is * `from: 1`), whole. A `from` that is not one of the task's notes throws * {@link TaskNoteRangeError}. Content is never cut here: the CLI byte cap is a * rendering concern. */ declare function selectTaskNotes(db: Db, taskLocalId: string, opts: { workstream: string; all?: boolean; from?: number; }): SelectedNotes; /** * All tasks currently owned by `agent` in a given workstream * (v5: agents.name is per-workstream unique). Sorted by local_id. * * Defaults to **excluding CLOSED** since the verb's purpose is "what * is X currently working on?" and a closed task is no longer being * worked on. closeTask intentionally preserves `owner` as a * historical record (so audit/notes can attribute decisions); pass * `{ includeClosed: true }` to surface that history. */ declare function listTasksByOwner(db: Db, workstream: string, owner: string, opts?: { includeClosed?: boolean; }): TaskRow[]; /** Deepest level of a frontier read; a node past it keeps a `kids` count. */ declare const FRONTIER_MAX_DEPTH = 12; /** Most distinct tasks one frontier read lists (recurrences are capped at the same number). */ declare const FRONTIER_MAX_NODES = 2000; /** CLOSED neighbours listed under one node; the rest collapse into its `closed` count. */ declare const FRONTIER_CLOSED_PER_NODE = 3; type FrontierDirection = "blockers" | "dependents"; /** One line of the open task tree, in preorder. */ interface FrontierEntry { task: TaskRow; /** 0 for a root. */ depth: number; /** `local_id` of the entry it is listed under; null for a root. */ parent: string | null; /** Already listed above (a diamond, or a cycle sync made): not expanded again. */ again: boolean; /** Neighbours (in the read direction) that are not CLOSED and are not listed under this entry. */ kids: number; /** CLOSED neighbours collapsed into this count instead of listed. */ closed: number; /** Neighbours of any status the node cap left out (not in `kids` / `closed`). */ capCut: number; } interface OpenTaskFrontier { direction: FrontierDirection; entries: FrontierEntry[]; /** Start tasks left out by the node cap (only without a `root`). */ rootsCut: number; /** Recurrence lines (diamonds) left out by the cap; every such task is listed elsewhere. */ againCut: number; } interface OpenTaskFrontierOptions { /** Walk through CLOSED tasks too (still capped by depth and entries unless `uncapped`). */ includeClosed?: boolean; /** No cap at all: CLOSED tasks walked, every level, every task, every recurrence (`task tree --all`). */ uncapped?: boolean; /** Levels below a root to list (default and ceiling `FRONTIER_MAX_DEPTH`). */ maxDepth?: number; } /** * The open part of the DAG, bounded: from `root` (always expanded, even when * CLOSED) or, without one, from every non-CLOSED task that has no non-CLOSED * predecessor; `direction` is the way to walk (dependents = what a task * blocks). A non-CLOSED task expands; a CLOSED one is a leaf, and only the * first `FRONTIER_CLOSED_PER_NODE` of a node's CLOSED neighbours are listed * (the rest are counted), so one umbrella with 870 closed children costs a * few lines. Depth is capped at `FRONTIER_MAX_DEPTH` and entries at * `FRONTIER_MAX_NODES`; what a cap leaves out is counted in `kids`. Diamonds * and cycles list a task once and mark later sightings `again`. One SQL read * per level (never per node), same-workstream edges only. */ declare function loadOpenTaskFrontier(db: Db, workstream: string, root?: string, direction?: FrontierDirection, opts?: OpenTaskFrontierOptions): OpenTaskFrontier; /** One ASCII tree line per frontier entry: the branch drawing, then `label(entry)`. */ declare function renderFrontierLines(entries: readonly FrontierEntry[], label: (entry: FrontierEntry) => string): string[]; /** One agent as `--json` emits it. */ interface AgentOutput { /** `agents.id`: the registry incarnation. */ id: number; name: string; workstream_id: number; workstream: string; cli: string; pane_id: string; role: string; tab: string | null; cwd: string | null; base: string | null; command: string | null; command_source: string | null; spawner: string | null; host: string | null; collected_head: string | null; repo: string | null; gate_token: string | null; created_at: string; updated_at: string; state?: string; source?: string; ctl?: string; ctl_socket?: string; since?: string | null; reason?: string; idle?: boolean; trust_dialog?: boolean; } type LogKind = "message" | "event" | "broadcast" | string; interface LogRow { /** Monotonic AUTOINCREMENT id. Use as the cursor for `--since`. */ seq: number; /** Workstream this entry belongs to, or `null` for machine-wide. */ workstreamName: string | null; /** Free TEXT: agent name, "system", "user", or anything a caller picks. * Captured ops that ran outside any actor context have no actor, and * render as "system" rather than the string "null". */ source: string; /** Structured intent ('task.close', 'agent.spawn', ...). Null only for * operator-authored prose lines (`mu log ""` / a `--kind` ledger), * which name no state change. The formatter in src/log-render.ts * renders from this — never from the payload text. */ intent: string | null; /** Op group: every op of one operator action shares it. Surfaced so * `mu log` can print it and `mu log --group ` can filter to it. */ group: string; /** 'put' (semantic partial update) or 'del' (tombstone). The formatter * needs it to tell "edge removed" from "row touched". */ op: string; /** Free TEXT: "message" (default), "event" (auto state changes), * "broadcast" (explicit cross-agent), or any caller-defined value. */ kind: LogKind; /** Free utf-8 string. May be JSON if the kind suggests structure. */ payload: string; /** ISO 8601 timestamp set at insert time. */ createdAt: string; } /** Raised when a group-id prefix matches more than one group. * Astronomically unlikely with uuids, but silently picking one of two * candidate groups is not an acceptable failure mode. */ declare class GroupIdAmbiguousError extends Error { readonly prefix: string; readonly candidates: readonly string[]; constructor(prefix: string, candidates: readonly string[]); } /** Raised when a group-id prefix matches no group. Exit 3. */ declare class GroupNotFoundError extends Error { readonly prefix: string; constructor(prefix: string); } interface AppendLogOptions { /** Workstream this entry belongs to. `null` for machine-wide. */ workstream: string | null; /** Who emitted this. Agent name, "system", "user", or arbitrary. */ source: string; /** Defaults to "message". */ kind?: LogKind; /** Free utf-8. Multi-line allowed. */ payload: string; /** Structured intent. Set by `emitEvent` for the local-only changes * no trigger can see. Stays null for operator-authored `mu log * write` lines, which are prose by nature and have no state change * to name. */ intent?: string; } /** Raised by `appendLog` for a kind in RESERVED_LOG_KINDS. Usage lane * (exit 2): the operator picked a tag that names a projectable entity. */ declare class LogKindReservedError extends Error { readonly kind: string; readonly name = "LogKindReservedError"; constructor(kind: string); } /** * Append a log entry. Returns the inserted row (with assigned `seq`). * Constant-time. Safe to call from any state-changing verb, inside or * outside a transaction (it nests as a savepoint). * * The HLC mint and the INSERT share one BEGIN IMMEDIATE transaction so * ops commit in hlc order: flushSegment publishes `hlc > lastPublished`, * and an op minted earlier but committed after a later one was flushed * would never be published. * * A JSON payload with line breaks is stored minified. A segment line * holds one op on one physical line and passes valid JSON through * verbatim, and a raw CR/LF in JSON can only be insignificant * whitespace, so nothing is lost. */ declare function appendLog(db: Db, opts: AppendLogOptions): LogRow; interface ListLogsOptions { /** Filter by workstream. `undefined` = every workstream + machine-wide. * `null` = ONLY machine-wide entries. */ workstream?: string | null; /** Strictly > this seq. Use to resume a tail. */ since?: number; /** At most this seq (with `since`: the exact range `since+1..upTo`). */ upTo?: number; /** Cap the result. With `since`, returns the FIRST N matching (oldest * first). Without `since`, returns the LAST N (most recent), * re-sorted oldest-first. */ limit?: number; source?: string; /** Filter by `ops.entity` (`kind` on a log row). */ kind?: string; /** Filter by structured `ops.intent`, e.g. 'task.close'. */ intent?: string; /** Filter to one op group (`ops.group_id`). */ group?: string; } /** * List log entries. Always returns oldest-first. Use `since` for * cursor-based reads (the canonical tail pattern); use `limit` alone * for "show me the most recent N" reads. */ declare function listLogs(db: Db, opts?: ListLogsOptions): LogRow[]; /** * Return the latest seq currently in the table (or 0 if empty). Used * by `mu log --tail` to start the cursor at "now" so the subscriber * only sees NEW entries unless they explicitly pass `--since 0`. */ declare function latestSeq(db: Db, workstream?: string): number; /** * Record a change that NO capture trigger can see. * * The triggers in src/capture.ts cover the four **portable** tables * (workstreams, tasks, task_edges, task_notes), so every task and * workstream mutation is already an op with a real `intent` and a real * natural key. Anything that mutates one of those tables must NOT call * this: it would write a second, prose, intent-less copy of a change the * trigger already captured with a real intent. * * What legitimately remains is state that lives OUTSIDE those tables: * * agent.* spawn / close / free / adopt / kick — `agents` is * machine-local (it holds `pane_id`), so there is no * trigger and never will be. The `worktrees` registry is * machine-local too (absolute paths); its changes ride the * agent.spawn / agent.close lines. * agent.stall a pure observation; nothing is mutated. * * `intent` is REQUIRED (and typed), not optional, because the whole * point is that these rows render through the same formatter as * captured ops. An intent-less op cannot be rendered without * prefix-matching prose, which is the brittleness * (`classifyEventVerb`, `CLAIM_EVENT_PREFIX`) the ops log deletes. * * These entities are deliberately NOT in SYNCED_ENTITIES: every one of * them describes something about THIS machine (a pane id, a filesystem * path) that is meaningless on a peer. They are still recorded, so * `mu log` and the TUI show them locally. */ declare function emitEvent(db: Db, workstream: string | null, intent: LocalIntent, payload: string, source?: string): void; /** * Intents for changes no trigger can capture. Closed union rather than * `string` so a typo is a compile error and the set stays auditable — * `mu log`'s formatter (src/log-render.ts) switches on exactly these. */ type LocalIntent = "agent.spawn" | "agent.spawn_failed" | "agent.close" | "agent.adopt" | "agent.kick" | "agent.send" | "agent.abort" | "agent.stall"; /** One op as `--json` and `--tail` print it: the `ops` column names plus the formatter's prose. */ interface LogItem { seq: number; /** `ops.key`; '' for a machine-wide entry. */ key: string; /** `ops.actor` ('system' where the op had none). */ actor: string; intent: string | null; group_id: string; op: string; /** `ops.entity` (the `--kind` tag). */ entity: string; payload: string; created_at: string; /** The one formatter's prose for this op. */ rendered: string; /** Characters clipped from this item's text fields; absent when none. */ cut?: number; } /** A follow-up plan: `seq` runs in order and is never truncated; `alt` offers alternatives. */ type NextPlan = { mode: "seq" | "alt"; commands: string[]; } | null; /** A running tool of a pi agent (from the ctl status). */ interface AgentTool { name: string; /** Epoch ms the tool started. */ since: number; /** Calculated at read time (`now - since`), when known. */ elapsed_ms?: number; } interface DashboardWorkstream { name: string; scratch: boolean; /** Newest visible op seq in the workstream; 0 when it has none, or when `activity: false` skipped the read. */ latest_seq: number; } /** One task, SQL column names, `key` = `/` (unique across workstreams). */ interface DashboardTask { key: string; workstream: string; id: string; title: string; status: string; substate: string; impact: number; effort_days: number; /** Owner's name; only while IN_PROGRESS. */ owner: string | null; /** The owner's registry id (`tasks.owner_id`; the identity: names repeat across workstreams). */ owner_id: number | null; /** The owner's OWN workstream, which can differ from the task's; null when unowned. */ owner_workstream: string | null; kind: string; wave: string | null; created_at: string; updated_at: string; /** `MAX(ops.seq)` over the task's own ops, its notes' and its edges' (raw log, db/sql writes included); 0 when none. */ activity_seq: number; /** The call behind a `kind=delegate` task, whoever made it; absent for any other task. */ call?: DashboardCall; } /** A delegate call as a task row shows it: machine-wide, never narrowed to a caller. */ interface DashboardCall { call_id: string; state: string; /** The parsed label of a done answer; null otherwise. */ verdict: string | null; /** `/` the call judges; null with no record target. */ judges: string | null; wave: string | null; } /** * One wave of delegate calls in a workstream, tallied as `verdictTally` counts it * (one result per call), over every caller. `pending` includes `refused`. */ interface DashboardWave { /** `/`. */ key: string; workstream: string; wave: string; calls: number; confirmed: number; refuted: number; unverified: number; pending: number; refused: number; /** The newest `updated_at` of its calls. */ updated_at: string; } /** One line of the open DAG (`loadOpenTaskFrontier`, preorder, per workstream). */ interface DashboardEdge { key: string; workstream: string; id: string; title: string; status: string; substate: string; depth: number; /** Qualified key of the entry it is listed under; null for a root. */ parent: string | null; /** Already listed above (a diamond): not expanded again. */ again: boolean; /** Open neighbours not listed under this entry. */ kids: number; /** CLOSED neighbours collapsed into this count. */ closed: number; cap_cut: number; } /** What the frontier cap left out of one workstream's DAG (per-entry `cap_cut` cannot say it). */ interface DashboardFrontierCut { workstream: string; /** Open start tasks the node cap left out: open work absent from `edges`. */ roots_cut: number; /** Recurrence (diamond) lines the cap left out; each such task is listed elsewhere. */ again_cut: number; } /** A registry agent with its computed state; unknown stays unknown. */ interface DashboardAgent extends AgentOutput { /** `/`. */ key: string; /** Qualified keys of the IN_PROGRESS tasks it owns (empty when none). */ tasks: string[]; /** The first of `tasks`; null when none. */ task: string | null; /** The running tool; null = none running; absent = unknown (old extension, no ctl): never read as "none". */ tool?: AgentTool | null; } /** An op of the canonical log, rendered by the one formatter (`logItem`), plus its workstream. */ interface DashboardLogItem extends LogItem { /** The workstream the op belongs to; null for a machine-wide op. */ scope: string | null; } /** An OPEN `human` task of the caller. */ interface DashboardHumanNeed { key: string; workstream: string; id: string; reason: string; } /** An unfinished delegate call of the caller. */ interface DashboardDelegate { call_id: string; workstream: string; /** The delegate task's local id. */ task: string; state: string; wave: string | null; judges: string | null; agent: string | null; verdict: string | null; error: string | null; updated_at: string; } /** * Something a human has to look at. Read over EVERY live workstream and blind * to the status/text filters and the workstream scope, so each surface (the * strip under the tabs, the Overview card) agrees. * * - `dead`: the agent's pane (or gate process) is gone. * - `permission`: `needs_permission`; `text` carries how long. * - `stalled`: one tool has run over `STALLED_TOOL_MS`. * - `input`: `needs_input` past the idle threshold on an agent that owns work * (the registry's `idle` flag; `MU_IDLE_THRESHOLD_MS`, 0 disables). * - `delegate`: a delegate call that is `refused` (parked until its pi session * reissues it) or `failed` within `FAILED_CALL_MS`, whoever made it. * - `human`: an OPEN `kind=human` task, whoever recorded it (`human_needs` * is caller-scoped and empty for a human shell). * - `next_unavailable`: `computeNext` failed. * - `sync`: a warning the caller passed as `syncWarnings`. */ type AttentionKind = "dead" | "permission" | "input" | "stalled" | "delegate" | "human" | "next_unavailable" | "sync"; interface DashboardAttention { /** Unique within a snapshot (`:`). */ key: string; /** The workstream it belongs to; null for a machine-wide item (`next_unavailable`, `sync`). */ workstream: string | null; kind: AttentionKind; /** One short line, age at read ("worker-3 permission 20m"; a human task: its title); bare names, the UI qualifies. */ text: string; /** Qualified key to drill into: the agent (dead/permission/input/stalled) or the task (delegate, human); null otherwise. */ target: string | null; } interface DashboardSnapshot { /** The identity the caller-scoped panels were read for; null = a human shell. */ caller: string | null; /** `none`: no identity, so `human_needs` and `delegates` are empty BY DESIGN (never another caller's); `call`, `waves` and attention are every caller's. */ caller_scope: "caller" | "none"; /** Every live workstream, scratch included; never narrowed by a filter (it is the switcher). */ workstreams: DashboardWorkstream[]; tasks: DashboardTask[]; edges: DashboardEdge[]; /** One per in-scope workstream: the frontier's root/recurrence cuts, passed through. */ frontier: DashboardFrontierCut[]; agents: DashboardAgent[]; /** The newest ops, oldest first. */ log: DashboardLogItem[]; /** `computeNext` for the caller set and the workstream filter; null when `next_source` is `unavailable`, or computed and nothing is authorised. */ next: NextPlan; /** `unavailable`: the computation failed; the UI must not read null as "no work". */ next_source: "unavailable" | "computed"; human_needs: DashboardHumanNeed[]; delegates: DashboardDelegate[]; /** Per read workstream (tasks scope), its `WAVE_MAX` most recently updated waves, newest first, any caller. */ waves: DashboardWave[]; /** Trouble across every workstream, filters ignored, most urgent first (see `AttentionKind`). */ attention: DashboardAttention[]; } interface DashboardOptions { /** Narrow tasks, frontier, agents, log and caller panels to one live workstream. */ workstream?: string; /** * Read tasks, edges and frontier of this workstream ONLY (a name that is not live, * or null: none), leaving agents, log, caller panels and attention machine-wide: * the dashboard shows one workstream, and reading every other one's tasks and DAG * each tick is what made it costly. Absent: every live workstream. `workstream`, when * set, wins. */ scopeWorkstream?: string | null; /** Statuses to list; default OPEN + IN_PROGRESS (CLOSED is hidden). */ status?: readonly TaskStatus[]; /** Case-insensitive substring over a task's key/title/owner/substate and an agent's key/name/state/task keys. */ text?: string; /** `spawnerFromEnv()`; null = a human shell (caller-scoped panels stay empty). */ caller: string | null; /** Who `next` is computed for (`currentCallers()`); default: `caller` alone, none for a human shell. */ callers?: readonly string[]; /** Log tail size (default 100). */ logLimit?: number; /** Read the log tail of this workstream only, leaving every other panel unscoped (not checked: a name that is not live reads an empty log). */ logWorkstream?: string; /** * Read each workstream's `latest_seq` (default true). It only ranks the starting * pick and costs one log scan per workstream, so the dashboard's poll passes false * once the set of live workstreams is known (every `latest_seq` is then 0). */ activity?: boolean; /** Sync warnings the caller holds (`ambientIngest`); each becomes a `sync` attention item. */ syncWarnings?: readonly string[]; /** * The agent-state cache (the dashboard's slow tier, see `readLiveAgents`): with * `probe: false` the read is SQL only for every cached agent. Absent: probe all. */ agentStates?: { cache: Map; probe: boolean; }; } /** A tool running this long counts as stalled. */ declare const STALLED_TOOL_MS: number; /** * Load the dashboard's whole read model in one pass. Read-only (see the file * header); async only for the ctl probes. Throws `WorkstreamNotFoundError` for * a `workstream` filter that is not a live workstream. */ declare function loadDashboard(db: Db, opts: DashboardOptions): Promise; type DoctorStatus = "ok" | "warn" | "fail"; interface DoctorCheck { /** Short, stable identifier — the row label. */ name: string; status: DoctorStatus; /** Free-form prose for the row's right-hand column. */ detail: string; } /** One field-level divergence between the live tables and the rebuild. */ interface DriftRecord { /** Portable table the divergence is in. */ table: string; /** NATURAL key of the row (never a surrogate id), so the report is * meaningful to an operator and stable across machines. */ key: string; /** Field that differs, or a marker for whole-row presence: * '' means the row exists on one side only. */ field: string; /** Value in the LIVE tables — what mu is currently showing. */ live: string | null; /** Value the ops log says it should be. The log is canonical. */ expected: string | null; /** Which side is missing the row entirely, when field is ''. */ presence?: "missing-in-live" | "missing-in-rebuild"; } interface DriftReport { /** True iff the projection matches the log exactly. */ clean: boolean; /** Every divergence found, capped (see DRIFT_REPORT_CAP). */ records: readonly DriftRecord[]; /** Total divergences found, which may exceed records.length. */ totalDrift: number; /** Rows compared, per table. Context for "clean" — a clean report on * an empty DB proves less than one on 900 rows. */ rowsCompared: Record; /** Wall-clock cost, so `mu doctor --deep` can be honest about it. */ elapsedMs: number; } /** Cap on reported records. A systematic capture bug can diverge every * row; printing 900 lines buries the signal. The count is always exact * (`totalDrift`) even when the list is truncated. */ declare const DRIFT_REPORT_CAP = 20; /** Result of the cheap invariant that runs in the DEFAULT doctor. */ interface CheapDriftReport { clean: boolean; /** Live rows with NO op naming their key — i.e. rows whose existence * the log cannot explain. */ unexplainedRows: readonly { table: string; key: string; }[]; totalUnexplained: number; elapsedMs: number; } /** * Every live row must have at least one op naming its natural key. * * Four indexed NOT EXISTS scans, no rebuild, no temp file: ~10 ms on a * 2.3k-task, 10.7k-note, 67k-op DB. Cheap enough to run on every `mu * doctor` and the TUI slow tick. * * WHAT IT CATCHES: a row that exists with no history — an uncaptured * INSERT, or a mutation path that bypassed the triggers entirely. * * WHAT IT CANNOT CATCH, by construction: an uncaptured UPDATE. The key * still has ops from the original insert, so the invariant holds while * the field value has silently diverged. Verified empirically. Only the * full rebuild-diff finds that, which is exactly why `--deep` exists and * why the default run points at it rather than claiming to be a proof. */ declare function checkCheapDriftInvariant(db: Db): CheapDriftReport; /** * Rebuild the log into a temp DB and diff it against the live tables. * * Any divergence is a bug in capture (a mutation that left no op) or in * apply (an op that does not reproduce its mutation). The log is * canonical, so `expected` is always the rebuilt value. * * SYNCHRONOUS, matching rebuildInto / applyOp: the op context is a * per-connection temp table, so interleaved async scopes would clobber * each other's capture-suppression flag. * * Cleans up its temp directory even on throw — a doctor run that leaked * a DB copy per invocation into /tmp would be its own bug. */ declare function checkDrift(db: Db): DriftReport; /** One-line-per-record rendering shared by the CLI and the TUI popup, so * the wording of a drift report lives in exactly one place. */ declare function formatDriftRecord(record: DriftRecord): string; /** What an operator should DO about drift. Detection without remediation * is just an alarm; this is the part that matters at 3am. * * Deliberately does NOT tell them to swap the rebuild in blindly. Drift * means one of the two sides is wrong and we cannot know which from * here: if capture missed a mutation, the LIVE tables hold the truth and * the log is incomplete, so rebuilding would DISCARD real work. If apply * is lossy, the log is right. So the guidance is: capture the evidence, * then choose deliberately. */ declare function driftRemediation(): readonly string[]; /** * Thrown by `mu doctor` when drift is found, so the verb exits non-zero * and a CI job or a wrapper script notices. * * Drift is not operator error and not a transient condition: it means the * ops log and the tables disagree, which is a capture or apply BUG. An * exit code is how that reaches automation; the printed report is how it * reaches a human. */ declare class DriftDetectedError extends Error { readonly totalDrift: number; readonly records: readonly DriftRecord[]; constructor(totalDrift: number, records: readonly DriftRecord[]); } /** Sub-directory of the state dir holding lock directories. */ declare function locksDir(): string; interface FileLockOptions { acquireTimeoutMs?: number; staleLockMs?: number; /** Env var consulted for the acquire timeout, if any. */ timeoutEnvVar?: string; /** Run instead of `fn` when the lock cannot be taken (contended past * the deadline, or the lock dir cannot be created). Absent, `fn` runs * unlocked. */ onUnavailable?: () => T; /** Strict ownership (see the header): an aged lock is reclaimed only * when the process that took it is gone (pid probe on this host), never * because it is old, and only under an exclusive claim. A holder doing * slow work (git cleanup, a pane kill) keeps its lock for as long as it * lives. Anything unreadable or failing is unavailable. Pair it with * `onUnavailable` to fail closed. */ strict?: boolean; } /** * Run `fn` while holding the lock directory at `lockPath`. * * Releases in a `finally`, so a throwing `fn` never leaks the lock. * Records pid + acquisition time in `meta.json` for stale-lock * diagnostics. */ declare function withFileLock(lockPath: string, label: string, fn: () => Promise, opts?: FileLockOptions): Promise; /** Read a held lock's metadata, or null. Exposed for tests. */ declare function readFileLockMeta(lockPath: string): Promise<{ pid: number; label: string; acquiredAt: string; token?: string; } | null>; type HazardSeverity = "ok" | "warn" | "fail"; interface FleetHazard { /** Stable token, used as the doctor row label. */ name: string; severity: HazardSeverity; /** One-line summary for the row. */ detail: string; /** Multi-line explanation + remediation, shown when non-ok. */ remediation?: readonly string[]; } /** * `mu doctor` found at least one `fail`-severity row. Thrown after the * whole report is printed (or the --json payload emitted) so scripts and * CI see a non-zero exit; maps to exit 5 like drift, because a `fail` * row means the substrate is unsafe, not that a hint is pending. */ declare class HazardFailError extends Error { readonly failedRows: readonly string[]; readonly name = "HazardFailError"; constructor(failedRows: readonly string[]); } /** * Raised by `handle()` before the DB is opened when it would live inside * MU_SYNC_DIR. Exit 4 (conflict): the two settings contradict each other * and only the operator can pick which one moves. Nothing was written. * `mu doctor` still reports the same condition as a `db-vs-sync` row. */ declare class DbInsideSyncDirError extends Error { readonly dbPath: string; readonly syncDir: string; readonly name = "DbInsideSyncDirError"; constructor(dbPath: string, syncDir: string); } /** * True iff `child` is inside `parent` (or is `parent`). * * Both sides are canonicalised (symlinks resolved through the nearest * existing ancestor) and the check stays path-based: it must fire even * when the DB file does not exist yet, so it cannot rely on inode * identity. A separator is appended so `/sync-data` does not read as the * parent of `/sync-database`. */ declare function isPathInside(child: string, parent: string): boolean; /** * THE footgun of the whole sync design, and the reason it is `fail` rather * than `warn`. * * Sync tools (Syncthing, Dropbox, iCloud, rsync loops) copy files * whole-file and out of order. A live SQLite DB in WAL mode is THREE * files — `mu.db`, `mu.db-wal`, `mu.db-shm` — whose mutual consistency is * the entire basis of durability. A sync daemon that copies the main file * while the WAL is mid-checkpoint, or that resurrects a stale `-wal` from * another machine, produces a DB that opens fine and is silently corrupt. * Two machines writing the same synced file is worse still: last-writer * wins on the FILE, so an entire machine's history vanishes. * * mu's sync design specifically avoids this by shipping append-only * per-machine SEGMENTS (one writer per file, never contended) rather than * the DB itself. Putting the DB inside the sync dir defeats that on * purpose-built-to-be-safe transport, so it is a hard failure. */ declare function checkDbInsideSyncDir(dbPath: string, syncDir: string | undefined): FleetHazard; /** Result of a filesystem probe. `unknown` is a first-class outcome: * telling the operator "cannot determine" is honest, whereas claiming * "ok" on a platform we cannot inspect would be a false assurance. */ interface FsProbe { kind: "local" | "network" | "unknown"; /** Human label for the detected fs, when known. */ label: string; } /** * Classify the filesystem a path lives on. * * PORTABILITY, and why this degrades rather than guesses: * * Linux — `statfsSync().type` is a documented magic number, so the * classification is exact. * macOS — `f_type` is a small driver INDEX, not a stable magic, so * the same number means different things across releases. * Comparing it would produce confident nonsense, so we do * not; macOS falls through to `unknown`. * Other — `unknown`. * * Exported separately from the check so it can be unit-tested against * SYNTHETIC input: mounting NFS in a test suite is not feasible, so the * tests exercise `classifyFsType` (pure, takes the magic number) rather * than the syscall. Stated plainly because an untested detector is worse * than no detector. */ declare function classifyFsType(magic: number, platform?: string): FsProbe; /** Probe the real filesystem for a path, falling back to `unknown` when * the syscall is unavailable or the path does not exist yet. */ declare function probeFilesystem(path: string): FsProbe; /** * WARN rather than fail on a network mount. * * Warn, not fail, because it is not always fatal: a single machine using * an NFS home with no concurrent access often works, and refusing * outright would lock such an operator out of their own tool. But it IS * the second-most-common corruption cause after (a), because WAL needs * both advisory locking and a shared-memory file, and NFS/SMB/sshfs * deliver neither reliably. Symptoms are 'database is locked' under no * contention, or silent corruption under real contention. */ declare function checkNetworkMount(dbPath: string, probe?: FsProbe): FleetHazard; interface CaseCollision { /** The names, as stored, that fold to the same lowercase form. */ names: readonly string[]; /** The shared case-folded form. */ folded: string; } /** * Workstream names differing only by case. * * On ext4 these coexist happily. On APFS (macOS default, * case-INSENSITIVE though case-preserving) and on NTFS they collide, so * the same fleet reaches different states depending on which machine * applies an op first. Concretely: `workstreams.name` is UNIQUE and IS * the tmux session name, so on a Mac 'Foo' and 'foo' are one session and * one directory but two DB rows — and every worktree path derived from * the name aliases onto one directory. * * Detected in the DB rather than at write time on purpose: the rows may * have been created on Linux and only become a hazard when the fleet * gains a Mac, so this must be a standing check rather than a validation. * * SQL-side `LOWER()` is ASCII-only in SQLite, which is the right * comparison here: workstream names are already constrained to * `[a-z0-9_-]` by `isValidWorkstreamName`, so any collision within the * legal charset is ASCII. Names that predate the rule (or were inserted * via `mu sql`) still get caught because we fold in JS too. */ declare function findCaseCollisions(db: Db): CaseCollision[]; declare function checkCaseCollisions(db: Db): FleetHazard; /** * Run all three mixed-fleet checks. Cheap enough for the default doctor: * two path/string comparisons, one statfs, one indexed table scan. */ declare function checkFleetHazards(db: Db, opts?: { dbPath: string; syncDir?: string | undefined; }): FleetHazard[]; declare const AGENT_STATE_GLYPH: Record; declare function agentStateGlyph(state: RuntimeState): string; /** * Non-agent state glyphs, keyed by what they MEAN rather than what * they look like. A call site asks for `GLYPH.blocked`, never for a * chain — so re-pointing the chain is a one-line edit here. * * `ok` / `fail` / `warn` deliberately reuse the same codepoints as the * agent statuses they rhyme with (`free`, `terminated`): a check-circle * means "fine" whether the row is an agent, a worktree or a doctor * check. */ declare const GLYPH: { /** Healthy, clean, passing, closed-successfully. */ readonly ok: ""; /** Failed check. */ readonly fail: ""; /** Needs attention but not broken (idle-but-assigned, stale warning). */ readonly warn: ""; /** Task is blocked by an incoming edge. */ readonly blocked: ""; /** Task is parked (OPEN/parked): open, but set aside on purpose. */ readonly parked: ""; /** Track whose roots merged (diamond dependency). */ readonly merge: ""; /** Agent owns more than one task (pane-title multi-count slot). */ readonly multi: ""; /** Filter toggle: enabled / disabled. */ readonly on: ""; readonly off: ""; /** Status outside the known enum. */ readonly unknown: ""; }; /** A parsed HLC. Never construct by hand — use `parseHlc` or `formatHlc`. */ interface Hlc { /** Wall-clock hint, milliseconds since the epoch. */ wallMs: number; /** Logical counter; breaks ties within one `wallMs`. */ counter: number; /** The machine that minted this HLC (`machine_identity.machine_id`). */ machineId: string; } /** Thrown when a TEXT value is not a well-formed HLC. */ declare class HlcParseError extends Error { readonly value: string; constructor(value: string); } /** Thrown when a field would not fit its fixed width. A counter * overflow means >1e6 ops landed in one millisecond; wrapping would * silently regress causal order, so we fail loudly instead. */ declare class HlcOverflowError extends Error { readonly field: "wall" | "counter"; readonly value: number; constructor(field: "wall" | "counter", value: number); } /** Thrown when the singleton `machine_identity` row is missing. */ declare class MachineIdentityMissingError extends Error { constructor(); } /** Serialize `(wall_ms, counter, machine_id)` to the sortable TEXT form. * See the module comment for the exact shape and an example. */ declare function formatHlc(hlc: Hlc): string; /** Inverse of `formatHlc`. Throws `HlcParseError` on anything else — * including the legacy placeholder `|` shape. */ declare function parseHlc(value: string): Hlc; /** Total order over serialized HLCs: -1 / 0 / 1. Identical to bytewise * string comparison (that is the whole point of the format), so * `ORDER BY hlc` in SQL and `.sort(compareHlc)` in JS agree. */ declare function compareHlc(a: string, b: string): -1 | 0 | 1; /** * Mint the next HLC for this machine and persist the advance. * * now = wall clock ms * if now > last_wall: wall = now, counter = 0 * else: wall = last_wall, counter = last_counter + 1 * * The `else` branch is the monotonicity guarantee: when the clock * stalls, jumps backwards, or two ops land in the same millisecond, the * counter carries the order instead. * * ATOMICITY — a SINGLE `UPDATE … RETURNING` statement, not an explicit * transaction. SQLite makes one statement atomic and takes the write * lock for its duration, so the read-modify-write cannot interleave * with a competing `mu` process; `busy_timeout = 5000` (set by openDb) * makes the loser wait rather than fail. A read-then-write pair inside * BEGIN DEFERRED would be upgrade-deadlock prone under the parallel * spawn fan-out, and BEGIN IMMEDIATE would be a strictly bigger lock * for the same effect. `receiveHlc` cannot use this trick (its * three-way max is not expressible as one clean statement) so it does * take BEGIN IMMEDIATE. * * @param now Injectable clock, for tests. Defaults to `Date.now()`. */ declare function nextHlc(db: Db, now?: number): string; /** * Advance the local clock past a peer's HLC while INGESTING their op, * and return the local HLC that now dominates it. This is what makes * "laptop edits after seeing the devserver's op" order correctly: the * laptop's next mint is guaranteed greater than anything it has seen, * even if its own wall clock is days behind. * * wall = max(local_wall, remote_wall, now) * * The counter has four explicit cases, by which of the three won: * - `now` strictly won -> counter = 0 (fresh millisecond) * - local and remote tie at max -> counter = max(local_c, remote_c) + 1 * - only local is at max -> counter = local_c + 1 * - only remote is at max -> counter = remote_c + 1 * * A remote HLC from the PAST therefore never drags the local clock * backwards — `max` keeps `local_wall`, and the counter still steps. * * ATOMICITY — BEGIN IMMEDIATE (`.immediate()`), because the three-way * max needs the old row in JS before the new value can be computed, so * unlike `nextHlc` it genuinely is a read-then-write pair. * * @param now Injectable clock, for tests. Defaults to `Date.now()`. */ declare function receiveHlc(db: Db, remoteHlc: string, now?: number): string; /** Every intent mu writes: the local ones (`emitEvent`, for changes no * trigger can see) plus the capture-trigger ones (set via * `withOpContext` around a portable-table mutation). */ type CaptureIntent = "task.add" | "task.update" | "task.note" | "task.delete" | "task.close" | "task.open" | "task.park" | "task.unpark" | "task.accept" | "task.reject" | "task.defer" | "task.claim" | "task.release" | "task.reap" | "task.block" | "task.unblock" | "task.reparent" | "workstream.init" | "workstream.teardown" | "close.apply" | "sql.write"; type KnownIntent = CaptureIntent | LocalIntent; /** A log row, reduced to what rendering needs. Structural so both * `LogRow` (the SDK shape) and raw op rows satisfy it. */ interface RenderableOp { intent: string | null; /** `ops.entity`. */ kind: string; /** `ops.key` — the natural key, verbatim. */ workstreamName: string | null; payload: string; /** `ops.actor`. */ source: string; op?: string; } /** Rendered op, split so callers can colour the verb independently. * `subject` is the entity the line is about; `detail` is the * human-readable consequence. */ interface RenderedOp { /** Operator-facing verb, e.g. 'task close'. Colour this. */ verb: string; /** The entity acted on, already stripped of workstream scope. */ subject: string; /** What changed, in prose. May be empty. */ detail: string; } /** * Split a natural key into workstream + local part. * * 'demo' -> { workstream: 'demo' } * 'demo/t1' -> { workstream: 'demo', local: 't1' } * 'demo/t1#3' -> { workstream: 'demo', local: 't1', note: '3' } * 'demo/a->demo/b' -> { workstream: 'demo', local: 'a', to: 'b' } */ declare function parseOpKey(key: string | null): { workstream?: string; local?: string; note?: string; to?: string; }; /** Every intent this formatter knows, derived from the verb table so * the two can't drift. */ declare const KNOWN_INTENTS: readonly KnownIntent[]; /** Every operator-facing verb the formatter can emit. Exported so audits * can check "this emitter's verb is declared" without re-deriving it * from the intent — the two are deliberately not always identical * (`agent.stall` renders as "agent stalled", matching its payload). */ declare const KNOWN_VERBS: readonly string[]; /** * Render one op as prose. * * Returns null when the row is not a rendered op at all — operator prose * from `mu log ""` / a `--kind` ledger, which has no intent and * should be shown verbatim. Callers print `payload` in that case. */ declare function renderOp(row: RenderableOp): RenderedOp | null; /** * Render an op as a single plain-text string. The convenience form for * callers that don't colour the verb separately (JSON `rendered` field, * TUI cards with their own column layout). */ declare function renderOpLine(row: RenderableOp): string; /** * The entity a row is about, for building a "show me this" command. * Reads `intent` + `key`, never prose. */ declare function opSubject(row: RenderableOp): { kind: "task" | "agent"; id: string; } | null; /** What one observation of a worker says (only what the decision uses). */ interface WorkerFacts { /** null = the pane is gone. */ status: RuntimeState | null; ctl?: "ok" | "missing" | "refused"; pending?: boolean; blocked?: CtlBlocked; /** pi's process incarnation. */ incarnation?: string; /** The settled run's count when `lastError` was read, else the current count. */ runs?: number; /** The error that ended the latest settled run (ctl `wait` reply only). */ lastError?: string; } /** Reads the facts of `agents` (keyed `/`), within `ms`. Injected by tests. */ type ObserveWorkers = (agents: readonly AgentRow[], opts: { signal?: AbortSignal; ms: number; }) => Promise>; /** What reading a checker found. */ type CheckerReading = { kind: "running"; } | { kind: "died"; } | { kind: "answer"; outcome: string; text?: string; }; /** Reads one checker once. `afterRuns` is the send's run baseline. Injected by tests. */ type ReadChecker = (db: Db, agent: AgentRow, base: { afterRuns: number; inc: string | null; }, o: { signal?: AbortSignal; ms: number; }) => Promise; /** Bad `--cap`: exit 2, nothing done. */ declare class NextArgError extends Error { readonly name = "NextArgError"; } /** Effects one invocation may perform unless `cap` says otherwise. */ declare const DEFAULT_NEXT_CAP = 5; /** The largest `cap` accepted. */ declare const MAX_NEXT_CAP = 100; interface ExecuteNextOptions { workstream: string; /** Who is asking (`spawnerFromEnv()`): its open human needs stop dispatch. */ caller: string; /** Plan only: zero writes. */ dry?: boolean; /** Effect budget, an integer 1..100 (default 5). */ cap?: number; /** * Worker ceiling: no new pane while this many workers are working (an * IN_PROGRESS task, or an attempt in flight); a worker whose task is CLOSED * is free. Default: none. Recovery in place and replays that need no pane * are never held back. */ workers?: number; /** Pull peer state before each effect. Default: the ambient sync ingest. */ ingest?: () => Promise; /** After the pass, wait (bounded) for the first event on the in-flight workers; recover and go on while the event is the executor's own. */ hold?: boolean; /** `hold`'s total deadline in ms (default and cap: the wait heartbeat, 1500 s). */ timeoutMs?: number; /** `hold`: what a stall does (default `exit`: the stall ends the invocation). */ onStall?: "exit" | "warn" | "steer" | "release"; /** Stop between effects; an effect in flight finishes. The result says `interrupted`. */ signal?: AbortSignal; /** * Opt in to ONE checker per CLOSED/done attempt of an authorised task (a * delegate call in `scratch`, src/next-refute.ts); its verdict is recorded * on the judged task before the call closes. Never accepts, reopens or closes. */ refute?: boolean; /** The checker's cli key. Default: the judged task's stored key (it must speak ctl). */ refuteCli?: string; /** Test seams: the worker reader, the checker reader and the wait. */ observe?: ObserveWorkers; readChecker?: ReadChecker; wait?: (db: Db, refs: readonly WaitRef[], o: WaitOptions) => Promise; } interface NextMove { /** * `spawn` (new attempt), `replay` (stored attempt resumed), `skip` (nothing * done), `failed` (unsent), `rolled_back` (a pane was spawned, then the guard * stopped the send: undone, charged), `abandoned` (a stored attempt ended: * nothing left to do), `needs_human` (a stored attempt that may have landed * and cannot be confirmed: never resent, a human need recorded), `retry` * (a live worker that stopped on an API error was sent its brief again, in * place), `reap` (a dead worker's row was removed; its task is open again), * `attention` (something that needs the orchestrator and was NOT acted on), * `refute` (a checker launched, read or recorded: `state` is its call's). */ kind: "spawn" | "replay" | "retry" | "refute" | "reap" | "attention" | "skip" | "failed" | "rolled_back" | "abandoned" | "needs_human"; task: string; attempt?: number; key?: string; agent?: string; cli?: string; /** `sent`, `unknown`, or (dry replay) the receipt state it would resume from. */ state?: string; reason?: string; /** `refute`: the checker's parsed verdict, once recorded. */ verdict?: string; stale?: "deleted" | "replaced"; /** The one receipt probe a replay made, when it made one. */ checked?: "accepted" | "not-accepted" | "unreachable"; } interface NextResult { workstream: string; dry: boolean; cap: number; /** Effects counted against the cap. */ used: number; moves: NextMove[]; /** Why the loop ended early: the cap with work left, this caller's human need, an unsent failure, or a signal. */ stopped?: "cap" | "human" | "failed" | "interrupted" | "workers"; /** Dry only: the in-flight workers a live run would observe (a dry run reads no pane and no socket). */ inFlight?: string[]; human_needed?: HumanNeeded[]; /** `hold`: the wait(s) that ended the invocation. */ held?: { /** Waits taken (each recovered event starts another pass). */ waits: number; /** The event that ended the hold: the engine's kind, or `none` (nothing in flight). */ event: string; ref: string | null; exit: number; timedOut: boolean; /** The ended wait's snapshots of the in-flight workers, cut to what a reader needs. */ open: { ref: string; status: string | null; lastError?: string; }[]; }; } declare function executeNext(db: Db, opts: ExecuteNextOptions): Promise; /** An agent as a hint names it: its workstream and name. */ interface NextAgent { workstream: string; name: string; } /** What the verb that just ran did, as far as a follow-up depends on it. */ type NextOutcome = /** A delivered send (also `spawn --send`/`--task`, `--fresh`, `--interrupt`): the baseline the SEND measured. */ { kind: "sent"; agent: NextAgent; runs?: number; incarnation?: string; } /** A send written but unconfirmed: `msgId` and the registry id (`AgentRow.incarnation`) it went to. */ | { kind: "send-unknown"; agent: NextAgent; msgId: string; agentId: number; } /** A remote command gate spawned: its wait returns the command's exit. */ | { kind: "gate-spawned"; agent: NextAgent; } /** `task claim --for`: the owner still has to be sent the task. `muxCanSend: false` (tmux): * a non-pi owner cannot be sent anything, so no send is offered. */ | { kind: "claimed-for"; task: { workstream: string; id: string; }; owner: NextAgent; muxCanSend?: boolean; } /** A destructive preview: `apply` is the verb's argv (from `mu`), `revision` its fence. */ | { kind: "preview"; apply: readonly string[]; revision: number; } /** A wait ended with something still owed: its stored handle. */ | { kind: "waited"; resumeId: string | null; scope?: string; }; interface NextContext { /** `currentCallers()`: the managed `/` and `pi:`. Empty = no owned work. */ callers: readonly string[]; /** The resolved scope of the call, when it has one. */ workstream?: string; outcome?: NextOutcome; /** * Whether the verb that just ran registered owed work (`armWait`). The CLI passes it, so * the bare wait follows only a command that created something to wait for; a read or an * unrelated write is not followed by it. Absent (the dashboard, no verb): any owed ref counts. */ armed?: boolean; } /** One move: the argv as typed (`mu` first). Destructive moves must carry `--if`. */ interface HintMove { argv: readonly string[]; destructive?: boolean; } /** A destructive move without its revision fence. */ declare class UnguardedMoveError extends Error { readonly name = "UnguardedMoveError"; } /** One command line, each word POSIX-quoted only when it needs it. */ declare function renderArgv(argv: readonly string[]): string; /** * The plan for `moves` in order: safe moves first (stable), duplicates dropped, at most * MAX_ALTERNATIVES for `alt`. A `seq` is kept whole. null for no moves. */ declare function renderPlan(mode: "seq" | "alt", moves: readonly HintMove[]): NextPlan; /** * Fit a plan into `bytes` (its JSON encoding). A `seq` that does not fit is dropped WHOLE * (never a partial sequence); an `alt` loses alternatives from its tail. `cut` counts the * commands left out. */ declare function capPlan(plan: NextPlan, bytes?: number): { next: NextPlan; cut: number; }; /** A course check is due after this many CLOSED/done work tasks since the last one... */ declare const COURSE_CHECK_CLOSED = 10; /** ...or this long since it (with at least one closed since). Constants: no flag, no env. */ declare const COURSE_CHECK_AGE_MS: number; /** * The course-correct trigger (skills/mu/recipes/course-correct.md), read from * state, never remembered: since the newest note of `ws` starting `COURSE CHECK` (none: the * workstream's creation), count the CLOSED/done work tasks by their close note's time (a later * note does not move it). One line when due, else undefined. * Read-only; one query. */ declare function courseCheckHint(db: Db, ws: string, now?: number): string | undefined; /** * The `next` of a result: the outcome's own follow-up first, then (unless a caller has an * open human need, which stops everything it would start) `mu next` over authorised work and * the bare wait over owned refs (only after a verb that armed one), as alternatives. A preview's apply ranks last. Read-only. */ declare function computeNext(db: Db, ctx: NextContext): NextPlan; /** * The one-line notices of a result, beside its `next` (they are not commands): today only * `courseCheckHint` for the call's workstream. `mu state` and `mu wait` print them as * `data.notices` (`noticesField`); empty when nothing is due. Read-only. */ declare function computeNotices(db: Db, ctx: NextContext, now?: number): string[]; /** What an op context carries. All fields optional — a partial context * is fine and a null intent is captured as null (fail safe). */ interface OpContext { /** Semantic label, e.g. `task.close`. Human-grade: `mu log` renders * prose from it. Use `.` with entities from * docs/VOCABULARY.md. */ intent?: string | undefined; /** * Intent to use ONLY when no enclosing context already set one. * * For shared internals that several public verbs funnel through. * `setTaskStatus` is the motivating case: called directly it is the * operator's action and should label itself, but called from * `closeTask` the OUTER verb is the operator-meaningful label and * must win. Without this, the inner call would report the mechanism * instead of the intent. * * Ignored when `intent` is also provided. */ intentIfUnset?: string | undefined; /** Who caused it. Free text (lands in `ops.actor`): an agent name, "user", "system". */ actor?: string | undefined; /** * Op grouping: * - omitted inherit the enclosing group, or start one if none. * - "new" force a fresh group even when nested. * - use this exact group id. */ group?: string | "new" | undefined; } /** Read the current context. Exported for tests and for `mu doctor`. */ declare function currentOpContext(db: Db): { groupId: string | null; actor: string | null; intent: string | null; applying: boolean; }; /** * Run `fn` with the given op context applied; see `withCtxRow` for the * transaction it owns and how the context is restored. * * Synchronous by design. Every mutating mu SDK function is synchronous * (better-sqlite3 is), so an async variant would only invite * interleaving two contexts on one connection — and, now that the scope * holds the write lock, would hold it across an await. Keeping this sync * makes both unrepresentable. */ declare function withOpContext(db: Db, ctx: OpContext, fn: () => T): T; /** * Run `fn` with capture SUPPRESSED — the echo guard. * * Applying a peer's op writes to `tasks`, which fires the capture * trigger, which mints a NEW local op, which flushes to our segment and * propagates back to the peer, which applies it and echoes again. This * sets `applying = 1` so every trigger's `WHEN` clause short-circuits * and the ingest writes rows WITHOUT writing ops. * * It lives here rather than in the sync module because the flag is part * of the op-context contract the triggers read, and having exactly one * writer of it is the point. Outermost, it owns a transaction like * `withOpContext`, so `applying = 1` is never committed: a crash cannot * leave capture off, and no other connection can write while it is on. * The previous value comes back in a `finally` (nested) or by ROLLBACK * (outermost), so a throw mid-ingest cannot leave capture disabled, the * worst failure mode: silently dropping every later local change. */ declare function withCaptureSuppressed(db: Db, fn: () => T): T; /** First line of every shim the old `mu link pi` generated. */ declare const MU_SHIM_MARKER = "// mu:shim"; type MigrateState = "absent" | "shim" | "foreign"; interface MigrateEntry { path: string; state: MigrateState; moved: boolean; /** Where it went (apply only). */ to?: string; } interface MigrateResult { extension: MigrateEntry; skill: MigrateEntry; guidance: string[]; } interface MigrateOptions { home?: string; /** Move what is verified mu-owned. Default: report only. */ apply?: boolean; /** This install's root (holds skills/mu). Default: the package root. */ packageRoot?: string; /** Digests of released skill dirs. Default: KNOWN_SKILL_DIGESTS. */ knownSkillDigests?: readonly string[]; } /** SHA-256 over the sorted `relative path NUL file-sha256` lines of a * directory tree. undefined if it holds anything but regular files and * directories (a symlink is never part of a released skill). */ declare function skillDirDigest(dir: string): string | undefined; declare function migratePi(opts?: MigrateOptions): MigrateResult; /** Raised when the rebuild target already exists. Never overwrite: the * operator may have pointed at their live DB by mistake, and a rebuild * is supposed to be the SAFE recovery path. */ declare class RebuildTargetExistsError extends Error { readonly path: string; constructor(path: string); } /** Raised when the target path resolves to the source DB. Rebuilding a * DB onto itself would truncate the very log being replayed. */ declare class RebuildTargetIsSourceError extends Error { readonly path: string; constructor(path: string); } /** Per-table counts of rows a rebuild cannot reconstruct, because the * table has no capture triggers and therefore leaves no ops. */ interface MachineLocalLoss { table: string; /** Rows present in the SOURCE that will be absent from the rebuild. */ rows: number; } /** What a rebuild did. Returned rather than printed so the drift check * can consume it programmatically. */ interface RebuildReport { /** Absolute-ish path written (as given by the caller). */ targetPath: string; /** Ops copied into the target's log. Every op, not just synced ones. */ opsCopied: number; /** Ops that projected into a portable table. Always <= opsCopied: * log-only entities (message / event / broadcast), machine-local * agent ops are copied but have no table to land in. */ opsProjected: number; /** Table rows the projection wrote (workstreams, tasks, note sets, edges * that changed). Far lower than opsProjected: the fold writes each key * once, however many ops named it. */ opsChangedRows: number; /** Ops skipped as non-projectable, by entity. Diagnostic only. */ logOnlyByEntity: Record; /** Row counts per portable table in the rebuilt DB. */ rebuiltRows: Record; /** Tables that cannot be rebuilt, with the row counts being lost. */ machineLocalLost: MachineLocalLoss[]; /** The machine identity carried across, so the rebuilt DB remains the * SAME peer rather than becoming a new one. */ machineId: string; } interface RebuildOptions { /** Path for the new DB. Must not exist. */ targetPath: string; /** Overwrite an existing target. Off by default so a mistyped path * cannot clobber a real file; the drift check sets it because it * owns a temp path it just created. */ force?: boolean; } /** * Replay `source`'s ops log into a brand-new DB at `opts.targetPath`. * * Prints nothing and returns a report, so both `mu rebuild` and the * (forthcoming) doctor drift check can use it — the latter rebuilds into * a temp path and diffs rather than showing a human anything. * * SYNCHRONOUS, matching applyOp and withOpContext: the op context is the * shared main-DB `op_ctx` row, so interleaved async scopes would clobber * each other's suppression flag. */ declare function rebuildInto(source: Db, opts: RebuildOptions): RebuildReport; /** A remote exec step failed or answered nothing mu can use. Exit 5 (substrate). */ declare class RemotePrepareError extends Error { readonly host: string; readonly name = "RemotePrepareError"; constructor(host: string, what: string, detail: string); } /** `agent close` refused to remove a remote worktree: nothing was removed. Exit 4. */ declare class RemoteCloseRefusedError extends Error { readonly agentName: string; readonly workstream: string; readonly reason: "dirty" | "uncollected" | "moved" | "unreadable" | "branch" | "held"; /** The recovery record being closed (no agent row left): every hint then names it. */ readonly recoveryId?: number | undefined; readonly name = "RemoteCloseRefusedError"; constructor(agentName: string, workstream: string, reason: "dirty" | "uncollected" | "moved" | "unreadable" | "branch" | "held", detail: string, /** The recovery record being closed (no agent row left): every hint then names it. */ recoveryId?: number | undefined); } /** * The close rule for a remote worktree `spawn --host --worktree` made, run * AFTER the agent's pane is dead (the caller frees the channel first; at cap 1 * the pane held it): under one lease, one exec observes the state (clean or * dirty, HEAD, and that the path is still a worktree of the repo the row recorded), * the rule is applied here, and a second exec removes WITHOUT --force. * * No git lock fences every ref backend (reftable ignores HEAD.lock), so the * removal proves QUIESCENCE instead: the tree is renamed to a fixed aside name * (`asideOf`, derived from the registry incarnation, so the intent survives a * crash with no record to lose), every writer addressing the old path now * fails, and a scan of /proc (else lsof, else refusal) proves no process has * its cwd or an open file under the aside. Only then are HEAD and cleanliness * re-checked (backend-neutral git commands), the `--keep-branch` branch made * create-only, and the tree removed. A holder, a doubt or any mismatch moves * the tree back and refuses. A retry after a crash finds the aside, moves it * back and runs the whole rule again; `gone` means neither path exists. * Never removes dirty files, whatever the flags; a HEAD that is neither the * base nor the last collected one refuses unless `keepBranch`, which makes (or * verifies) `mu//-` at the observed HEAD first. */ declare function closeRemoteWorktree(db: Db, agent: Pick, opts?: { keepBranch?: boolean; paneAlive?: (paneId: string) => Promise; /** Runs between the observation and the removal; throws to stop (an identity re-check). */ beforeRemove?: () => void; /** The removed agent's recovery record (src/remote-recovery.ts) when there is no row: refusals name it. */ recoveryId?: number; /** Runs on the ADMITTED profile before any exec; throws to stop (a recovery pins the profile it was made for). */ checkProfile?: (profile: HostProfile) => void; /** Test seam: where the quiescence scan looks (default /proc, then `lsof`). */ probe?: { proc: string; lsof: string; }; /** Remove past a holder or an unprovable scan (each becomes a warning). Never past dirt or a moved HEAD. */ force?: boolean; }): Promise; /** `agent collect` on an agent that has no remote worktree to collect from. Exit 2. */ declare class RemoteCollectUnsupportedError extends Error { readonly name = "RemoteCollectUnsupportedError"; constructor(agent: string, why: string); } /** The fetch failed or produced no commit: nothing was recorded. Exit 5 (substrate). */ declare class RemoteCollectError extends Error { readonly host: string; readonly name = "RemoteCollectError"; constructor(host: string, detail: string); } interface CollectResult { /** The collected commit, as fetched. */ head: string; /** The durable ref holding it in the local repo. */ ref: string; /** A command for the user to run; mu never runs it. */ merge: string; /** False when this HEAD is the one already collected (or still the base). */ changed: boolean; /** Set when the commits were collected for a removed agent's recovery record (its id), not a live agent. */ recoveryId?: number; } declare function collectRef(workstream: string, name: string): string; /** * Fetch `name`'s remote worktree into the local repo `opts.repo` (a git * checkout) and record the collected HEAD. Idempotent: the same remote HEAD * twice records once and says `changed: false`. */ declare function collectRemoteAgent(db: Db, opts: { workstream: string; name: string; repo: string; recoveryId?: number; }): Promise; /** Why the row went away: `reap` (any plain delete), a `teardown`, or its workstream's deletion. */ type RemoteRecoveryReason = "reap" | "teardown" | "workstream-delete"; interface RemoteRecovery { /** The ORIGINAL registry incarnation (`agents.id`). */ agentId: number; workstream: string; /** With `workstreamCreatedAt`: which generation of the name the agent lived in. */ workstreamId: number; workstreamCreatedAt: string; name: string; paneId: string; host: string; /** The worktree path on the host. */ cwd: string; base: string; collectedHead: string | null; /** The exact path a crashed close left (or would leave) the tree at. */ aside: string; /** The profile's exec template when it existed, else null: identity, not authority. */ hostExec: string | null; /** The worktree's repo on the host (agents.repo), null when the row had none. */ hostRepo: string | null; reason: RemoteRecoveryReason; preservedAt: string; } /** A recovery operation refused: nothing was changed on the host or in the record. Exit 4. */ declare class RemoteRecoveryRefusedError extends Error { readonly workstream: string; readonly agentName: string; readonly reason: "ambiguous" | "mismatch" | "gone" | "live-agent" | "resource-in-use" | "profile" | "changed" | "pin"; /** The record ids the refusal is about (every candidate, or the one that moved). */ readonly candidates: readonly number[]; readonly name = "RemoteRecoveryRefusedError"; constructor(workstream: string, agentName: string, reason: "ambiguous" | "mismatch" | "gone" | "live-agent" | "resource-in-use" | "profile" | "changed" | "pin", detail: string, /** The record ids the refusal is about (every candidate, or the one that moved). */ candidates?: readonly number[]); } /** Every record, oldest incarnation first. */ declare function listRemoteRecoveries(db: Db): RemoteRecovery[]; /** The record of ORIGINAL incarnation `agentId`, or undefined. */ declare function getRemoteRecovery(db: Db, agentId: number): RemoteRecovery | undefined; /** Every record of workstream NAME `workstream` and agent `name`, oldest incarnation first. */ declare function recoveriesFor(db: Db, workstream: string, name: string): RemoteRecovery[]; /** * The record `/` means. An explicit `recoveryId` must be a * record of that very name (`mismatch` otherwise) and is undefined once * removed: the caller decides (a retried close is a no-op; a collect refuses * with `recoveryGone`, never falling through to a live agent). Without it exactly ONE record * selects itself; several refuse (`ambiguous`) and list their ids, never the * newest one. Read only. */ declare function selectRemoteRecovery(db: Db, workstream: string, name: string, recoveryId?: number): RemoteRecovery | undefined; /** A guarded verb was given a revision that no longer matches. Exit 4, * nothing was done. One stderr line naming the fresh preview * command; JSON carries `expected_rev` / `current_rev`. */ declare class RevisionMismatchError extends Error { readonly subject: string; readonly expectedRev: number; readonly currentRev: number; /** The read-only command that prints the current revision. */ readonly previewCommand: string; readonly name = "RevisionMismatchError"; constructor(subject: string, expectedRev: number, currentRev: number, /** The read-only command that prints the current revision. */ previewCommand: string); } /** Throws when a supplied `expected` differs from `current`. `undefined` * (no `--if`) always passes. */ declare function assertRevision(expected: number | undefined, current: number, ctx: { subject: string; previewCommand: string; }): void; /** Revision of every task of `workstream` (task id → revision). Read only. */ declare function taskRevisions(db: Db, workstream: string): Map; /** The revision of one task (0 when it does not exist). Read only. */ declare function taskRevision(db: Db, workstream: string, id: string): number; /** The revision of one agent (`agent close --if`), 0 when nothing of that name * exists. Derived on read, like the task one: a hash of the registry * incarnation (the row's AUTOINCREMENT id, never reused, so a name that is * deleted and created again can never match an old preview, even with the * same pane id), every column of the row (pane, cwd, host, spawner, ...), EVERY * task it owns in any workstream with its status (claim and release write * `tasks.owner_id`, not the agent row), and the registered worktree rows of * that (workstream, name). With no agent row but a registered worktree (an * orphan: a failed spawn or an earlier close left it) the hash is of those * rows alone, so an orphan has a revision of its own and a replaced registry * row moves it. Read only. */ declare function agentRevision(db: Db, workstream: string, name: string, recoveryId?: number): number; /** The segment line format this build WRITES. Bumped only on a breaking * shape change. */ declare const SEGMENT_FORMAT_VERSION = 2; /** The line versions this build READS. The version is checked per line, * and a line of any other version (v1 included) is REFUSED rather than * guessed at. */ declare const SEGMENT_READ_VERSIONS: readonly number[]; /** One serialized op, as it appears on a line of a segment. */ interface SegmentLine { v: number; hlc: string; machine: string; group: string; intent: string | null; actor: string | null; entity: string; key: string; op: "put" | "del"; payload: unknown; crc: string; } /** Why a segment line was rejected. Reported, never silently swallowed. */ type SegmentDefectKind = "torn-write" | "manifest-mismatch" | "crc-mismatch" | "non-monotonic-hlc" | "unknown-version" | "malformed-shape" | "entity-not-synced" | "duplicate-op"; interface SegmentDefect { kind: SegmentDefectKind; /** 1-based line number within the segment. */ line: number; detail: string; /** The format version found, for an `unknown-version` defect. */ version?: number; } /** Serialisable running state: eight hash words (hex), bytes hashed so * far, and the partial trailing block not yet compressed (base64). */ interface Sha256State { h: string; n: number; tail: string; } /** Whole-file verification sidecar. */ interface SegmentManifest { v: number; machine: string; count: number; lastHlc: string | null; sha256: string; updatedAt: string; /** Bytes of the segment `sha256` covers. Written by the owner so its * next flush can trust this manifest instead of rescanning; absent in * manifests from older builds. */ size?: number; /** The segment's mtime when this manifest was written (owner-local). */ mtimeMs?: number; /** Running hash state after `size` bytes, so an append rehashes only * what it added. */ shaState?: Sha256State; /** sha256 over the fields above (`manifestSeal`). The owner trusts * count/size/shaState only when it matches, so a torn or hand-edited * manifest falls back to the full scan instead of being persisted. */ seal?: string; } /** Read a segment's manifest, or null when absent/unparsable. */ declare function readManifest(segment: string): SegmentManifest | null; /** * The sync directory, or null when sync is not configured. * * Null is the normal single-machine case, and every entry point here * treats it as "do nothing, cost nothing" rather than an error. Sync is * opt-in by setting one env var; there is no config file and no * membership list (see `discoverPeers`). */ declare function syncDir(): string | null; /** This machine's id — the identity every op it writes is stamped with. */ declare function localMachineId(db: Db): string; /** Path of a machine's own segment inside `dir`. */ declare function segmentPath(dir: string, machineId: string): string; interface FlushResult { /** Absolute path written, or null when sync is not configured. */ segmentPath: string | null; /** Ops appended by this call. */ appended: number; /** Total lines in the segment afterwards. */ total: number; /** Ops skipped because their entity is machine-local. A full scan * (`verify`, or an untrusted manifest) counts every unflushed one; the * fast path counts only those up to the last op it appended, so a * no-op ambient flush reports 0 (see `pendingLines`). */ skippedLocal: number; /** * Non-null when THIS MACHINE'S OWN segment was found defective past * its last good line (bit rot, a torn write not at EOF, a bad manual * edit). A segment is DERIVED and REGENERABLE from the canonical * `ops` table, so the repair is to truncate the file back to its * last good record and let the append below regenerate the rest — * never to append after the damage, which is the defect that caused * unbounded regrowth (each flush re-deriving the same ops from a * watermark frozen at the corruption point and stacking them after * it, forever). Surfaced here rather than printed directly: this * module does no I/O beyond the filesystem, callers decide how loud * to be (`mu sync`, `ambientFlush`). */ selfRepaired: SegmentDefect | null; /** * Non-null when THIS MACHINE'S OWN segment holds a line of a version * this build cannot read (first such line, 1-based): written by a newer * mu. The segment is then READ-ONLY to this build: flush appends * nothing, repairs nothing (`selfRepaired` stays null), and deletes the * manifest so the fast path cannot vouch for the file. The pending ops * stay in `ops` until mu is upgraded. `foreignVersion` is the version * found. Both null otherwise. */ foreignVersionAt: number | null; foreignVersion: number | null; /** * True when ops were pending but another process held the segment * lock past the wait, so this call wrote nothing. The ops stay * unflushed in `ops` (the segment's last hlc is the watermark), and * the next flush appends them. Never set on a successful flush. */ lockBusy?: boolean; } declare function flushSegment(db: Db, dir?: string | null, opts?: { verify?: boolean; lockWaitMs?: number; }): Promise; /** Number of GOOD lines in a segment (stopping at the first defect, as * ingest does). The denominator of "how far behind am I" — exported for * `mu sync`'s peer table. */ declare function segmentLineCount(path: string): number; /** * Verify a segment against its manifest (layer 4). * * Whole-file, so it catches damage the per-line layers cannot see: a * segment silently replaced wholesale, or truncated exactly on a line * boundary (where every remaining line is individually valid). */ declare function verifyAgainstManifest(segment: string): { ok: true; } | { ok: false; reason: string; }; interface PeerSegment { /** Machine id the segment belongs to. */ machineId: string; /** Key of this FILE's watermark in `sync_peers`: the machine id for * the peer's own segment, the full file stem for a conflict copy. * A copy diverges from its original, so line N of one says nothing * about line N of the other; sharing one watermark skipped every op * that existed only in the copy. Optional for SDK callers written * before it existed: absent means the machine id (`peerWatermarkKey`). */ watermarkKey?: string; /** Path on disk. */ path: string; /** True for a Syncthing-style conflict copy. */ conflictCopy: boolean; } /** * Every segment in `dir` that is not mine. * * IMPLICIT, with no membership list. `MU_SYNC_PEERS` was explicitly * rejected as "a config file with extra steps that must be kept * consistent across every machine" — dropping a segment in the folder * joins the cluster, deleting it leaves. * * CONFLICT COPIES ARE INGESTED, not ignored. Syncthing names them * `.sync-conflict-20260609-123456-ABCDEFG.jsonl`; they are still * valid op logs, and dedup by `(machine_id, hlc)` makes reading them * safe. Ignoring them would silently drop real ops precisely when * something already went wrong. */ declare function discoverPeers(dir: string, selfMachineId: string): PeerSegment[]; /** * How far into one segment file we have applied, keyed by * `PeerSegment.watermarkKey` (the machine id, or a conflict copy's own * stem). * * ONE INTEGER SUFFICES because segments are append-only and ordered — a * set or a vector clock would be strictly more state for no more * information. Stored in `sync_peers.last_applied_seq`, which has been in * the v9 schema unused until now. * * The integer is a LINE COUNT within that peer's segment, not the peer's * `ops.seq` (which is a local-only cursor on their machine and means * nothing here). */ declare function getWatermark(db: Db, watermarkKey: string): number; /** * Store a watermark and stamp `last_seen_at` with the ISO time. The * segment fingerprint goes to `sync_fingerprints` when the caller just * read the file cleanly to its end (`segmentCaughtUp`). Every other * write deletes it, so a reset or partial ingest never vouches for the * file. */ declare function setWatermark(db: Db, watermarkKey: string, value: number, fingerprint?: string | null): void; /** Reset a peer's watermark so the next ingest re-reads from zero. The * universal repair, safe because ingest is idempotent. */ declare function resetWatermark(db: Db, watermarkKey: string): void; interface IngestResult { machineId: string; path: string; /** Lines consumed past the starting watermark: applied plus skipped * (refused, re-delivered) lines. A line that * halted ingest is not counted. */ read: number; /** Ops applied (some are no-ops: already present, or lost an LWW). */ applied: number; /** Ops that changed a row. */ changed: number; /** Watermark after this ingest. */ watermark: number; /** Problems found, in line order. Reported, never swallowed. */ defects: readonly SegmentDefect[]; /** True iff a defect stopped us short of the file's end. */ truncatedAt: number | null; } /** * Read one peer segment from its watermark and apply each op. * * STOPS AT THE FIRST DAMAGED RECORD and advances the watermark only that * far. Never skips a damaged line to continue past it: in an ordered log, * a gap is indistinguishable from reordering, and applying ops around a * hole risks a state neither machine ever had. The tail is re-read on the * next ingest, by which time the transfer has usually completed. * * A REFUSED line is not a damaged one. A well-formed op naming an entity * that must not travel (`entity-not-synced`) is reported as a defect and * SKIPPED: it projects nothing, so it leaves no hole, and halting on it * makes the watermark unrecoverable by any means the CLI offers. * * A RE-DELIVERED line is not a damaged one either. A block of ops the * peer already wrote, appended verbatim a second time, breaks the hlc * ordering (`duplicate-op`) without losing anything: every one of those * ops is already in our `ops` table byte-for-byte, so applying them * again is a no-op by construction. Halting there is unrecoverable — * `--repair` re-reads from zero straight back into the same three * lines, forever — so it is reported and SKIPPED. A non-monotonic line * we have NOT seen before is still real damage and still halts. * * A manifest mismatch (layer 4) is reported but does not halt: every * line still present is individually valid, so applying them is safe. * * Calls `receiveHlc` per op so the local clock advances past the peer's, * which is what makes "laptop edits after seeing the devserver's op" order * correctly rather than losing to it. */ declare function ingestSegment(db: Db, peer: PeerSegment, opts?: { verify?: boolean; }): IngestResult; /** * Apply ONE incoming op and record it in the local `ops` table. * * The shared tail of every ingest path — segment ingest above, and the * `mu sync --from ` reader in `src/sync.ts`, which is a * different READER over the same apply semantics. Extracted so the two * cannot drift: a second copy of "advance the clock, apply, record" is * how one of them silently stops advancing the clock. * * Three steps, in this order: * 1. `receiveHlc` BEFORE applying, so anything we mint afterwards * sorts above the peer's op. * 2. `applyOp`, which is capture-suppressed (no echo op is minted). * 3. Record the op locally so it survives, participates in * provenance, and can be re-flushed by rebuild. INSERT OR IGNORE * makes this idempotent via UNIQUE (machine_id, hlc) — the * property that lets "re-read from zero" be the universal repair. */ declare function applyIncomingOp(db: Db, op: Op): { changed: boolean; }; interface SyncPassResult { flushed: FlushResult; ingested: readonly IngestResult[]; /** True iff any peer reported a defect. */ defective: boolean; } /** * One flush + one ingest of every discovered peer. * * The SDK seam `mu sync` calls (src/cli/sync.ts); it deliberately * prints nothing and starts nothing. No daemon, no watcher, no polling * loop that outlives the command — the anti-feature pledges are firm, and * mu never moves files itself: the operator owns transport. * * A no-op costing nothing when `MU_SYNC_DIR` is unset, which is the * normal single-machine case. */ declare function syncPass(db: Db, dir?: string | null): Promise; /** One task in a state read: SQL column names, `id` = `local_id`. */ interface StateTask { id: string; title: string; status: string; substate: string; impact: number; effort_days: number; /** Owner's name; only while IN_PROGRESS (a CLOSED task's owner is history). */ owner: string | null; kind: string; wave: string | null; updated_at: string; /** `MAX(ops.seq)` over the task's own ops, its notes' and its edges' (raw log, db/sql writes included); 0 when none. */ activity_seq: number; } /** A delegate call that has not finished (`done`/`failed`/`cancelled` are in the wave tally). */ interface StateDelegate { call_id: string; /** The delegate task's local id, in this workstream. */ task: string; state: string; wave: string | null; /** The task judged, `/`; null with no record target. */ judges: string | null; agent: string | null; verdict: string | null; error: string | null; updated_at: string; } /** One wave of the caller's own calls in this workstream, tallied per call (`verdictTally`'s findings summed). */ interface StateWave { wave: string; calls: number; confirmed: number; refuted: number; unverified: number; pending: number; queued: number; running: number; /** Call ids refused past the cap, still to reissue. */ refused: string[]; } /** An OPEN `human` task of the caller. */ interface StateHumanNeed { id: string; /** The brief notes joined with "; " (whole; the CLI bounds it). */ reason: string; } interface StateCounts { tasks: number; open: number; parked: number; triage: number; ready: number; blocked: number; in_progress: number; closed: number; } interface WorkstreamState { workstream: string; workstream_id: number; counts: StateCounts; /** `scored` order (`byRoiDesc`). */ ready: StateTask[]; /** Newest activity first. Absent in a `me` read, which narrows to counts, ready, delegates, waves and human needs. */ in_progress?: StateTask[]; blocked?: StateTask[]; triage?: StateTask[]; recent_closed?: StateTask[]; /** Registry rows (host/cwd/base/collected_head included) with their runtime state; unknown stays unknown. */ agents?: AgentOutput[]; delegates: StateDelegate[]; waves: StateWave[]; human_needs: StateHumanNeed[]; /** Set when the workstream is mostly history (past BIG_WORKSTREAM_TASKS tasks, under 10% open): start the next effort in a new one. */ hint?: string; /** Panes in the workstream's mux session that look like agents but are not registered. Set only by `mu state`, which reconciles first (the snapshot itself reads no mux). */ orphans?: { pane_id: string; command: string; }[]; } /** `mu me`: the caller's own identity and the tasks it owns. */ interface StateMe { caller: string; /** The managed agent's record; null for a pi session (`pi:`), which has no agent row. */ agent: AgentOutput | null; /** Non-CLOSED tasks the agent owns in its workstream. */ owned_tasks: StateTask[]; } interface StateSnapshot { /** `spawnerFromEnv()` identity: `/`, `pi:`, or null (delegates and human needs then stay empty). */ caller: string | null; me: StateMe | null; workstreams: WorkstreamState[]; } interface StateSnapshotOptions { /** The scope, already resolved (variadic/comma/repeated `-w` and `--all` live in the CLI). Empty is legal. */ workstreams: readonly string[]; caller: string | null; /** A `me` read: adds the caller's identity block (`agent` is its registry row, null for a pi session) and narrows each workstream. */ me?: { agent: AgentRow | null; }; } /** Live workstreams where `caller` has a delegate call or an open human need (a pi session has no workstream of its own). */ declare function callerWorkstreams(db: Db, caller: string): string[]; /** * The canonical state read: tasks, agents, the caller's delegate queue and * waves, and its open human needs, per workstream, under SQL column names. * READ-ONLY: it never reconciles or reaps (`mu state` reconciles first, on its * own), so the dashboard can reuse it. Async only for the ctl probes. The * result is whole; the CLI bounds it (`mu state`). * * `me` narrows each workstream to counts, ready, delegates, waves and human * needs, and adds the caller's identity block. The delegate queue and human * needs are the CALLER's own, never another session's. */ declare function stateSnapshot(db: Db, opts: StateSnapshotOptions): Promise; /** No discovered peer matches the operator's `--repair ` ref. * Exit 3 (not found), like every other resolve-time miss. */ declare class SyncPeerNotFoundError extends Error { readonly ref: string; readonly known: readonly string[]; readonly name = "SyncPeerNotFoundError"; constructor(ref: string, known: readonly string[]); } /** A `--repair ` prefix matched several peers. A conflict (exit 4), * never a guess — the same rule `mu log --group` follows for * abbreviated group ids. */ declare class SyncPeerRefAmbiguousError extends Error { readonly ref: string; readonly candidates: readonly string[]; readonly name = "SyncPeerRefAmbiguousError"; constructor(ref: string, candidates: readonly string[]); } /** `mu sync --from ` pointed at a file that is not there. Exit 3. */ declare class SyncSourceNotFoundError extends Error { readonly path: string; readonly name = "SyncSourceNotFoundError"; constructor(path: string); } /** `mu sync --from ` pointed at a DB written by a newer mu. Its ops * may carry fields this build would half-apply, which is exactly what a * segment line's version prevents; this path has no such line. Exit 4. */ declare class SyncSourceTooNewError extends Error { readonly path: string; readonly detectedVersion: number; readonly name = "SyncSourceTooNewError"; constructor(path: string, detectedVersion: number); } /** A peer whose segment has not moved in this long is reported STALE. * A fixed constant, not an env var: it is a display threshold, and mu's * whole cluster configuration is deliberately ONE env var. */ declare const PEER_STALE_MS: number; /** How many characters of a `machine_id` uuid we show. Peers are known * only by uuid — `machine_identity.hostname` is machine-LOCAL and never * ships, so mu genuinely cannot render a peer's hostname without * inventing a membership file. A short prefix is the honest display, * and every verb that takes one accepts any unique prefix (the * affordance git gives for shas). */ declare const PEER_SHORT_LEN = 8; interface PeerStatus { machineId: string; /** First `PEER_SHORT_LEN` chars of the machine id, for display. */ short: string; path: string; /** True for a Syncthing-style `*.sync-conflict-*.jsonl` copy. */ conflictCopy: boolean; /** `sync_peers` key of this file's watermark (see `PeerSegment`). * `peerStatuses` always sets it. Optional so SDK callers that build * the pre-watermarkKey shape still compile; absent means the machine * id (`peerWatermarkKey`). */ watermarkKey?: string; /** Lines of this segment file already applied. */ watermark: number; /** GOOD lines currently in the segment (a defect stops the count). */ total: number; /** `total - watermark`: how much of what we HOLD is not yet applied. * Non-zero after a defect stopped an ingest short, or mid-transfer. */ behind: number; /** Segment mtime in epoch ms — when transport last delivered. */ lastSeenMs: number | null; /** Age of the segment file, ms. Null when it does not exist. */ ageMs: number | null; stale: boolean; } /** Peer table for the sync dir, newest contact first. Pure read: it * neither flushes nor ingests, so `mu sync` can call it after its own * pass and the TUI could call it on a tick. */ declare function peerStatuses(db: Db, dir: string): PeerStatus[]; /** Resolve an operator-typed peer reference (full machine id, or any * unique prefix) against the discovered peers. Ambiguity is a * UsageError, never a guess — repairing the wrong peer would re-read a * whole segment for nothing and confuse the report. * * A ref names a MACHINE, so a segment and its conflict copies (same * machine id) count as one match; the peer's own segment is returned * when present. Counting them separately made the short id `mu sync` * prints unresolvable. */ declare function resolvePeerRef(peers: readonly PeerStatus[], ref: string): PeerStatus; /** Reset the watermark of every segment file a peer has (its own and * any conflict copies) so the next ingest re-reads them from zero. Safe * by construction: apply is idempotent and `ops` dedupes on * `UNIQUE (machine_id, hlc)`. */ declare function repairPeer(db: Db, ref: string, dir: string): PeerStatus; /** Is sync configured at all? THE single `if` the no-sync case pays. * Callers check this before awaiting anything, so an unconfigured * machine allocates no promise and touches no filesystem. */ declare function syncEnabled(): boolean; interface AmbientResult { /** Peers whose segments were read. Empty when sync is off. */ ingested: readonly IngestResult[]; /** Non-fatal problems, already warned about. */ warnings: readonly string[]; } interface AmbientOptions { /** Suppress the stderr warnings. Set by the TUI, which owns the * alternate screen — a stray write there paints garbage over the * dashboard. The warnings are still returned: the dashboard keeps the * latest ones as `sync` attention items until the next ingest. */ quiet?: boolean; } interface AmbientFlushOptions extends AmbientOptions { /** How long to wait for the segment lock (default * AMBIENT_FLUSH_LOCK_WAIT_MS). On timeout nothing is written; the ops * stay in the DB and the next flush appends them. */ lockWaitMs?: number; } /** * INGEST half of the ambient hook: pull every peer segment before the * verb body runs, so the command sees the freshest state the filesystem * can offer. * * Total: no input can make this throw. A per-peer failure is isolated so * one broken segment cannot hide the others. */ declare function ambientIngest(db: Db, opts?: AmbientOptions): Promise; /** * FLUSH half of the ambient hook: append this invocation's own ops to * this machine's segment AFTER the verb body has committed them. * * Order matters and is the whole reason the hook is split in two: a * flush before the body would leave the ops the operator just wrote * sitting unflushed until the NEXT invocation, so `mu task add` on the * laptop followed by `mu sync` on the devserver would show nothing — * exactly the no-hands claim, broken. * * Appends under the cross-process file lock inside `flushSegment`, so * two concurrent mu processes cannot interleave partial lines. A flush * with nothing to append skips the lock, so read-only verbs never queue. * A flush that cannot take the lock within AMBIENT_FLUSH_LOCK_WAIT_MS * writes nothing (`lockBusy`) and the verb carries on (exit 0) after one * stderr line: its ops stay pending in `ops` and the next invocation's * flush appends them. `mu doctor` reports the backlog (`unpublished-ops`). */ declare function ambientFlush(db: Db, opts?: AmbientFlushOptions): Promise; /** * Both halves in one call, for a caller that has no before/after seam to * straddle — today the TUI's SLOW tick (10s; never the 1s fast tick, * which is a repaint cadence and has no business doing filesystem work). * * Ingest first, then flush: the same order the CLI hook uses, so a * long-lived TUI converges on exactly the same schedule as a shell that * runs one verb every ten seconds. */ declare function ambientSyncPass(db: Db, opts?: AmbientOptions): Promise<{ ingested: readonly IngestResult[]; flushed: FlushResult | null; }>; interface IngestFromDbResult { path: string; /** Ops read out of the peer's `ops` table (already filtered). */ read: number; /** Ops that changed a row here. */ changed: number; /** Ops skipped because their entity is machine-local. */ skippedLocal: number; } /** * Ingest straight from a peer's `mu.db`, reading its `ops` table with * SQLite instead of parsing a JSONL segment. * * WHY THIS EARNS A FLAG when `MU_SYNC_DIR=/mnt/whatever mu state` covers * the one-off-directory case: it is a DIFFERENT READER, and nothing * about an env var can express it. The file you have is a database, not * a segment — because you scp'd it, or because you have the devserver's * state dir on sshfs and would rather read the real thing than wait for * a flush. * * Opened `readonly` so mu cannot write to a file it does not own, and * `fileMustExist` so a typo'd path is an error rather than a freshly * created empty DB. * * Filters exactly as flush does — `SYNCED_ENTITIES` only, and never the * peer's copy of OUR ops (we already have those; `UNIQUE (machine_id, * hlc)` would dedupe them anyway, but not reading them is cheaper and * keeps the reported count honest). Ops the peer itself ingested from a * THIRD machine are read, which is how transitive convergence falls out * for free. * * Watermarks are deliberately untouched: a watermark counts LINES of a * segment, and a DB has no such coordinate. The next segment ingest for * that peer re-reads from wherever it was, applies the same ops again, * and changes nothing — idempotence is what makes leaving it alone safe. */ declare function ingestFromDb(db: Db, path: string): IngestFromDbResult; declare class SyncDirForeignError extends Error { readonly dir: string; /** The segment that opens with a pre-v2 line. */ readonly segment: string; readonly name = "SyncDirForeignError"; constructor(dir: string, /** The segment that opens with a pre-v2 line. */ segment: string); } /** * Throw `SyncDirForeignError` when ANY file in `dir` is 3.x evidence: a * `*.jsonl` (conflict copies included) whose first line is pre-v2, or a * `*.manifest` whose `"v"` is below 2 (covers an empty segment). Every file is * scanned, in name order. A missing or empty dir passes. Only the first line * decides: a v1 file that later gained v2 lines is a 3.x file. */ declare function assertSyncDirV2(dir: string): void; /** The task has no `brief` note : nothing is built. */ declare class TaskBriefMissingError extends Error { readonly task: string; readonly workstream: string; readonly name = "TaskBriefMissingError"; constructor(task: string, workstream: string, reason: string); } interface TaskBriefOptions { /** The previous attempt died (`true`) or its run stopped on an API error (`"error"`): say so. */ retry?: boolean | "error"; } interface TaskBrief { text: string; } /** Build the prompt of one attempt for `task` (see the file header). */ declare function buildTaskBrief(db: Db, workstream: string, task: string, opts?: TaskBriefOptions): TaskBrief; type tmux_AttachTarget = AttachTarget; type tmux_CaptureOptions = CaptureOptions; type tmux_MuxCommand = MuxCommand; type tmux_MuxError = MuxError; declare const tmux_MuxError: typeof MuxError; type tmux_MuxHealth = MuxHealth; declare const tmux_PANE_ID_RE: typeof PANE_ID_RE; type tmux_PaneNotFoundError = PaneNotFoundError; declare const tmux_PaneNotFoundError: typeof PaneNotFoundError; type tmux_TmuxError = TmuxError; declare const tmux_TmuxError: typeof TmuxError; type tmux_TmuxExecResult = TmuxExecResult; type tmux_TmuxExecutor = TmuxExecutor; type tmux_TmuxPane = TmuxPane; declare const tmux_assertValidPaneId: typeof assertValidPaneId; declare const tmux_attachCommands: typeof attachCommands; declare const tmux_capture: typeof capture; declare const tmux_currentSessionName: typeof currentSessionName; declare const tmux_ensureSession: typeof ensureSession; declare const tmux_healthCheck: typeof healthCheck; declare const tmux_isValidPaneId: typeof isValidPaneId; declare const tmux_killPane: typeof killPane; declare const tmux_killSession: typeof killSession; declare const tmux_listPanes: typeof listPanes; declare const tmux_paneAlive: typeof paneAlive; declare const tmux_paneResult: typeof paneResult; declare const tmux_resetSleep: typeof resetSleep; declare const tmux_resetTmuxExecutor: typeof resetTmuxExecutor; declare const tmux_setSleepForTests: typeof setSleepForTests; declare const tmux_setTmuxExecutor: typeof setTmuxExecutor; declare const tmux_sleep: typeof sleep; declare const tmux_tmuxAttachMode: typeof tmuxAttachMode; declare const tmux_tmuxBackend: typeof tmuxBackend; declare namespace tmux { export { type tmux_AttachTarget as AttachTarget, type tmux_CaptureOptions as CaptureOptions, type tmux_MuxCommand as MuxCommand, tmux_MuxError as MuxError, type tmux_MuxHealth as MuxHealth, tmux_PANE_ID_RE as PANE_ID_RE, tmux_PaneNotFoundError as PaneNotFoundError, tmux_TmuxError as TmuxError, type tmux_TmuxExecResult as TmuxExecResult, type tmux_TmuxExecutor as TmuxExecutor, type tmux_TmuxPane as TmuxPane, tmux_assertValidPaneId as assertValidPaneId, tmux_attachCommands as attachCommands, tmux_capture as capture, tmux_currentSessionName as currentSessionName, tmux_ensureSession as ensureSession, tmux_healthCheck as healthCheck, tmux_isValidPaneId as isValidPaneId, tmux_killPane as killPane, tmux_killSession as killSession, tmux_listPanes as listPanes, tmux_paneAlive as paneAlive, tmux_paneResult as paneResult, tmux_resetSleep as resetSleep, tmux_resetTmuxExecutor as resetTmuxExecutor, tmux_setSleepForTests as setSleepForTests, tmux_setTmuxExecutor as setTmuxExecutor, tmux_sleep as sleep, spawnAgent$1 as spawnAgent, tmux$1 as tmux, tmux_tmuxAttachMode as tmuxAttachMode, tmux_tmuxBackend as tmuxBackend }; } interface Track { /** Goal tasks (no outgoing edges) belonging to this track. */ roots: TaskRow[]; /** Every task id reachable as a prerequisite of any root in this track. */ taskIds: ReadonlySet; /** Number of READY tasks (per the SQL view) within this track's subgraph. */ readyCount: number; /** True when every non-CLOSED task in the track is OPEN/parked: the * track holds work, but none of it is schedulable. */ parked: boolean; } /** * Identify independent task subtrees suitable for parallel assignment * within a workstream. Open goals only; CLOSED goals are excluded as * they no longer represent work to schedule. * * Scoping: only goals belonging to `workstream` are considered. * Cross-workstream edges are forbidden by addTask, so a goal's * prerequisite subgraph is naturally workstream-internal. */ declare function getParallelTracks(db: Db, workstream: string): Track[]; interface ActionKey { /** `tasks.id`. */ taskId: number; /** `agents.id` of the owner when the action was taken. */ ownerId: number; /** pi's ctl incarnation; '' when none. */ incarnation: string; action: WaitAction; } /** What a reservation records about the situation. */ interface ActionInfo { /** Who asked (one of the caller set); attribution only, not identity. */ caller: string; /** `/`. */ taskRef: string; /** `/`. */ ownerRef: string; paneId: string; /** The task revision the action was decided against. */ previewRevision: number; } interface ActionReceipt extends ActionKey, ActionInfo { msgId: string | null; state: WaitActionState; runs: number | null; detail: string | null; createdAt: string; updatedAt: string; } declare function getActionReceipt(db: Db, key: ActionKey): ActionReceipt | undefined; /** * Reserve the exclusion. `created: true` means THIS call made the row and * owns the action; `false` returns the row that is already there (whoever * wrote it, in whatever state). One statement: concurrent connections race * on the primary key and exactly one wins. */ declare function reserveActionReceipt(db: Db, key: ActionKey, info: ActionInfo & { /** The id a ctl send will carry: reserved WITH the row, so a resumer reuses it and a crash leaves one to probe. */ msgId?: string; }): { created: boolean; receipt: ActionReceipt; }; /** * Compare-and-set `from` -> `to`: true iff this call moved the row. False * when the row is absent or no longer in `from` (someone else won). A * `from`/`to` pair that is not an edge throws. Patch fields left undefined * keep their stored value. */ declare function transitionActionReceipt(db: Db, key: ActionKey, from: WaitActionState, to: WaitActionState, patch?: { msgId?: string; runs?: number; detail?: string; }): boolean; interface StallActionOptions { /** The caller set of this process (default: `currentCallers()`). */ callers?: readonly string[]; } /** * Release the stalled task if every guard still holds, atomically; else report * why not and change nothing. Always `disposition: "return"`: the caller gets * an actionable snapshot, the wait does not go on watching a freed task. */ declare function releaseStall(db: Db, ctx: WaitStallContext, opts?: StallActionOptions): Promise; /** `handleStall` for `onStall: 'release'`. */ declare function releaseStallHandler(db: Db, opts?: StallActionOptions): (ctx: WaitStallContext) => Promise; /** * The one reminder a stalled worker gets: fixed, plain text, bounded (a task * id is a short name). Never carries worker output, notes or free text. */ declare function steerText(workstream: string, taskId: string): string; /** * Nudge the stalled owner once. See the file header for the protocol. The * result is never a thrown error: `sent` (+ the real run baseline) continues * the wait; everything else returns the stall with the reason. */ declare function steerStall(db: Db, ctx: WaitStallContext, opts?: StallActionOptions): Promise; /** `handleStall` for `onStall: 'steer'`. */ declare function steerStallHandler(db: Db, opts?: StallActionOptions): (ctx: WaitStallContext) => Promise; /** * `waitForEvents` with the stall actions wired: `onStall: 'steer' | 'release'` * picks the handler (a `handleStall` you pass wins). A composition, not a loop. */ declare function waitWithActions(db: Db, refs: readonly WaitRef[], opts: WaitOptions): Promise; /** The callers of this process, from its env: managed ws/name first, then pi:. */ declare function currentCallers(env?: NodeJS.ProcessEnv): string[]; /** * `paneText` (a wait with `--until-text`): also read the pane tail of an agent whose control * socket does not answer (a non-pi command, a `--no-ctl` pane). A pi agent that answers has * authoritative ctl `lastText` and is never read from its pane. Best effort: a pane the mux * cannot read just has no text and never matches. */ declare function ctlAgentHooks(db: Db, opts?: { paneText?: boolean; }): Required>; declare const WAIT_REGEX_MAX_LEN = 200; /** Only this many trailing characters of a text are matched. */ declare const WAIT_TEXT_MAX = 8192; /** The pattern is refused (grammar, size, syntax) or its match ran out of budget. */ declare class WaitRegexError extends Error { readonly name = "WaitRegexError"; } interface TextMatcher { readonly source: string; /** Does the (tail of the) text match? Throws WaitRegexError past the time budget. */ test(text: string): boolean; } /** Throws WaitRegexError unless `src` is inside the accepted grammar. */ declare function checkRegexGrammar(src: string): void; /** Validate `src` and return a matcher with the time and size budget applied. */ declare function compileTextMatcher(src: string): TextMatcher; /** What a wait asked for, whatever spelling it came in; a resume replays exactly this. */ interface ResumeSpec { refs: WaitRef[]; mode: "first" | "all"; scope?: string; status?: string; untilText?: string; stuckAfter?: number; onStall: "exit" | "warn" | "steer" | "release"; wave?: string; blockersOf?: { workstream: string; id: string; }; /** Seconds as the caller gave them; absent = the default. */ timeout?: number; /** The reaper-flip anchor (ops seq) of the wait that saved it: a worker that died since is still dead. */ since?: number; } interface StoredResume { id: string; /** The callers the wait ran for (see `currentCallers`): the identity is part of the condition set. */ callers: string[]; spec: ResumeSpec; } /** `mu wait --resume ` names a handle that does not exist (never saved, or older than RESUME_KEEP_DAYS). */ declare class WaitResumeNotFoundError extends Error { readonly resumeId: string; readonly name = "WaitResumeNotFoundError"; constructor(resumeId: string); } /** Save the condition set; returns the handle. */ declare function saveResume(db: Db, callers: readonly string[], spec: ResumeSpec): string; /** The stored wait, or WaitResumeNotFoundError. */ declare function loadResume(db: Db, id: string): StoredResume; declare function isValidWorkstreamName(name: string): boolean; /** A workstream name collision. Exported SDK error class mapped to exit * 4 (conflict) by `classifyError`; no mu verb throws it today * (`ensureWorkstream` is idempotent). Invalid names throw * `WorkstreamNameInvalidError`. */ declare class WorkstreamExistsError extends Error { readonly workstream: string; readonly name: string; constructor(workstream: string); } declare class WorkstreamNameInvalidError extends Error { readonly attempted: string; readonly name = "WorkstreamNameInvalidError"; constructor(attempted: string); } /** Thrown by `mu workstream init` when the operator tries to create a * reserved workstream (e.g. `scratch`) explicitly. The name is not * invalid — it auto-creates on spawn — it just can't be `init`ed. */ declare class WorkstreamNameReservedError extends Error { readonly attempted: string; readonly name = "WorkstreamNameReservedError"; constructor(attempted: string); } /** A destructive verb was given a preview revision that no longer matches: * the workstream (its ops, agents or worktrees) changed since the preview. * Exit 4, nothing was done. One stderr line: the message names the * preview command. */ declare class WorkstreamRevisionMismatchError extends Error { readonly workstream: string; readonly expected: number; readonly actual: number; /** The command that previews again. */ readonly previewCommand: string; readonly name = "WorkstreamRevisionMismatchError"; constructor(workstream: string, expected: number, actual: number, /** The command that previews again. */ previewCommand?: string); } /** `workstream init --if` over a torn-down name would hard-delete rows * while this machine still has agent rows (so possibly live panes) in it. * A peer's teardown can reach a machine that still runs its agents. Exit 4. */ declare class WorkstreamHasAgentsError extends Error { readonly workstream: string; readonly agents: readonly string[]; readonly name = "WorkstreamHasAgentsError"; constructor(workstream: string, agents: readonly string[]); } /** A teardown found registered panes still alive outside its session * (moved by hand): killing the session would not stop them, and deleting * their rows would orphan them. Exit 4, nothing was done. */ declare class WorkstreamPanesLiveError extends Error { readonly workstream: string; readonly agents: readonly string[]; readonly name = "WorkstreamPanesLiveError"; constructor(workstream: string, agents: readonly string[]); } /** `workstream init --if` over a torn-down name found its mux session still * alive: the old rows are not deleted while panes may run. Exit 4. */ declare class WorkstreamSessionLiveError extends Error { readonly workstream: string; readonly session: string; readonly name = "WorkstreamSessionLiveError"; constructor(workstream: string, session: string); } /** * The preview **revision** of a workstream: a number that changes whenever * anything a destructive verb would act on changes. It hashes the newest * op touching the workstream's keys (0 if none), and the machine-local * state ops cannot see: agent names + pane ids and registered worktree * paths. An agent spawned after the preview therefore invalidates `--if`. * Read only: safe for a preview. */ declare function workstreamRevision(db: Db, name: string): number; /** * Ensure a row exists in the `workstreams` table for `name`. Idempotent; * INSERT OR IGNORE so concurrent callers race safely. Called by * `insertAgent` and `addTask` so callers don't need to remember to call * `mu workstream init` before adding a task / spawning an agent (preserves the * spawn-without-init ergonomics now that agents.workstream and * tasks.workstream are real FKs into this table). * * Validates the name before inserting; throws `WorkstreamNameInvalidError` * for names tmux would silently mangle (containing '.' or ':') or that * exceed 32 chars / start with a non-letter. * * Returns true iff a row was actually inserted (vs. already present). */ declare function ensureWorkstream(db: Db, name: string): boolean; interface WorkstreamSummary { /** The workstream's own name. */ name: string; /** Mux session name, defaults to `mu-`. */ muxSession: string; /** True iff the mux session `` is alive right now. */ muxAlive: boolean; /** Rows in `agents` for this workstream. */ agentCount: number; /** Rows in `tasks` for this workstream. */ taskCount: number; /** Rows in `task_notes` whose task is in this workstream. */ noteCount: number; /** Rows in `task_edges` whose `from_task` is in this workstream. */ edgeCount: number; /** Registered worktrees (`worktrees` rows) of this workstream. Surfaced so * the teardown dry-run can warn about per-agent worktrees that need * cleanup. */ worktreeCount: number; /** True iff a row exists in the `workstreams` table itself. False * for tmux-only `mu-*` sessions that mu never observed via * `mu workstream init`. Surfaced so teardown can clean up bare * registry rows (workstream row exists, no agents/tasks/etc.) — * otherwise such rows are orphaned forever (the previous * `nothingToDo` heuristic short-circuited on them). */ registered: boolean; } interface TeardownResult { /** True iff killing the mux session actually killed something. */ killedMux: boolean; /** Number of machine-local `agents` rows deleted (their panes are gone). */ deletedAgents: number; /** Rows KEPT because their recorded pane was alive (or could not be asked) * after the kill: orphaned, in a torn-down workstream. * `mu agent close -w ` removes one. */ orphanedAgents: { name: string; paneId: string; reason: "alive" | "unknown"; }[]; /** Rows that STAY (the teardown is soft): tasks, and their notes and * edges. Reported so the output says what a later `init --if` would * hard-delete. */ tasks: number; notes: number; edges: number; /** Worktrees left alone: teardown never removes one. Their registry rows * stay; `mu agent close -w ` applies the close rule to one. */ keptWorktrees: { agent: string; path: string; }[]; } interface WorkstreamOptions { workstream: string; /** Override the mux session name. Defaults to `mu-`. */ muxSession?: string; } interface TeardownWorkstreamOptions extends WorkstreamOptions { /** The preview revision (`workstreamRevision`). Required: the guarantee * is code, not a CLI convention. A mismatch throws * `WorkstreamRevisionMismatchError` before anything is touched. */ ifRevision: number; } /** * Discover every registered workstream: the rows in the `workstreams` * table (canonical DB source; populated by `mu workstream init` and auto-created by * insertAgent / addTask), each decorated with mux liveness. A `mu-*` mux * session with no row is not discovered: the DB owns the workstream list. * * Returns one `WorkstreamSummary` per workstream, sorted by name. */ declare function listWorkstreams(db: Db): Promise; declare function summarizeWorkstream(db: Db, opts: WorkstreamOptions): Promise; /** * Tear down a workstream (soft): kill its mux session, delete its * machine-local agent rows and set `torn_down_at`. Worktrees are never * removed. Tasks, edges and notes STAY (a later `init --if` is what * hard-deletes them, after a preview). Needs the preview revision. * * ORDER IS THE GUARANTEE. Nothing is written before the session is known * dead: the revision check reads only; the mux probe PROPAGATES ("could * not ask" is not "dead"); a registered pane outside the session is asked * about by id and blocks the teardown while it lives; the kill comes before * any DB change; the DB commit is last, one op group. A failure at any step * leaves the workstream live and the command re-runnable. * * Idempotent: an unknown or already-torn-down name has nothing to do. */ declare function teardownWorkstream(db: Db, opts: TeardownWorkstreamOptions): Promise; export { AGENT_STATE_GLYPH, type AbortAgentOptions, type AbortResult, type ActionInfo, type ActionKey, type ActionReceipt, type AddNoteOptions, type AddNoteResult, type AddTaskOptions, type AddTasksOptions, type AddTasksResult, AgentAbortNeedsCtlError, AgentAbortTimeoutError, type AgentBaseline, AgentBusyError, AgentCtlUnreachableError, AgentDiedOnSpawnError, AgentEmptySendError, AgentExistsError, AgentExtensionOutdatedError, AgentFreshNeedsCtlError, type AgentIdentity, AgentLockBusyError, AgentNameExhaustedError, AgentNotFoundError, AgentNotInWorkstreamError, type AgentObservation, AgentPaneDeadError, AgentPaneStillAliveError, type AgentReading, AgentReplacedError, AgentRestartedError, type AgentRow, AgentSendIndeterminateError, AgentSendUnsupportedError, AgentSlashCommandUnsupportedError, AgentSpawnCliNotFoundError, AgentSpawnStartupError, type AgentTool, type AgentWatchHandle, type AmbientOptions, type AmbientResult, type AppendLogOptions, type ApplyResult, type AttemptPlan, type AttemptRecord, type AttentionKind, type AuthorisedTask, type AuthorizeTasksOptions, type AuthorizeTasksResult, type AutoConfig, type BlockEdgeResult, CAPTURE_TRIGGER_DDL, CAPTURE_VERSION, COURSE_CHECK_AGE_MS, COURSE_CHECK_CLOSED, CURRENT_SCHEMA_VERSION, type CaptureDb, type CaptureIntent, type CaptureOptions, type CaseCollision, type ChannelOpts, type CheapDriftReport, type ClaimResult, type ClaimTaskOptions, ClaimerNotRegisteredError, type CloseAgentOptions, type CloseAgentResult, CloseNeedsEvidenceError, type CloseOutcome, type ClosePreview, type CloseSubstate, type CloseTaskResult, type CollectResult, type CommandResolutionResult, type CommandResolver, CrossWorkstreamEdgeError, type CtlLink, type CursorKind, type CursorToken, CycleError, DEFAULT_NEXT_CAP, DEFAULT_SUBSTATE, DELEGATE_TOKEN, DRIFT_REPORT_CAP, type DashboardAgent, type DashboardAttention, type DashboardCall, type DashboardDelegate, type DashboardEdge, type DashboardHumanNeed, type DashboardLogItem, type DashboardOptions, type DashboardSnapshot, type DashboardTask, type DashboardWave, type DashboardWorkstream, type Db, DbInsideSyncDirError, DelegateCallConflictError, DelegateNotFoundError, type DelegateRequest, type DelegateRow, type DelegateState, DelegateStateError, type DeleteTaskResult, type DoctorCheck, type DoctorStatus, DriftDetectedError, type DriftRecord, type DriftReport, EXPECTED_TABLES, type EvidenceOption, type ExecuteNextOptions, FRONTIER_CLOSED_PER_NODE, FRONTIER_MAX_DEPTH, FRONTIER_MAX_NODES, type FileLockOptions, type FinishDelegateOptions, type FinishDelegateResult, type FleetHazard, type FlushResult, type FrontierDirection, type FrontierEntry, type FsProbe, GLYPH, GroupIdAmbiguousError, GroupNotFoundError, HOST_ENV_KEYS, HOST_PLACEHOLDERS, HazardFailError, type HazardSeverity, type HintMove, type Hlc, HlcOverflowError, HlcParseError, type HostChannel, HostChannelBusyError, HostCheckFailedError, type HostCheckReport, type HostCheckStep, HostExistsError, HostInUseError, type HostLease, HostNotFoundError, type HostProfile, HostProfileError, HostSpawnUnsupportedError, HostTemplateError, type HostTemplateValues, type HumanNeeded, HumanNeededInvalidError, type HumanNeededOptions, HumanTaskError, type IdFromTitleResult, type IngestFromDbResult, type IngestResult, type InsertAgentInput, InvalidSubstateError, KNOWN_INTENTS, KNOWN_VERBS, type KnownIntent, type ListLiveAgentsOptions, type ListLogsOptions, type ListNotesOptions, type ListTasksOptions, type LiveAgent, type LiveAgentsView, type LocalIntent, type LogKind, LogKindReservedError, type LogRow, MACHINE_LOCAL_ENTITIES, MACHINE_LOCAL_TABLES, MAX_NEXT_CAP, MU_SHIM_MARKER, MachineIdentityMissingError, type MachineLocalEntity, type MachineLocalLoss, type MachineLocalTable, type MigrateEntry, type MigrateResult, type MigrateState, type MuxBackend, type MuxBackendName, MuxError, type MuxPane, type MuxPaneStatus, NextArgError, type NextContext, type NextMove, type NextOutcome, type NextPlan, type NextResult, NoMultiplexerError, type ObservedAgent, type ObservedWatch, type Op, type OpContext, OpEntityNotSyncedError, OpKeyMalformedError, type OpenDbOptions, type OpenTaskFrontier, type OpenTaskFrontierOptions, PANE_ID_RE, PEER_SHORT_LEN, PEER_STALE_MS, PORTABLE_TABLES, PaneNotFoundError, type ParkTaskOptions, type PeerSegment, type PeerStatus, type PortableTable, type RebuildOptions, type RebuildReport, RebuildTargetExistsError, RebuildTargetIsSourceError, type ReconcileMode, type ReconcileOptions, type ReconcileReport, type RecordRef, type RefBaseline, type RegisterDelegateOptions, type RegisterRef, type RegisteredRef, type ReleaseResult, type ReleaseTaskOptions, type ReleasedWorktree, RemoteCloseRefusedError, RemoteCollectError, RemoteCollectUnsupportedError, RemotePrepareError, type RemoteRecovery, type RemoteRecoveryReason, RemoteRecoveryRefusedError, type RemoveBlockEdgeResult, type RenderableOp, type RenderedOp, type ReparentTaskResult, RevisionMismatchError, type RuntimeState, SEGMENT_FORMAT_VERSION, SEGMENT_READ_VERSIONS, STALLED_TOOL_MS, SYNCED_ENTITIES, SchemaTooNewError, SchemaTooOldError, type SegmentDefect, type SegmentDefectKind, type SegmentLine, type SegmentManifest, type SelectedNotes, type SendResult, type SetStatusOptions, type SetStatusResult, type SlugifyResult, type SpawnAgentOptions, SpawnAttemptBusyError, SpawnAttemptFailedError, SpawnAttemptGuardError, type SpawnReceipt, SpawnReceiptConflictError, SpawnTaskStateError, SpawnTrustDialogError, SqliteTooOldError, type StallActionOptions, type StallActionResult, type StateCounts, type StateDelegate, type StateHumanNeed, type StateMe, type StateReading, type StateSnapshot, type StateSnapshotOptions, type StateSource, type StateTask, type StateWave, SubstateReasonRequiredError, SyncDirForeignError, type SyncPassResult, SyncPeerNotFoundError, SyncPeerRefAmbiguousError, SyncSourceNotFoundError, SyncSourceTooNewError, type SyncedEntity, TASK_KINDS, TASK_STATUSES, TASK_STATUS_LIST, TASK_SUBSTATES, TASK_SUBSTATE_ROWS, TaskAlreadyOwnedError, TaskAutoArgError, TaskAutoRefusedError, type TaskBrief, TaskBriefMissingError, type TaskBriefOptions, type TaskEdgeWithStatus, type TaskEdges, type TaskEdgesWithStatus, TaskExistsError, TaskHasOpenBlockersError, TaskIdInvalidError, TaskInTriageError, type TaskKind, TaskNotFoundError, TaskNotInWorkstreamError, TaskNoteRangeError, type TaskNoteRow, type TaskPair, TaskParkStateError, TaskParkedError, type TaskRow, type TaskSpawnOutcome, type TaskSpawnRequest, type TaskStatus, type TaskSubstate, TaskTitleSlugEmptyError, type TeardownResult, type TextMatcher, TmuxError, type TmuxExecResult, type TmuxExecutor, type TmuxPane, type Track, type Transport, UNKNOWN_REASON, UnguardedMoveError, type UpdateTaskOptions, type UpdateTaskResult, type VerdictLabel, type VerdictTally, DEFAULT_CTL_STUCK_AFTER_MS as WAIT_DEFAULT_CTL_STUCK_AFTER_MS, DEFAULT_STUCK_AFTER_MS as WAIT_DEFAULT_STUCK_AFTER_MS, WAIT_HEARTBEAT_CAP_MS, WAIT_REF_KINDS, WAIT_REGEX_MAX_LEN, WAIT_REGISTRATION_MAX_REFS, WAIT_REGISTRATION_STATES, WAIT_TEXT_MAX, type WaitEvent, type WaitEventKind, WaitNothingError, type WaitOptions, type WaitRef, WaitRefError, type WaitRefKind, WaitRegexError, type WaitRegistration, WaitRegistrationError, type WaitRegistrationState, type WaitResult, WaitResumeNotFoundError, type WaitSnapshot, type WaitStallContext, WaveNotFoundError, WorkstreamExistsError, WorkstreamHasAgentsError, WorkstreamNameInvalidError, WorkstreamNameReservedError, WorkstreamNotFoundError, type WorkstreamOptions, WorkstreamPanesLiveError, WorkstreamRevisionMismatchError, WorkstreamSessionLiveError, type WorkstreamState, type WorkstreamSummary, WorkstreamTornDownError, WorktreeBranchExistsError, WorktreeDirtyError, WorktreeHeldError, type WorktreeHolder, WorktreePathNotEmptyError, type WorktreeRow, type WorktreeState, WorktreeVcsRequiredError, abortAgent, acceptTask, activeMux, addBlockEdge, addHost, addNote, addTask, addTasks, admitHostChannel, agentKey, agentRevision, agentStateGlyph, ambientFlush, ambientIngest, ambientSyncPass, appendLog, applyIncomingOp, applyOp, applyOps, assertRevision, assertSyncDirV2, assertValidPaneId, attemptName, authorisedReady, authorizeTasks, autoPermitted, beginWait, bindHostChannel, buildTaskBrief, callerWorkstreams, capPlan, capture, captureTriggersCurrent, checkCaseCollisions, checkCheapDriftInvariant, checkCommandResolvable, checkDbInsideSyncDir, checkDrift, checkFleetHazards, checkHost, checkNetworkMount, checkRegexGrammar, claimTask, classifyFsType, closeAgent, closeRemoteWorktree, closeTask, closedToken, collectRef, collectRemoteAgent, compareHlc, compileTextMatcher, computeNext, computeNotices, courseCheckHint, createWorktree, ctlAgentHooks, ctlRuntimeState, currentCallers, currentOpContext, cursorKey, defaultDbPath, defaultSpawnLivenessMs, defaultStateDir, deleteAgent, deleteTask, detectMux, discoverPeers, driftRemediation, emitEvent, endWait, ensureWorkstream, envVarNameForCli, executeNext, expandHostTemplate, expectsCtl, findCaseCollisions, finishDelegate, flushSegment, forgetWorktree, formatDriftRecord, formatHlc, formatPair, getActionReceipt, getAgent, getAgentByPane, getAutoConfig, getDelegateCall, getHost, getParallelTracks, getPrerequisites, getRemoteRecovery, getTask, getTaskEdges, getTaskEdgesWithStatus, getWatermark, getWorktreeAt, getWorktreeFor, hostEnvWords, hostStarter, idFromTitle, idFromTitleVerbose, ingestFromDb, ingestSegment, insertAgent, installCapture, interruptAgent, isConsumed, isPathInside, isTaskStatus, isValidAgentName, isValidPair, isValidPaneId, isValidTaskId, isValidWorkstreamName, killPane, killSession, latestSeq, listAgents, listBlocked, listDelegates, listDelegatesOf, listGoals, listHostChannels, listHosts, listInProgress, listLiveAgents, listLogs, listNotes, listPanes, listReady, listRecentClosed, listRemoteRecoveries, listTasks, listTasksByOwner, listWorkstreams, listWorktrees, loadDashboard, loadOpenTaskFrontier, loadResume, localMachineId, locksDir, mapLegacyStatus, markDelegate, migratePi, murmurAvailable, mux, muxByName, nextHlc, observationFromReading, observeAgentStates, opSubject, openBlockersClosure, openDb, openHumanNeeds, openTask, paneAlive, parkTask, parseHlc, parseOpKey, parseVerdict, peerStatuses, pendingAttempts, planAttempt, previewClose, probeFilesystem, readAgent, readAgentStates, readCursors, readFileLockMeta, readLiveAgents, readManifest, readWaitRegistration, rebuildInto, receiveHlc, reclaimWorktreePath, reconcile, recordCursors, recordHumanNeeded, recoveriesFor, registerDelegateCall, registerOwnedWait, registerWorktree, releaseHostChannel, releaseHostChannelsForAgent, releaseStall, releaseStallHandler, releaseTask, releaseWorktree, removeBlockEdge, removeHost, removeWorktree, renderArgv, renderFrontierLines, renderOp, renderOpLine, renderPlan, repairPeer, reparentTask, reserveActionReceipt, reserveAttempt, reserveHostChannel, resetCommandResolverForTests, resetMux, resetSleep, resetTmuxExecutor, resetWatermark, resolveActorIdentity, resolveCliCommand, resolveCliCommandWithSource, resolvePair, resolvePeerRef, runHostTemplate, saveResume, segmentLineCount, segmentPath, selectRemoteRecovery, selectTaskNotes, sendToAgent, sendViaTransport, setCommandResolverForTests, setMuxForTests, setSleepForTests, setTaskStatus, setTmuxExecutor, setWatermark, settledCloseInput, skillDirDigest, sleep, slugifyTitle, slugifyTitleVerbose, spawnAgent, spawnAutoAttempt, spawnForTask, stateSnapshot, steerStall, steerStallHandler, steerText, summarizeWorkstream, syncDir, syncEnabled, syncPass, taskRevision, taskRevisions, teardownWorkstream, tmux, tmuxBackend, transitionActionReceipt, unparkTask, updateTask, upstreamRefs, validateHostTemplate, verdictTally, verifyAgainstManifest, waitForEvents, waitWithActions, watchAgentRun, withAgentLock, withCaptureSuppressed, withFileLock, withHostChannel, withOpContext, workstreamRevision, worktreePath, worktreeState, worktreesRoot };