import { Database } from 'better-sqlite3'; type CtlState = "busy" | "idle" | "needs_input"; /** Session commands the `command` op runs: `/new`, `/reload`, `/compact`. */ declare const CTL_COMMANDS: readonly ["new", "reload", "compact"]; type CtlCommandName = (typeof CTL_COMMANDS)[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; } interface StateAgentRef { name: string; workstreamName: string; paneId: string; /** When set and it names pi, the control socket is the state source. */ cli?: string; } /** ctl's idle maps to needs_input, the same as murmur's idle. */ declare function ctlRuntimeState(state: CtlState): 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). */ declare function readAgentStates(agents: readonly StateAgentRef[], opts?: { now?: number; stateDir?: string; }): Promise>; /** * One actionable next step. The `intent` is human-prose ("Drop notes * as you work"); the `command` is a literal shell command the user (or * an LLM) can copy-paste or `eval` directly. * * Used both for success-path hints (post-verb) and for typed-error * resolutions (in the error message + JSON output). */ interface NextStep { /** Short human-prose label, e.g. "Drop notes as you work". */ intent: string; /** Literal shell command, e.g. `mu task note foo "..."`. */ command: string; } /** * Marker interface for typed errors that carry actionable resolutions. * The handler checks this with a duck-typed `typeof err.errorNextSteps * === "function"` rather than instanceof so cross-realm errors (e.g. * thrown from a different module instance after a hot-reload) still * surface their nextSteps. */ interface HasNextSteps { errorNextSteps(): NextStep[]; } 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 mu state directory: * MU_STATE_DIR > $XDG_STATE_HOME/mu > ~/.local/state/mu */ declare function defaultStateDir(): string; /** * 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 on every open. Safe to call from many short-lived processes * concurrently — WAL mode handles cross-process writes. */ declare function openDb(options?: OpenDbOptions): Db; /** * 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 implements HasNextSteps { readonly detectedVersion: number; readonly requiredVersion: number; readonly name = "SchemaTooOldError"; constructor(detectedVersion: number, requiredVersion: number); errorNextSteps(): NextStep[]; } /** The schema version a fresh DB starts at. v11 adds tasks.substate * and the task_substates lookup table on top of v10's three-state * lifecycle (OPEN, IN_PROGRESS, CLOSED). See CHANGELOG.md. */ declare const CURRENT_SCHEMA_VERSION = 11; /** 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. Sorted; exactly 11 entries in v11. */ 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 six 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. * vcs_workspaces holds absolute paths — /home/... vs /Users/... * on a mixed macOS/Linux fleet. * machine_identity IS the per-machine identity. * schema_version local bookkeeping. * sync_peers local bookkeeping (per-peer watermarks). * task_substates seeded identically on every machine from * TASK_SUBSTATE_ROWS; code, not data. * 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 ["agents", "machine_identity", "ops", "schema_version", "sync_peers", "task_substates", "vcs_workspaces"]; 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 MuxSession { name: string; } interface MuxWindow { /** Backend-specific window handle (tmux `@1`). */ id: string; name: string; /** Session this window belongs to (only set by cross-session listings). */ sessionName?: string; } interface MuxPane { /** Backend-specific stable pane id (tmux `%15`). Never a volatile index. */ paneId: string; /** The agent's name, in mu's convention. */ title: string; /** Current foreground command (e.g. "claude", "node", "bash"). */ command: string; /** Window this pane lives in. Only set by cross-window listings. */ windowId?: string; /** Session this pane lives in. Only set by cross-session listings. */ sessionName?: string; } interface NewSessionOptions { detached?: boolean; windowName?: string; command?: string; /** Initial working directory for the first pane (`-c `). */ cwd?: string; /** Extra env vars to set in the new pane. On tmux this is `-e KEY=VALUE` * (tmux 3.0+), which sets the variable in the new pane's environment * without polluting the server's global env. */ env?: Record; } interface NewSessionWithPaneOptions { windowName: string; command: string; cwd?: string; detached?: boolean; /** Extra env vars to set in the new pane. */ env?: Record; } interface NewWindowOptions { /** Target session. Required if invoking outside an existing client. */ session?: string; /** Window name. Maps to the agent's `tab:` value (or its name if no tab). */ name: string; /** Command to run in the first pane. */ command: string; /** If true, do not switch focus. Defaults to true. */ detached?: boolean; /** Initial working directory (`-c `). */ cwd?: string; /** Extra env vars to set in the new pane. */ env?: Record; } interface SplitWindowOptions { /** Target window or pane (e.g. ":Backend" or "%15"). */ target: string; command: string; /** Horizontal split (side-by-side). Default true. */ horizontal?: boolean; detached?: boolean; /** Initial working directory for the new pane (`-c `). */ cwd?: string; /** Extra env vars to set in the new pane. */ env?: Record; } /** 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; } /** One argv the caller may execute to hand the terminal to the mux. */ 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; } /** 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 { /** Override the default delay between paste and Enter, in ms. */ delayMs?: number; /** * Override the pre-send readiness budget, in ms. 0 disables the wait * AND the post-send submit verification. */ 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 stayed stranded * in the input box. 'busy-at-deadline' — pane never quiesced within * the budget and the text stayed stranded. */ reason: "busy-at-deadline" | "paste-vanished"; message: string; } /** * What `startAgentInPane` needs to know. mu has already resolved the * command it WOULD have run in a create-and-run backend; a backend that * starts agents itself needs both that string and the `--cli` key it came * from, because those select different routes (see the herdr impl). */ interface StartAgentInPaneOptions { /** An existing pane, sitting at its interactive shell prompt. */ paneId: 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 otherwise have spawned. */ command: string; /** * Where `command` came from. A backend that resolves the executable * ITSELF (rather than running the string mu hands it) 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: "cli-key" | "env" | "explicit"; } interface CaptureOptions { /** * Number of trailing lines to capture. Omitted = full scrollback. * 0 = visible pane only. */ lines?: number; } /** * Extract the agent-name token from a (possibly composed) pane title. * mu's `composeAgentTitle` renders titles as `name · task_id`; * the agent name is always the first ' · '-separated token. Adopted * panes mu never re-titled have just the name — still parses. * * Backend-INDEPENDENT: the format is mu's, not any multiplexer's, so * every backend that can read a pane title parses it the same way. * Lives here rather than in an impl so migrated call sites can use it * without importing tmux. */ declare function parseAgentNameFromTitle(title: string): 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 implements HasNextSteps { name: string; errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly paneId: string; private readonly backend?; readonly name = "PaneNotFoundError"; constructor(paneId: string, backend?: MuxDiagnostics | undefined); errorNextSteps(): NextStep[]; } /** 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; paneNotFoundNextSteps(paneId: string): NextStep[]; } /** * 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 implements HasNextSteps { readonly tried: readonly string[]; readonly name = "NoMultiplexerError"; constructor(tried: readonly string[]); errorNextSteps(): NextStep[]; } /** * 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 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; listSessions(): Promise; sessionExists(name: string): Promise; newSession(name: string, opts?: NewSessionOptions): Promise; newSessionWithPane(name: string, opts?: NewSessionWithPaneOptions): Promise; killSession(name: string): Promise; listWindows(session?: string): Promise; newWindow(opts: NewWindowOptions): Promise; selectLayout(window: string, layout: string): Promise; listPanes(target?: string): Promise; listPanesInSession(session: string): Promise; splitWindow(opts: SplitWindowOptions): Promise; killPane(paneId: string): Promise; paneExists(paneId: string): Promise; paneTTY(paneId: string): Promise; setPaneTitle(paneId: string, title: string): Promise; getPaneTitle(paneId: string): Promise; currentAgentName(): Promise; /** Name of the session the CALLER is running inside, or undefined * when outside one. Backs the `mu-` rung of workstream * auto-detection, which is why it is identity and not topology. */ currentSessionName(): Promise; startAgentInPane?(opts: StartAgentInPaneOptions): Promise; sendToPane(paneId: string, text: string, opts?: SendOptions): Promise; capturePane(paneId: string, opts?: CaptureOptions): Promise; /** * The pane's runtime state as the mux itself reports it, or undefined * when the pane is gone or unclassified. tmux omits this capability; * herdr reports it directly. */ paneStatus?(paneId: string): Promise; enableMuPaneBordersForSession(session: string): Promise; enableMuPaneBordersForPane(paneId: string): Promise; /** Copy-pasteable shell line that lands the user on `target`. */ attachHint(target: AttachTarget): string; /** The same attach, as argv steps to spawn in order. */ attachCommands(target: AttachTarget): readonly MuxCommand[]; /** Backend-specific `PaneNotFoundError` remediation. */ paneNotFoundNextSteps(paneId: string): NextStep[]; /** 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); errorNextSteps(): NextStep[]; } /** * 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); errorNextSteps(): NextStep[]; } /** * 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); errorNextSteps(): NextStep[]; } /** * `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); errorNextSteps(): NextStep[]; } /** A `MuxBackend` method whose herdr implementation is owned by another * task. Never thrown on any path mu currently drives on herdr. */ declare class HerdrNotImplementedError extends Error { readonly name = "HerdrNotImplementedError"; constructor(method: string, owner: 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 (execa-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 aliases: tmux-specific names predating the MuxBackend * extraction. Same shapes, now owned by ./types.js. */ type TmuxSession = MuxSession; type TmuxWindow = MuxWindow; 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); errorNextSteps(): NextStep[]; } /** * 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; /** * Delay between bracketed-paste and Enter, in milliseconds. Claude/Codex/pi * process pasted text asynchronously; without this delay, Enter can arrive * before the agent has ingested the text. Defaults to 500; lower for tests, * raise for slow remotes via `MU_SEND_DELAY_MS`. */ declare function defaultSendDelayMs(): number; 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 (execa-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; declare function listSessions(): Promise; declare function sessionExists(name: string): Promise; declare function newSession(name: string, opts?: NewSessionOptions): Promise; /** * Create a tmux session AND its first window+pane in one atomic call. * Returns the new pane's stable id. Used by mu when spawning the first * agent in a workstream so we never end up with an empty `mu-` * session left behind by a failed spawn. */ declare function newSessionWithPane(name: string, opts: NewSessionWithPaneOptions): 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 " — 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 * Layer 3 of bug_test_suite_flake_leaks_isolation. * - "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; declare function listWindows(session?: string): Promise; /** * Create a new tmux window with one pane. Returns the new pane's stable * pane id (e.g. `%15`). */ declare function newWindow(opts: NewWindowOptions): Promise; /** * List ALL panes in a tmux session (across every window). Used by * reconciliation to find every pane in the workstream's session. * * 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. */ declare function listPanesInSession(session: string): Promise; /** * List panes in the current session, a specific window/session target, or * all panes across all sessions when `target` is the literal "*". */ declare function listPanes(target?: string): Promise; /** * Split a window and run a command in the new pane. Returns the new pane's * stable pane id. */ declare function splitWindow(opts: SplitWindowOptions): Promise; /** Idempotent: succeeds even if the pane is already gone. */ declare function killPane(paneId: string): Promise; declare function paneExists(paneId: string): Promise; declare function setPaneTitle(paneId: string, title: string): Promise; /** * Look up the window id (e.g. `@42`) that contains a given pane id * (e.g. `%15`). Used by spawn so we can apply window-scoped options * (`pane-border-status`) to the freshly created window. * * Returns undefined if the pane no longer exists. */ declare function getWindowIdForPane(paneId: string): Promise; /** * Enable the mu pane border on EVERY window currently in `session` * without overriding the user's pane-border format. Idempotent. Best-effort: * windows that have vanished mid-iteration are silently skipped. Used * by `mu workstream init` (covers the placeholder `_mu` window plus * any windows that already exist, e.g. on re-init of an upgraded * mu-pre-border session) and by `mu agent spawn` (covers the * just-created window so the border shows immediately on attach). * * No-op (returns 0) when `MU_BANNER_QUIET=1`. * * Returns the number of windows that received the option. */ declare function enableMuPaneBordersForSession(session: string): Promise; /** * Apply the mu pane border to the window containing `paneId`. This is * the spawn/adopt shape: callers have a pane id (from `new-window` or * from an adopt target), and need to resolve the enclosing window * before calling `enableMuPaneBorders` (a window-scoped option). * * Self-checks `MU_BANNER_QUIET` and swallows tmux errors — the border * is decorative; failing to set it is never load-bearing. */ declare function enableMuPaneBordersForPane(paneId: string): Promise; /** * Enable a one-line top pane border on a specific window/session target. * The user's inherited `pane-border-format` is left intact; mu owns the * title's durable context, not border presentation. Idempotent * (`set-option` is a write, not a toggle). * * IMPORTANT: tmux's `pane-border-status` is a **window** option, not a * session option. `set-option -t ` * only updates the active window at call time — windows created later * inherit from the GLOBAL value (which is `off` by default and which * we deliberately do NOT touch, since changing the global would * affect every other tmux session on the user's machine, including * dotfile-curated ones). * * Therefore mu must call this twice: * 1. At `mu workstream init` time on the placeholder `_mu` window * (so an attached operator sees a border immediately). * 2. On every `mu agent spawn` (which calls `tmux new-window`), * against the new window's id. * * The border is tmux chrome, not pane content: it doesn't scroll, it * survives copy-mode, and the inner CLI never sees it. * * Designed as the pane-border visual cue for mu-managed panes. */ declare function enableMuPaneBorders(target: string): Promise; /** * Look up the TTY device path for a pane (e.g. `/dev/ttys012` on macOS, * `/dev/pts/3` on Linux). Used by `mu agent kick` to find the * foreground process group on the pane's TTY so it can be signalled * directly — `tmux send-keys C-c` does NOT propagate to wrapped * subprocesses inside a CLI like pi/claude/codex (the CLI catches it * itself and treats it as a UI input). The escape hatch is signalling * the foreground pgid of the underlying TTY from outside the pane. * * Throws `PaneNotFoundError` when the pane id is invalid or the pane * has vanished. Throws `TmuxError` on any other tmux failure. */ declare function paneTTY(paneId: string): Promise; declare function getPaneTitle(paneId: string): Promise; /** * Read the title of the *current* pane (the one whose shell is running this * process), via $TMUX_PANE. Returns undefined when not inside tmux. Used by * `mu claim` to derive the agent identity from the pane title — the claim * protocol's zero-config identity step. */ declare function currentPaneTitle(): Promise; /** * Convenience: read the current pane's title and extract the agent name. */ declare function currentAgentName(): 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. */ declare function currentSessionName(): Promise; declare function selectLayout(window: string, layout: string): Promise; /** * Default pre-send readiness budget in ms. Override with * MU_SEND_READINESS_MS; 0 disables the wait entirely. * * 15s (vs spawn's 10s) because the blocking operation here is an LLM * round-trip inside the target TUI, not a process cold-start. pi's * post-`/new` "Naming session before closing…" step measured ~1.5s on a * warm session but is a model call, so it has no useful upper bound. */ declare function defaultSendReadinessMs(): number; /** True when the pane tail shows the agent working on a turn (rather * than a modal / re-init spinner). Only the last 40 lines count, so a * work marker scrolled far above cannot make a live modal look like an * in-flight turn. See awaitPaneQuiescence. */ declare function hasWorkMarker(scrollback: string): boolean; /** * Block until the pane is ready to ACCEPT input, or the budget expires. * Returns true if the pane quiesced, false on timeout. * * WHY THIS EXISTS (dogfood_send_after_new_dropped, reproduced 3/6 at * sleep=0.3s and 1/5 at sleep=2s against a real pi pane): * * `mu agent send W "/new"` makes pi run an ASYNC "Naming session before * closing…" step — an LLM call — before the new session exists. A * bracketed paste that arrives while that modal is up is ACCEPTED, but * the Enter that follows it is SWALLOWED. The text is therefore left * sitting in the new session's input box, typed but never submitted — * exactly the reported symptom: pane at needs_input, context 0.0%, no * error anywhere, and an orchestrator waiting on a task the agent never * started. * * (Verified rather than assumed: pasting during the modal and capturing * afterwards shows the probe string exactly ONCE, in the input box, on * 3 of 3 attempts. Re-sending Enter afterwards took it to 2 and moved * context 0.0% -> 2.4%, which is what makes recovery possible.) * * Because the blocker is a model call, no fixed sleep can be correct — * `sleep 2` is a coin flip, not a fix. The signal already existed: pi's * spinner is a Braille glyph, so the private input-timing helper can wait * for it to clear before sending. * * `needs_permission` counts as ready: a pane sitting on a confirm * dialog is waiting for a keystroke, and refusing to send would break * the documented "answer the prompt with `mu agent send`" flow. * * IMPORTANT — what this does NOT wait for: an agent that is BUSY doing * the work you gave it. Queuing a follow-up into a working agent is a * legitimate, documented pattern, and pi accepts it. Waiting on that * made every send into a working agent pay the full budget — measured * at 14.5s of 15s, versus milliseconds before. The two cases are told * apart by WHAT is busy, not by when: * * agent working — tail carries a work marker (`to interrupt)`, * `Working`). Send immediately; pi queues it. * modal / re-init — spinner with NO work marker, which is what pi's * "Naming session before closing…" looks like. * Wait it out; this is the one that eats the Enter. */ declare function awaitPaneQuiescence(paneId: string, budgetMs: number): Promise; /** * Send a single line of text to a pane and submit it. * * Sequence: * 0. wait until the pane is not mid-modal (see awaitPaneQuiescence) * 1. exit copy mode (silent if not in copy mode) * 2. load text into a uniquely-named tmux buffer * 3. paste with bracketed-paste mode (-p) so apps treat as literal text; * delete buffer after paste (-d); preserve LF (-r) * 4. wait MU_SEND_DELAY_MS (default 500) so the agent ingests the text * 5. send Enter as a real key event * 6. confirm the Enter took; re-send it if the text is stranded * * DELIVERY CONTRACT (dogfood_send_after_new_dropped): exit 0 must not * be able to mean "silently dropped". After Enter, the pane is checked * for text left STRANDED in the input box — typed but unsubmitted, * which is what a swallowed Enter looks like. If found, Enter is * re-sent (up to SUBMIT_RETRIES times), which recovers the prompt. * `onUndelivered` fires only if it is STILL stranded afterwards. * * Not a throw, deliberately: by then the text is in the agent's input * box, so the operator's next `mu agent read` shows it and a bare Enter * finishes the job. Failing the command would also break every existing * caller for a usually-recoverable condition. The warning is loud and * names the recovery step. * * Naive `send-keys ""` would let characters like /, ?, f, : be * interpreted by the agent's TUI or by tmux's copy mode. Always use this. */ declare function sendToPane(paneId: string, text: string, opts?: SendOptions): 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 (`-S -N`) */ declare function capturePane(paneId: string, opts?: CaptureOptions): Promise; declare function attachHint(target: AttachTarget): string; declare function attachCommands(target: AttachTarget): readonly MuxCommand[]; declare function paneNotFoundNextSteps(paneId: string): NextStep[]; /** * 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_HerdrCommandOverrideError = HerdrCommandOverrideError; declare const mux_HerdrCommandOverrideError: typeof HerdrCommandOverrideError; type mux_HerdrError = HerdrError; declare const mux_HerdrError: typeof HerdrError; type mux_HerdrNotImplementedError = HerdrNotImplementedError; declare const mux_HerdrNotImplementedError: typeof HerdrNotImplementedError; 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; 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_MuxSession = MuxSession; type mux_MuxWindow = MuxWindow; type mux_NewSessionOptions = NewSessionOptions; type mux_NewSessionWithPaneOptions = NewSessionWithPaneOptions; type mux_NewWindowOptions = NewWindowOptions; type mux_NoMultiplexerError = NoMultiplexerError; declare const mux_NoMultiplexerError: typeof NoMultiplexerError; type mux_PaneNotFoundError = PaneNotFoundError; declare const mux_PaneNotFoundError: typeof PaneNotFoundError; type mux_SendOptions = SendOptions; type mux_SendWarning = SendWarning; type mux_SplitWindowOptions = SplitWindowOptions; type mux_StartAgentInPaneOptions = StartAgentInPaneOptions; declare const mux_activeMux: typeof activeMux; 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_parseAgentNameFromTitle: typeof parseAgentNameFromTitle; 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_tmuxBackend: typeof tmuxBackend; declare namespace mux { export { type mux_AttachTarget as AttachTarget, type mux_CaptureOptions as CaptureOptions, mux_HerdrCommandOverrideError as HerdrCommandOverrideError, mux_HerdrError as HerdrError, mux_HerdrNotImplementedError as HerdrNotImplementedError, mux_HerdrSyntaxError as HerdrSyntaxError, mux_HerdrUnsupportedCliError as HerdrUnsupportedCliError, mux_HerdrWorkspaceGroupCloseError as HerdrWorkspaceGroupCloseError, 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, type mux_MuxSession as MuxSession, type mux_MuxWindow as MuxWindow, type mux_NewSessionOptions as NewSessionOptions, type mux_NewSessionWithPaneOptions as NewSessionWithPaneOptions, type mux_NewWindowOptions as NewWindowOptions, mux_NoMultiplexerError as NoMultiplexerError, mux_PaneNotFoundError as PaneNotFoundError, type mux_SendOptions as SendOptions, type mux_SendWarning as SendWarning, type mux_SplitWindowOptions as SplitWindowOptions, type mux_StartAgentInPaneOptions as StartAgentInPaneOptions, mux_activeMux as activeMux, mux_detectMux as detectMux, mux_herdrBackend as herdrBackend, mux_isHerdrStatusUsable as isHerdrStatusUsable, mux_muxByName as muxByName, mux_parseAgentNameFromTitle as parseAgentNameFromTitle, mux_resetHerdrExecutor as resetHerdrExecutor, mux_resetMux as resetMux, mux_setHerdrExecutor as setHerdrExecutor, mux_setMuxForTests as setMuxForTests, 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), runs status detection * against surviving panes, surfaces orphans. * * "report-only" Pure observation. Counts would-be-pruned ghosts * without deleting; skips status detection entirely * (no DB writes, no tmux title writes); surfaces * orphans (pure read). Used by `mu undo` (the * post-restore pass MUST NOT delete rows the snapshot * just restored — see * snap_undo_reconcile_destroys_recovered_agents) and * `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 workspace * 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. NOT auto-adopted. */ 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; elapsedMs: number; }; type AbortAgentOptions = { workstream: string; timeoutMs?: number; /** Control socket path. Default: derived from the agent's identity. */ socket?: string; }; declare function abortAgent(db: Db, name: string, opts: AbortAgentOptions): Promise; type VcsBackendName = "jj" | "sl" | "git" | "none"; interface RebaseResult { /** The ref the workspace was actually rebased onto (resolved * symbolic-or-revset → concrete name). For git that is the * resolveGitMainRef() symbolic ref; for jj/sl it's the literal * `trunk()` revset (or whatever the operator passed via fromRef). */ fromRef: string; /** Commit subjects (or descriptions) that got replayed, oldest-first. * Empty when the workspace was already at fromRef (no-op). */ replayed: string[]; /** Files / commits that conflicted during the rebase. Always * empty for a successful rebase — a non-empty conflicts list * means we threw WorkspaceConflictError before returning. The * field exists so the error's serialised payload can carry it. */ conflicts: string[]; } interface CommitSummary { /** Full commit / change id. */ sha: string; /** First-line description / subject. */ subject: string; /** Remainder of the commit message (may be empty). */ body: string; /** Author display name, when the backend exposes one. */ author: string; /** ISO-8601 author / commit timestamp. */ authorDate: string; /** Compact relative author time (e.g. "3m", "2d"). */ relTime: string; } interface ShowCommitResult { /** Captured VCS show output (possibly truncated). Empty string on error. */ text: string; /** True when stdout exceeded SHOW_COMMIT_MAX_CHARS and was clipped. */ truncated: boolean; /** Human-readable error message; omitted on success. */ error?: string; } interface CreateWorkspaceOptions$1 { /** The repository being branched from. Absolute path. */ projectRoot: string; /** Where to place the new workspace. Absolute path; must NOT exist. */ workspacePath: string; /** Optional commit / branch / changeset id to base off. Backend-specific: * git uses it as a `git worktree add`'s ref, jj as a revset, sl as a * rev. Undefined = current head. */ parentRef?: string; } interface CreateWorkspaceResult { /** The actual ref the workspace points at (resolved to a stable id * when possible). Stored on the row; useful for `mu workspace list` * and for `--commit` flows. May be null for backends that don't * expose a meaningful parent (e.g. `none`). */ parentRef: string | null; } interface FreeWorkspaceOptions$1 { workspacePath: string; /** If true, attempt to commit any pending changes BEFORE removal. * Backend-specific: jj auto-commits via `jj describe + jj new`, git * needs an explicit commit on the worktree, sl needs `sl commit`, * none has nothing to commit. If pending changes exist and `commit` * is false, the on-disk directory still gets removed and changes are * lost — the verb prints a clear warning. */ commit: boolean; } interface FreeWorkspaceResult$1 { /** The commit id that captured the pending changes, when `commit` was * true and there was something to commit. Otherwise undefined. */ committedRef?: string; /** True iff the on-disk path was actually removed (vs. already gone). */ removed: boolean; } interface VcsBackend { readonly name: VcsBackendName; /** True iff this backend should handle `projectRoot`. Implementations * check for the relevant marker dir (`.jj`, `.sl`, `.git`); `none` * always returns true and is consulted last. */ detect(projectRoot: string): Promise; createWorkspace(opts: CreateWorkspaceOptions$1): Promise; freeWorkspace(opts: FreeWorkspaceOptions$1): Promise; /** * Count commits that the project's default branch ("main") has but * `ref` does not — i.e. how many commits `ref` is BEHIND main. * * Used by `mu workspace list` and `mu state` to surface staleness * (bug_workspace_stale_parent_silent_drift). Cheap, pure-observation: * NO automatic fetch. We compare against whatever main resolves to in * the workspace's LOCAL refs cache. The user can `git fetch` (or * equivalent) themselves if they want a fresher number. * * Returns null when: * - main / trunk cannot be resolved (no origin/HEAD, no origin/main, * no origin/master, no trunk() bookmark, etc.) * - the underlying VCS command fails for any reason (detached worktree, * missing refs, the `none` backend which has no notion of "main") * * Callers treat null as "unknown — render — — and don't warn". */ commitsBehind(workspacePath: string, ref: string): Promise; /** * Rebase the workspace onto `fromRef` (or the backend's tracked * base when undefined: `origin/HEAD` for git, `trunk()` for jj/sl). * Returns the resolved ref + replayed commits + conflicts list. * * Backend-specific behaviour: * - git: refuses on dirty WC (WorkspaceDirtyError); fetches first; * `git rebase `. On conflict, aborts the rebase and throws * WorkspaceConflictError so the operator resolves manually. * - jj: always-snapshotted, so dirty is never an issue. After * `jj rebase -d ` the conflict-set is queried via * `jj log -r 'conflict()'`. Conflicts surface as * WorkspaceConflictError without an abort (jj's conflict markers * persist as commits; the operator resolves in-place). * - sl: similar to jj. `sl rebase -d `; conflicts via * `sl resolve -l`. On dirty WC sl errors itself; we wrap that * into WorkspaceDirtyError. * - none: throws WorkspaceVcsRequiredError unconditionally. * * Surfaced by fb_workspace_recycle_verb: dogfood between waves * needed `close → free → spawn` to refresh a worker against new * main; that killed the worker's LLM context. `refresh` updates * the on-disk dir without touching the agent or pane. */ rebaseTo(workspacePath: string, fromRef?: string): Promise; /** * Cheap "is the working copy clean?" probe used by close-auto-free * (allow_mu_agent_close_without_discard). Definition: ZERO uncommitted * changes (no working-tree modifications, no staged changes, no * untracked-not-ignored files). Pure observation; no fetch, no commit. * * Backend-specific: * - git: empty `git status --porcelain` output. * - jj: jj is auto-snapshotted, so the @ commit IS the WC; clean * here means @ has no diff from its parent (empty `jj diff * -r @ --summary`). A description-only difference still * counts as clean. * - sl: empty `sl status` output. * - none: meaningless (cp -a snapshot has no notion of * "committed" vs "uncommitted"); always returns true so the * close-auto-free path treats every none-workspace as * eligible for silent free (no commits can be lost; the only * loss is local file edits, which the operator implicitly * accepts by closing the agent). * * Returns false on any backend command failure — be conservative * (we'd rather refuse a close than auto-free a workspace whose * cleanliness we couldn't verify). */ isClean(workspacePath: string): Promise; /** * List commits the workspace has on top of `baseRef`, oldest-first. * Used by `mu workspace commits` (fb_workspace_commits_verb) to * promote the dogfood-painful * cd $(mu workspace path X) && git log ..HEAD * incantation into a typed verb that knows the workspace's * parent_ref. The CommitSummary fields survive subjects/bodies with * embedded newlines (NUL-delimited record format on the wire). * * `none` throws WorkspaceVcsRequiredError. Returns `[]` when the * workspace is exactly at baseRef (no commits since fork). Throws * on backend command failure (unknown ref, missing repo). */ commitsSinceBase(workspacePath: string, baseRef: string): Promise; /** Last N commits on the project root, newest-first. Used by the * TUI Commits card / popup. Unlike commitsSinceBase, this is NOT * a per-workspace since-fork query. */ recentCommits(projectRoot: string, limit: number): Promise; /** Show one commit / change from the project root, capped for TUI * rendering. Backend-specific equivalent of `git show `. */ showCommit(projectRoot: string, sha: string): Promise; /** * Return the list of dirty (uncommitted / unstaged / untracked-not- * ignored) paths in the workspace. Empty array = clean. * * Used by `mu workspace list`'s dirty decoration and by the * dirty-check `rebaseTo` does internally before a refresh. * * Backend semantics: * - git: `git status --porcelain` (working-tree + staged + * untracked-not-ignored, mirroring the rebaseTo path). * - sl: `sl status` parsed for non-empty output. * - jj: always-snapshotted, so no concept of "dirty" — returns []. * - none: cp -a snapshots have no VCS, so we can't decide "dirty"; * returns [] so the caller doesn't refuse for an unanswerable * question. * * Throws on backend command failure (the operator should see a * real error, not a silent "clean"). */ listDirtyFiles(workspacePath: string): Promise; } declare const gitBackend: VcsBackend; declare const jjBackend: VcsBackend; declare const noneBackend: VcsBackend; declare const slBackend: VcsBackend; /** Return the backend that should handle projectRoot. Walks BACKENDS * in precedence order; never returns undefined because noneBackend * always claims. */ declare function detectBackend(projectRoot: string): Promise; /** Look up a backend by name. Throws on unknown name. Used by * `mu agent spawn --workspace-backend ...` to honour an explicit * override. */ declare function backendByName(name: VcsBackendName): VcsBackend; /** * 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` * (fb_agent_spawn_no_validation, part C). */ 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 * prestageWorkspace so a bad --cli never creates an orphan workspace. */ declare function checkCommandResolvable(command: string): Promise; interface SpawnAgentOptions { name: string; workstream: string; /** Defaults to "pi". 0.1.0 only really supports "pi" but the column * accepts any string for forward-compat with future multi-CLI support * (claude/codex). */ 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 for the spawned pane (`tmux -c `). * When `workspace: true` is passed, this is ignored — the workspace * path is used instead. */ cwd?: string; /** Override the tmux session name. Defaults to `mu-`. */ tmuxSession?: string; /** Auto-create a VCS workspace for this agent before spawning the * pane and use the workspace path as cwd. Backend defaults to * detection (jj > sl > git > none). */ workspace?: boolean; /** Force a specific VCS backend (only meaningful with `workspace: true`). */ workspaceBackend?: VcsBackendName; /** Optional ref to base the workspace on (only meaningful with * `workspace: true`). Backend-specific. */ workspaceFrom?: string; /** Project root the workspace branches from (only meaningful with * `workspace: true`). Defaults to `process.cwd()`. */ workspaceProjectRoot?: string; /** Run the control-socket handshake for pi agents (default true). * `false` skips it and reports `ctl: "skipped"`. */ ctl?: boolean; } /** 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; }; /** * Spawn a new agent in its tmux pane and register it in the DB. * * Phases: * 1. Validate name + uniqueness. * 2. If --workspace: prestageWorkspace() (placeholder agent row + * workspace dir + workspace row). * 3. createOrReusePane() in the workspace path (or opts.cwd). * 4. setPaneTitle + enableMuPaneBordersForPane. * 5. finalizeAgentRow() — patch placeholder pane_id to real (workspace * path), or insert a fresh agent row (no-workspace path). * 6. awaitSpawnLiveness(). * * Failure between any of (3)–(6) calls rollbackSpawn() to undo the * pane + row + workspace. 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; interface AdoptAgentOptions { /** tmux pane id (e.g. '%15'). Must already exist on the tmux server. */ paneId: string; /** Workstream to adopt the pane into. The pane MUST be in the * matching tmux session (`mu-`); cross-session adopt is * rejected. */ workstream: string; /** Override the pane's title with this name. When omitted, the pane's * current title becomes the agent name (zero-config adoption). */ name?: string; /** Defaults to 'pi' via the schema DEFAULT. */ cli?: string; /** 'full-access' (default) or 'read-only'. */ role?: string; /** Override the tmux session lookup. Defaults to `mu-`. */ tmuxSession?: string; } interface AdoptAgentResult { agent: AgentRow; /** True when the pane already had a matching agents row — the call * was a no-op (idempotent). */ alreadyAdopted: boolean; /** The pane title before adopt, or null if the pane had no title. */ previousTitle: string | null; /** The title the pane was set to (== agent.name post-adopt). Equal to * previousTitle when no retitle happened. */ paneTitleSetTo: string; /** One probe of the derived control socket; "skipped" for non-pi CLIs. * An adopted pi pane usually lacks MU_CTL_SOCK, so this is "missing". */ ctl: SpawnCtl; ctlSocket: string; } /** * Register an existing tmux pane as a managed mu agent. Inverse of the * 'orphan' state surfaced by `mu agent list`: a pane that looks like an * agent (running pi/claude/codex) but has no DB row. * * Identity contract (matches the claim protocol invariant): * - Post-adopt, the pane's title equals the agent's name. * - When `name` is omitted, the pane's existing title becomes the * agent name verbatim. Adopting a pane titled 'pi' would fail name * validation — caller must supply --name in that case. * * Idempotent: adopting the same pane twice with the same name is a * no-op (returns alreadyAdopted=true). Adopting a different pane under * an existing agent name throws AgentExistsError. * * Validation order (matches the design in note #100): * 1. Pane id format -> assertValidPaneId via paneExists / setPaneTitle * 2. Pane exists -> PaneNotFoundError * 3. Pane is in session -> AgentNotInWorkstreamError (cross-session) * 4. Resolved name OK -> isValidAgentName / Error('agent name invalid') * 5. Idempotent check -> if pane already owned by an agent of this * name, return alreadyAdopted=true * 6. Name not taken -> AgentExistsError (else) * 7. Insert + retitle. * * Status starts at 'free' — reconcile/detect will update it on the next * `mu agent list` based on actual pane content (the pi prompt yields * 'free'; an agent mid-thought yields 'busy'; etc.). We don't run * detection inline here because the caller may not have $TMUX, and * adoption shouldn't depend on a captured-pane probe succeeding. */ declare function adoptAgent(db: Db, opts: AdoptAgentOptions): 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 `prestageWorkspace` so a typo in `--cli` never leaves an * orphan workspace dir behind. * * Source: feedback ws task `fb_agent_spawn_no_validation`. 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 (workspace, 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 implements HasNextSteps { 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 nextSteps hint can recommend * exporting 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 nextSteps hint can recommend * exporting it. */ envVarChecked: string); errorNextSteps(): NextStep[]; } declare class AgentExistsError extends Error implements HasNextSteps { readonly agentName: string; readonly name = "AgentExistsError"; constructor(agentName: string); errorNextSteps(): NextStep[]; } declare class AgentNotFoundError extends Error implements HasNextSteps { 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 workspace path -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 workspace path -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); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly agentName: string; readonly expectedWorkstream: string; readonly actualWorkstream: string; readonly name = "AgentNotInWorkstreamError"; constructor(agentName: string, expectedWorkstream: string, actualWorkstream: string); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { 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 spawned command is an ssh hop: remote causes, not wrapper locks. */ readonly remote: boolean; constructor(agentName: string, paneId: string, scrollback: string | undefined, /** The command the pane ran; an `ssh ...` command gets remote hints. */ command?: string | undefined); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { 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); errorNextSteps(): NextStep[]; } /** * Thrown when `closeAgent` is called on an agent that has an associated * workspace AND the caller didn't explicitly opt into discarding it. * * Background: the FK on `vcs_workspaces.agent` cascades on agent * delete, so a naive `closeAgent` drops the workspace registry row * but leaves the on-disk dir orphaned (mu can't see it via * `mu workspace list / free / path` afterwards). Surfaced during * the multi-agent dogfood teardown when three workspaces went * orphaned silently. * * The fix: refuse close if a workspace exists; force the caller to * decide. Two actionable resolutions: * - `mu workspace free ` first, then close cleanly. * - `mu agent close --discard-workspace` to free the * workspace AND close the agent in one shot (lossy: pending * changes in the workspace are gone). * * Maps to exit code 4 (conflict) via the cli.ts handler. */ declare class WorkspacePreservedError extends Error implements HasNextSteps { readonly agentName: string; readonly workspacePath: string; readonly name = "WorkspacePreservedError"; constructor(agentName: string, workspacePath: string); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly agentName: string; readonly workstream: string; readonly socket: string; readonly kind: "missing" | "refused"; readonly name = "AgentCtlUnreachableError"; constructor(agentName: string, workstream: string, socket: string, kind: "missing" | "refused"); errorNextSteps(): NextStep[]; } /** * `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 implements HasNextSteps { readonly agentName: string; readonly workstream: string; readonly cli: string; readonly name = "AgentAbortNeedsCtlError"; constructor(agentName: string, workstream: string, cli: string); errorNextSteps(): NextStep[]; } /** The abort was delivered but pi did not settle within the timeout. */ declare class AgentAbortTimeoutError extends Error implements HasNextSteps { readonly agentName: string; readonly workstream: string; readonly timeoutMs: number; readonly name = "AgentAbortTimeoutError"; constructor(agentName: string, workstream: string, timeoutMs: number); errorNextSteps(): NextStep[]; } /** * `mu agent send --fresh` on an agent without a control socket (non-pi * CLI, or forced `--via mux`): there is no one-operation new session. */ declare class AgentFreshNeedsCtlError extends Error implements HasNextSteps { readonly agentName: string; readonly workstream: string; readonly cli: string; readonly name = "AgentFreshNeedsCtlError"; constructor(agentName: string, workstream: string, cli: string); errorNextSteps(): NextStep[]; } /** * `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 implements HasNextSteps { 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); errorNextSteps(): NextStep[]; } /** * 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 does not silently paste into a * pi pane: `--via mux` is the explicit way to type it there. */ declare class AgentSlashCommandUnsupportedError extends Error implements HasNextSteps { 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[]); errorNextSteps(): NextStep[]; } /** The signal set kick supports. SIGINT is graceful (matches Ctrl-C * semantics — what the operator probably wanted in the first place); * SIGTERM is the polite escalation; SIGKILL is the unblockable * hammer. We deliberately don't expose arbitrary signals — the * three above are the actionable ones for "interrupt a wedged * foreground tool subprocess." */ type KickSignal = "SIGINT" | "SIGTERM" | "SIGKILL"; declare function isKickSignal(s: string): s is KickSignal; /** * Thrown when the foreground pgid lookup on a pane's TTY yields * either no rows at all (the pane is sitting at an idle shell with * no foreground job) or only the wrapping shell itself (the LLM CLI * — pi/claude/codex — is the foreground; signalling it would close * the agent, which is what `mu agent close` is for). * * Maps to the generic exit code 1 in handle.ts (this is a * runtime-state condition, not a typed not-found / conflict). */ declare class NoForegroundProcessError extends Error implements HasNextSteps { readonly agentName: string; readonly tty: string; readonly reason: "no-foreground" | "shell-only"; readonly name = "NoForegroundProcessError"; constructor(agentName: string, tty: string, reason: "no-foreground" | "shell-only"); errorNextSteps(): NextStep[]; } interface KickProcessExecResult { stdout: string; stderr: string; exitCode: number | null; } type KickProcessExecutor = (cmd: string, args: readonly string[]) => Promise; /** Install a custom executor (for tests). Returns the previous one so * tests can restore cleanly. */ declare function setKickProcessExecutor(executor: KickProcessExecutor): KickProcessExecutor; /** Restore the real executor. */ declare function resetKickProcessExecutor(): void; interface PsRow { pid: number; pgid: number; /** ps's `stat` (or `state`) field. The presence of `+` means * "foreground process group on its controlling tty". */ stat: string; /** Process command (just the comm; truncated, used for diagnostics). */ comm: string; } /** * Parse `ps -t -o pid=,pgid=,stat=,comm=` output. Each non-blank * line is one process: four whitespace-separated fields. Defensive * about leading whitespace and command names with embedded spaces * (the comm is the LAST field — join the tail). */ declare function parsePsTtyOutput(output: string): PsRow[]; /** * Resolve the foreground process group id for a TTY device path. The * canonical signal `ps`'s `stat` field uses is `+` (BSD/Darwin AND * Linux procps). We pick the first row whose stat contains `+`; its * `pgid` is the foreground pgid of that controlling terminal. * * Returns: * - `{ kind: "ok", pgid, fgRow }` on success * - `{ kind: "no-foreground" }` when no row carries `+` AND there * are no candidate rows at all * - `{ kind: "shell-only", pgid, fgRow }` when the foreground pgid * resolves to a shell whose comm is the agent's wrapping CLI * (caller decides whether to refuse — kick refuses) * * The wrapping-CLI guard is intentionally narrow: we only refuse * when the foreground process command matches one of the known * pi/claude/codex/zsh/bash shapes. Anything else (a `find`, a * `cargo build`, a `python script.py`) is exactly what we want to * signal — that's the unbounded-tool case the verb was built for. */ interface ForegroundLookup { kind: "ok" | "shell-only" | "no-foreground"; pgid?: number; fgRow?: PsRow; /** All rows ps returned for the tty, for diagnostics / tests. */ rows: PsRow[]; } declare function foregroundPgid(tty: string): Promise; interface KickAgentOptions { workstream: string; /** Defaults to SIGINT (matches Ctrl-C semantics). */ signal?: KickSignal; } interface KickAgentResult { agentName: string; paneId: string; /** TTY device path the foreground pgid was resolved against. */ tty: string; /** The pgid we signalled. */ signaledPgid: number; signal: KickSignal; /** The comm of the foreground process at the time of signal — useful * diagnostic in the event log ("we kicked a `find`, not a `cargo`"). */ foregroundComm: string; } /** * Send `signal` to the foreground process group of an agent's pane * TTY. Default signal is SIGINT. * * Errors: * - `AgentNotFoundError` — the agent doesn't exist in this workstream. * - `PaneNotFoundError` (from paneTTY) — the agent's pane has vanished. * - `NoForegroundProcessError` — pane has no foreground job, OR the * foreground is the wrapping CLI itself (refuse; use `mu agent close`). * * Emits an `agent kick (signal=..., pgid=..., comm=...)` event * on success. */ declare function kickAgent(db: Db, name: string, opts: KickAgentOptions): Promise; type Transport = "ctl" | "mux"; type SendResult = { transport: Transport; state?: CtlState; /** Set when the text ran as a pi session command over ctl. */ command?: CtlCommandName; }; type TransportSendOptions = SendOptions & { /** How a busy pi queues the message. Default followUp. ctl only. */ mode?: "steer" | "followUp"; /** Force a transport instead of choosing by agent and text. */ via?: Transport; /** 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; }; /** * 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. */ declare function expectsCtl(agent: Pick, sock?: string): boolean; declare function sendViaTransport(agent: AgentRow, text: string, opts: TransportSendOptions & { socket: string; }): Promise; interface AgentRow { 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; /** 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; } 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; } 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[]; /** Build the pane title for `agent` based on current DB state. * Pure (no tmux side effect; no DB write). Read-only on the DB. */ declare function composeAgentTitle(db: Db, agent: AgentRow): string; /** Push a fresh pane title for `agentName`. Best-effort — a missing * agent, a placeholder pane id, or a tmux failure are all swallowed * silently (titles are decorative; never block the calling verb). */ declare function refreshAgentTitle(db: Db, agentName: string, workstream: string): Promise; /** * 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 * `task reap` event in `agent_logs`. 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): boolean; 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 or `via: "mux"` goes through the mux paste path. See * src/agents/transport.ts. */ declare function sendToAgent(db: Db, name: string, text: string, opts: TransportSendOptions & { workstream: 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 CloseAgentOptions { /** * Lossy override: when true, free the agent's workspace BEFORE * deleting the agent regardless of whether it's clean. (We control * the order rather than relying on FK cascade, which leaves the * on-disk dir orphaned.) Any pending changes / commits since fork * are gone unless the caller frees with `--commit` separately first. * * When false (default), behaviour depends on workspace state: * - clean (no uncommitted changes AND no commits since fork): * silently auto-free. allow_mu_agent_close_without_discard. * - dirty (uncommitted changes OR commits since fork): throw * WorkspacePreservedError so the caller decides explicitly. * Surfaced as a real bug in the multi-agent dogfood teardown. */ discardWorkspace?: boolean; } interface CloseAgentResult { killedPane: boolean; deletedRow: boolean; /** True iff the agent had an associated workspace AND we proactively * freed it — either because the caller passed `discardWorkspace: * true` (lossy) or because the workspace was clean and we * auto-freed (allow_mu_agent_close_without_discard). False on the * no-workspace path (nothing to free) and on the refused path (we * threw before doing anything). */ workspaceFreed: boolean; /** True iff `workspaceFreed` was triggered by the clean-workspace * auto-free path (no uncommitted changes AND no commits since * fork) rather than the explicit `discardWorkspace: true` override. * Lets the CLI render an accurate message ("auto-freed (clean)" * vs "workspace discarded") and gives JSON consumers a stable * signal. False on every other path. */ workspaceAutoFreedClean: boolean; } /** * Close an agent: kill its tmux pane and remove its DB row. Idempotent: * - if the agent doesn't exist in the DB, returns a no-op result * - if the tmux pane is already gone, killPane swallows the error * * Workspace handling: closing an agent and freeing its workspace are * separate concerns (agent lifecycle vs disk artifacts). Three cases: * * - No workspace: close proceeds normally. * - Workspace exists AND is CLEAN (no uncommitted changes, no * commits since fork): silently auto-free (so a workspace that * contains nothing worth preserving doesn't make the operator * type --discard-workspace just to clean it up). Surfaced by * allow_mu_agent_close_without_discard — a misconfigured-spawn * teardown was needlessly forced through the lossy flag. * - Workspace exists AND has either uncommitted changes OR commits * since fork: REFUSE with WorkspacePreservedError so the operator * decides explicitly. Two resolutions: * 1. `freeWorkspace(db, name)` first, then `closeAgent(db, name)`. * Preserves the option to `--commit` pending changes. * 2. `closeAgent(db, name, { discardWorkspace: true })`. * One-shot; lossy. * * The CLI surfaces these as the two actionable nextSteps on the * `WorkspacePreservedError` thrown by the refuse path. */ declare function closeAgent(db: Db, name: string, opts: CloseAgentOptions & { 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` and `mu undo` pass `"report-only"`: count drift, * mutate nothing. `mu undo` MUST use this so a post-restore * reconcile doesn't delete the rows the snapshot just restored * (snap_undo_reconcile_destroys_recovered_agents). * * Mid-spawn placeholders (pane id `%pending-`) are protected * directly in reconcile's prune loop, independent of mode * (bug_agent_spawn_workspace_fk_failure). * * 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); read-only diagnostic / restore paths * (`mu doctor`, `mu undo`) call it with `mode: "report-only"` to mutate * nothing at all. */ declare function listLiveAgents(db: Db, opts: ListLiveAgentsOptions): 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, for `mu undo`. */ 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 losing op, a duplicate, or a * no-op — all three are legitimate and non-exceptional. */ changed: boolean; /** Fields actually written, for a 'put' on task / workstream. Empty * when every field in the payload lost its HLC comparison. */ appliedFields: string[]; /** Why nothing happened, when `changed` is false. */ skipped?: "older-than-tombstone" | "older-than-current" | "already-present" | "absent"; } /** * Apply one op to the portable tables. * * SYNCHRONOUS, for the same reason `withOpContext` is: the op context is * a per-connection temp table, so two interleaved async apply scopes * would clobber each other's `applying` flag with no way to tell whose * suppression was in force. 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. * * Does NOT insert the op into `ops` — that is the caller's job (v2-sync, * which owns segment bookkeeping and the (machine_id, hlc) dedupe). This * function is deliberately only "make the tables reflect this op", so it * can be called on a replay of ops already in the log without * double-recording them. Provenance queries exclude the op's own HLC * precisely so the order of those two steps does not matter. * * 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. * * HLC order is what makes per-field LWW correct without any extra * bookkeeping: process oldest-first and each field simply ends up * holding the newest write. Callers may pass ops in arrival order. * * Grow-only entities are order-insensitive by construction, so a single * sort is enough for every rule here. */ declare function applyOps(db: Db, ops: readonly Op[]): ApplyResult[]; /** * DDL for the three per-connection temp tables the triggers read. * Created by `installCapture` before the triggers that reference them. * * _op_ctx the **op context** (docs/VOCABULARY.md): group_id / * actor / intent / applying. Exactly ONE row, seeded with * defaults so a mutation occurring outside any SDK context * is still CAPTURED, just with a null intent. Fail safe, * never fail silent. * * _op_clock a one-row scratchpad holding "now" for the current * trigger firing. The HLC advance needs to compare `now` * against `last_wall` and then reuse the SAME `now` to * write it; calling unixepoch() twice could straddle a * millisecond boundary and mint a non-monotonic pair. So * the value is stashed once per op and read from here. * * _op_dying natural keys of rows that are mid-DELETE — see the * DELETE section below. Keyed by (kind, id). * * Temp tables are per-connection and mu is one connection per * short-lived process, so there is no cross-process leakage and no * cleanup to schedule. */ declare const OP_CTX_DDL = "\nCREATE TEMP TABLE IF NOT EXISTS _op_ctx (\n group_id TEXT,\n actor TEXT,\n intent TEXT,\n applying INTEGER NOT NULL DEFAULT 0\n);\nCREATE TEMP TABLE IF NOT EXISTS _op_clock (\n now_ms INTEGER NOT NULL DEFAULT 0\n);\nCREATE TEMP TABLE IF NOT EXISTS _op_dying (\n kind TEXT NOT NULL,\n id INTEGER NOT NULL,\n key TEXT NOT NULL,\n PRIMARY KEY (kind, id)\n);\n"; /** The capture trigger DDL. Built once at module load; pure string. */ declare const CAPTURE_TRIGGER_DDL: string; /** * Create the op-context temp tables and the capture triggers on this * connection. Called by `openDb` on every non-readonly open. * * Everything here is idempotent (CREATE TEMP TABLE / TRIGGER IF NOT * EXISTS + guarded seed INSERTs) so calling it twice is harmless. * * No transaction and no 'already exists' swallow, unlike * `applySchema`: the temp schema is PRIVATE to this connection, so the * concurrent-openDb race that forced those measures on the main schema * cannot occur here. Two processes opening the same file each build * their own temp schema without contending. */ 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 { exec(sql: string): unknown; prepare(sql: string): { run(...params: readonly unknown[]): unknown; get(...params: readonly unknown[]): unknown; }; } 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. Closed: `rejected` = the proposal was declined; * `wontfix` = valid, but not worth doing. */ declare const TASK_SUBSTATES: { readonly OPEN: readonly ["todo", "parked"]; 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 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; } 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. * * Surfaced in `fb_umbrella_no_auto_close` (impact=60): 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; * the CLI renders the skip with a Next: hint pointing at * `mu task wait`. 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". Any CLOSED/* satisfies edges. */ as?: CloseSubstate; /** Required (non-empty) unless `as` is "done"; stored as a * `: ` note 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. * Skipped * for the idempotent no-op (already CLOSED) so we don't accumulate * empty-delta snapshots on retry loops. * * 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: EvidenceOption & { 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/parked → OPEN/todo. A no-op (`changed: false`) on any other pair. */ declare function unparkTask(db: Db, localId: string, opts: EvidenceOption & { 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 EvidenceOption { /** 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; } /** * Release a task: clear `tasks.owner`. * * Status side-effects (review_release_open_in_progress_inconsistency): * - 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 EvidenceOption { /** 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 / $MU_SESSION. */ workstream: string; /** * Override the agent name. If omitted, resolved from the ambient * environment via `resolveWorkerIdentity()`: `$MU_AGENT_NAME` first, * then the current pane's title. * * 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; /** * Anonymous claim: write `owner = 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 `agent_logs.source` * for the auto-emitted `task claim` event — 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. Current pane title (when `$TMUX_PANE` is set). * 3. `$USER`. * 4. The literal string 'unknown'. * * 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 agent_logs event — 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` or from $TMUX_PANE's * pane title. The name MUST exist in the agents table (FK on * tasks.owner). Sets `owner = `. 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 = NULL`. * Records the actor in `agent_logs.source` 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. tmux pane title (the pane-title identity step). Works * when running inside any pane mu manages OR adopted. * 3. $USER (when running outside tmux entirely). * 4. The literal 'orchestrator' as a last-resort default. * * Why prefer env over pane title: pane titles are a tmux-server-wide * resource that anything can rewrite. The env var is set per-pane at * spawn time and is unforgeable from outside without explicit * `--actor` override. Pane title is the only identity available for * adopted panes that didn't go through mu's spawn path. */ declare function resolveActorIdentity(): Promise; interface TaskRow { /** Per-workstream-unique TEXT name. The operator-facing identifier. */ name: string; /** Foreign-name reference to the owning workstream. */ workstreamName: string; title: string; status: TaskStatus; /** Qualifies status (e.g. OPEN/parked, CLOSED/wontfix). Never null. */ substate: TaskSubstate; impact: number; effortDays: number; /** Foreign-name reference to the owning agent (NULL when unowned). */ ownerName: string | null; createdAt: string; updatedAt: string; } interface TaskNoteRow { author: string | null; content: string; createdAt: string; } 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; interface AddTaskOptions { localId: string; workstream: string; title: string; /** 1..100; enforced by schema CHECK. */ impact: number; /** > 0; enforced by schema CHECK. */ 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[]; } /** * 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 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; } declare function addNote(db: Db, taskLocalId: string, content: string, opts: AddNoteOptions): { author: string | null; content: string; createdAt: string; }; 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; } interface DeleteTaskOptions { /** When true, return the cascade preview (would-be edge / note * counts) without mutating and without snapshotting. The CLI uses * this to power the bare `mu task delete ` two-phase pattern * (mirrors `mu workstream teardown` / `mu snapshot prune`). Surfaced * by feedback ws task * fb_task_delete_no_yes (impact=30): 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 and without taking a snapshot (no mutation = no * snapshot — same reasoning that gates the closeTask snap on the * idempotent no-op path). 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 const WORKSPACE_STALE_THRESHOLD = 10; declare function isWorkspaceStale(behind: number | null | undefined): boolean; interface WorkspaceRow { agentName: string; workstreamName: string; backend: VcsBackendName; path: string; parentRef: string | null; createdAt: string; /** How many commits the workspace's parent_ref is behind the project's * default branch HEAD, as of the last time the workspace's local refs * cache was updated. Undefined when not yet computed (the listWorkspaces * fast path leaves it unset; call decorateWithStaleness to populate). * Null when staleness was queried but cannot be computed (no main found, * none-backend, missing parent_ref, command failure). */ commitsBehindMain?: number | null; /** True when the workspace has uncommitted / unstaged / untracked-not- * ignored files, as observed by the backend's `listDirtyFiles`. * Undefined when not yet computed (the listWorkspaces fast path leaves * it unset; call decorateWithDirty to populate). Null when the dirty * check could not be performed (backend command failure). For jj / * none backends — which have no operator-visible "dirty" concept — * this is always false (their listDirtyFiles returns []). */ dirty?: boolean | null; } declare class WorkspaceExistsError extends Error implements HasNextSteps { readonly agent: string; readonly name = "WorkspaceExistsError"; constructor(agent: string); errorNextSteps(): NextStep[]; } declare class WorkspaceNotFoundError extends Error implements HasNextSteps { readonly agent: string; readonly name = "WorkspaceNotFoundError"; constructor(agent: string); errorNextSteps(): NextStep[]; } /** * Thrown by createWorkspace when the on-disk path it would create is * already occupied. Distinct from WorkspaceExistsError (which is about * the DB row) so the recovery is clear: the dir is orphaned (no DB * row points at it) and needs cleanup. * * Maps to exit code 4 (conflict). */ declare class WorkspacePathNotEmptyError extends Error implements HasNextSteps { readonly agent: string; readonly workstream: string; readonly workspacePath: string; readonly name = "WorkspacePathNotEmptyError"; constructor(agent: string, workstream: string, workspacePath: string); errorNextSteps(): NextStep[]; } /** * Thrown by createWorkspace when the resolved projectRoot is the * user's $HOME. * * Maps to exit code 4 (conflict). */ declare class HomeDirAsProjectRootError extends Error implements HasNextSteps { readonly agent: string; readonly workstream: string; readonly homeDir: string; readonly name = "HomeDirAsProjectRootError"; constructor(agent: string, workstream: string, homeDir: string); errorNextSteps(): NextStep[]; } /** * Compose the canonical on-disk path for an agent's workspace. Used by * createWorkspace and reachable from `mu workspace path` so the user * can `cd $(mu workspace path foo)` even before the directory exists. */ declare function workspacePath(workstream: string, agent: string): string; /** Root dir for a workstream's workspaces — the parent of all * per-agent workspace dirs. Used by listWorkspaceOrphans to scan * the filesystem. */ declare function workspacesRoot(workstream: string): string; interface WorkspaceStaleness { agentName: string; workstreamName: string; commitsBehindMain: number | null; isStale: boolean; } interface CreateWorkspaceOptions { agent: string; workstream: string; /** Project root to branch from. Defaults to the current working * directory (the `mu` invocation site, which is normally what the * user wants). */ projectRoot?: string; /** Override backend detection. Default: walk `detectBackend`. * Accepts either a name ("jj" / "sl" / "git" / "none") OR a * pre-built `VcsBackend` object — the object form lets tests inject * a fresh fake backend without mutating the exported singletons. */ backend?: VcsBackendName | VcsBackend; /** Optional ref to base the workspace on. Backend-specific. */ parentRef?: string; } /** * Create a fresh workspace for an agent. Allocates the on-disk * directory, records the row, emits a system event. Idempotent ONLY * to the extent that the row check is up-front; if the row exists * we throw `WorkspaceExistsError` rather than silently re-using a * possibly-stale on-disk state. Callers should `freeWorkspace` first. */ declare function createWorkspace(db: Db, opts: CreateWorkspaceOptions): Promise; declare function getWorkspaceForAgent(db: Db, agent: string, workstream: string): WorkspaceRow | undefined; declare function listWorkspaces(db: Db, workstream?: string): WorkspaceRow[]; interface FreeWorkspaceOptions { /** If true, attempt to commit pending changes before tearing down. * Backend-specific; see VcsBackend.freeWorkspace. */ commit?: boolean; } interface FreeWorkspaceResult { /** The committed ref, when `commit` was true and there was something * to commit. */ committedRef?: string; /** True iff the on-disk path was actually removed. */ removed: boolean; /** True iff the DB row was actually deleted. */ rowDeleted: boolean; } /** * Tear down an agent's workspace. Calls the backend to remove the * on-disk directory (with optional auto-commit), then DELETEs the row. * Idempotent on a missing workspace (returns all-false). */ declare function freeWorkspace(db: Db, agent: string, opts: FreeWorkspaceOptions & { workstream: string; }): Promise; declare function getWorkspaceStaleness(db: Db, agentName: string, workstreamName: string): Promise; /** * Decorate each row with `commitsBehindMain` by asking the row's backend * how far the parent_ref is behind the project's default branch HEAD. * Cheap, pure observation: NO automatic `git fetch` / `jj git fetch` / * `sl pull`. The number is as fresh as the workspace's local refs cache. * * Returns a NEW array; does not mutate the input. Rows whose parent_ref * is missing, or whose backend's commitsBehind throws / returns null, * get `commitsBehindMain: null`. */ declare function decorateWithStaleness(rows: readonly WorkspaceRow[]): Promise; /** * Decorate every row with a `dirty` marker — true when the backend's * `listDirtyFiles` reports any uncommitted / unstaged / untracked-not- * ignored files; false when clean; null on backend-command failure. * * Returns a NEW array; does not mutate the input. */ declare function decorateWithDirty(rows: readonly WorkspaceRow[]): Promise; interface WorkspaceOrphan { /** The on-disk dir name (the agent name it WOULD be for, if mu had * registered it). */ agentName: string; /** Workstream the dir is filed under. */ workstreamName: string; /** Absolute path to the orphan dir. */ path: string; } /** * Like WorkspaceOrphan but additionally flags whether the parent * workstream itself is gone (no row in `workstreams`). Returned by * listAllOrphanWorkspaces; the per-workstream listWorkspaceOrphans * doesn't carry this since by construction it only runs against an * existing workstream. */ interface StrandedWorkspaceOrphan extends WorkspaceOrphan { /** True iff the parent workstream has no DB row (the dir was left * behind by a `mu workstream teardown` or a manual DELETE). */ stranded: boolean; } /** * Scan `/workspaces//` for directories that * have no row in `vcs_workspaces`. * * Returns `[]` when the workstream's workspaces dir doesn't exist, * or when every dir on disk has a corresponding DB row. Filesystem * read is best-effort: a missing/inaccessible dir returns `[]`. */ declare function listWorkspaceOrphans(db: Db, workstream: string): WorkspaceOrphan[]; /** * Cross-workstream variant of listWorkspaceOrphans. Reads * `/workspaces/`, recurses one level (per-ws subdir → * per-agent subdir), and surfaces every dir with no row in * `vcs_workspaces`. */ declare function listAllOrphanWorkspaces(db: Db): StrandedWorkspaceOrphan[]; declare class TaskNotFoundError extends Error implements HasNextSteps { readonly taskId: string; readonly name = "TaskNotFoundError"; constructor(taskId: string); errorNextSteps(): NextStep[]; } declare class TaskExistsError extends Error implements HasNextSteps { readonly taskId: string; readonly name = "TaskExistsError"; constructor(taskId: string); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly taskId: string; readonly expectedWorkstream: string; readonly actualWorkstream: string; readonly name = "TaskNotInWorkstreamError"; constructor(taskId: string, expectedWorkstream: string, actualWorkstream: string); errorNextSteps(): NextStep[]; } declare class TaskAlreadyOwnedError extends Error implements HasNextSteps { readonly taskId: string; readonly currentOwner: string; readonly name = "TaskAlreadyOwnedError"; constructor(taskId: string, currentOwner: string); errorNextSteps(): NextStep[]; } /** * 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` references `agents.name`; 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 (run `mu agent adopt ` to register, or use --for to pick a * different agent). * * Maps to exit code 4 (conflict) via the cli.ts handler. */ declare class ClaimerNotRegisteredError extends Error implements HasNextSteps { readonly agentName: string; readonly paneId: string | null; readonly name = "ClaimerNotRegisteredError"; constructor(agentName: string, paneId: string | null); /** * Three actionable resolutions in expected-frequency order: * 1. --self : orchestrator pattern (working directly) * 2. --for : dispatcher pattern (assigning to a worker) * 3. mu agent adopt: registration pattern (promote pane to worker) */ errorNextSteps(): NextStep[]; } declare class CycleError extends Error implements HasNextSteps { readonly from: string; readonly to: string; readonly name = "CycleError"; constructor(from: string, to: string); errorNextSteps(): NextStep[]; } declare class CrossWorkstreamEdgeError extends Error implements HasNextSteps { readonly blocker: string; readonly blockerWorkstream: string; readonly dependent: string; readonly dependentWorkstream: string; readonly name = "CrossWorkstreamEdgeError"; constructor(blocker: string, blockerWorkstream: string, dependent: string, dependentWorkstream: string); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly verb: "close" | "park"; readonly substate: TaskSubstate; readonly taskId: string; readonly name = "SubstateReasonRequiredError"; constructor(verb: "close" | "park", substate: TaskSubstate, taskId: string); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly taskId: string; readonly status: "IN_PROGRESS" | "CLOSED"; readonly workstream: string; readonly name = "TaskParkStateError"; constructor(taskId: string, status: "IN_PROGRESS" | "CLOSED", workstream: string); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly taskId: string; readonly workstream: string; readonly name = "TaskParkedError"; constructor(taskId: string, workstream: string); errorNextSteps(): NextStep[]; } /** * 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 implements HasNextSteps { readonly status: TaskStatus; readonly substate: string; readonly name = "InvalidSubstateError"; constructor(status: TaskStatus, substate: string); errorNextSteps(): NextStep[]; } 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 if `title` yields an empty slug after stripping. */ 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): IdFromTitleResult; 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[]; } declare function listTasks(db: Db, workstream?: string, opts?: ListTasksOptions): TaskRow[]; /** Options for listReady. The optional `statuses` filter composes * on top of the `ready` view (which itself constrains to * `status='OPEN'`); passing only OPEN is identical to today's no- * filter shape, passing only non-OPEN values returns []. Exists so * `mu task next --status` can mirror the multi-status flag shape * shipped on `mu task list` (task_list_multi_status_union). */ interface ListReadyOptions { status?: TaskStatus | readonly TaskStatus[]; /** Substate filter; the `ready` view already excludes OPEN/parked. */ substate?: TaskSubstate | readonly TaskSubstate[]; } declare function listReady(db: Db, workstream: string, opts?: ListReadyOptions): 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 (review_code_raw_task_state_duplicate). */ declare function listInProgress(db: Db, workstream: string): TaskRow[]; /** Most-recently-closed tasks in a workstream, newest first, capped at * `limit` (default 5). 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 * (review_code_raw_task_state_duplicate). */ 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` event in * agent_logs) 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. * Comparison is lexicographic on the ISO string (matches the way * the rest of the codebase compares ISO timestamps). */ 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[]; /** * 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[]; declare function setWaitSleepForTests(impl: ((ms: number) => Promise) | undefined): (ms: number) => Promise; /** Test seam: swap the stderr writer used by the stuck-task warning so * unit tests can capture warnings without spying on process.stderr. */ declare function setWaitStuckWarnForTests(impl: ((msg: string) => void) | undefined): (msg: string) => void; /** Total number of polls performed across all `waitForTasks` calls in this * process. Tests typically reset before exercising and read after. */ declare function getWaitPollCount(): number; declare function resetWaitPollCount(): void; /** A single task ref the wait verb is watching. Cross-workstream * waits arrive as a heterogeneous list of (workstream, name) pairs; * the legacy single-workstream call passes the same workstream on * every ref. task_wait_cross_workstream. */ interface TaskWaitRef { /** The workstream the task lives in. Each ref carries its own so * the SDK doesn't need a single "the workstream" — cross-ws waits * pass refs from multiple workstreams in one call. */ workstreamName: string; /** The task's per-workstream-unique local id. */ name: string; } interface TaskWaitOptions { /** Target status. Default 'CLOSED'. */ status?: TaskStatus; /** When true, succeed as soon as ONE listed task reaches the target. * Default false: every listed task must reach the target. */ any?: boolean; /** Maximum time to wait, in milliseconds. Default 600_000 (10 min). * Pass 0 to wait forever. */ timeoutMs?: number; /** Polling interval. Default 1000ms; overridable for tests. */ pollMs?: number; /** Workstream context applied to bare-string ids. Required when the * caller passes `string[]`; ignored when the caller passes * `TaskWaitRef[]` (each ref carries its own ws). The legacy * single-ws SDK call site keeps its today's shape; the cross-ws * callers (CLI verb) pass `TaskWaitRef[]` and omit `workstream`. * task_wait_cross_workstream. */ workstream?: string; /** Emit a yellow STUCK warning to stderr (once per task per wait call) * when an IN_PROGRESS task's owner has been in `needs_input` for at * least this many milliseconds since the agent row's last update. * Default 300_000 (5 min). Pass 0 to disable. * * Surfaced by agent_attention_required: a worker sitting in * needs_input leaves wait blocked indefinitely. The cause may be a * finish-without-close, a question awaiting an answer, or a prompt * — the predicate cannot tell them apart, so the warning reports * the observation and points at `mu agent read`. Default action is * observation-only (wait keeps polling); `onStall: 'exit'` makes it * terminal. */ stuckAfterMs?: number; /** What to do when the `--stuck-after` predicate fires on a watched * task. `'warn'` (default) = today's behaviour: yellow STUCK line * to stderr (deduped per task per wait call) + corroborating * `kind='event'` agent_logs row; wait keeps polling. `'exit'` = * same emit + persist, but THEN throw `StallDetectedDuringWaitError` * so the CLI wrapper exits 7 (STALL_DETECTED). The exit-action is * the unattended-orchestrator escape: a wrapping policy can branch * on 7 (idle, ambiguous — operator decides poke vs release) vs 6 * (dead pane, unambiguous — re-dispatch). * * Carve-out (lives at the call site, not here): the CLI passes * `'exit'` only when the wait target is CLOSED — mirrors exit-6's * reaper-flip suppression. With `--status OPEN` the worker reaching * needs_input might BE the success path. See * task_wait_stall_action_flag. */ onStall?: "warn" | "exit"; /** Read an owner's current runtime state for attention detection. * Without this hook, SDK waits never infer a stall from stored data. */ readOwnerState?: (owner: { name: string; workstreamName: string; }) => Promise; /** Optional async hook run BEFORE every snapshot (initial + each * poll iteration). The CLI uses this to reconcile the workstream * each tick (reaper flips IN_PROGRESS → OPEN for dead-pane * workers) and to throw a typed error when a reaper-flip on a * watched task should abandon the wait — see * task_wait_reconcile_dead_panes. Throwing from `beforePoll` * propagates out of `waitForTasks` unchanged. * * Kept as a generic seam (not a `--reconcile`-shaped option) so * the SDK module stays free of tmux/reconcile imports — that * layering belongs above the SDK in the CLI wrapper. */ beforePoll?: () => Promise; } interface TaskWaitTaskState { /** The workstream this task lives in. Cross-workstream waits * return a mixed list; the workstream is part of identity. * task_wait_cross_workstream. */ workstreamName: string; /** The task's per-workstream-unique name. */ name: string; /** Current status (at the moment we exit). */ status: TaskStatus; /** Current substate. A CLOSED ref that is not `done` has no deliverable. */ substate: TaskSubstate; /** Owner at exit time (NULL when unowned, after release, or after * the reaper flipped IN_PROGRESS → OPEN due to a dead pane). */ owner: string | null; /** True when this task's status equals the target. */ reachedTarget: boolean; /** True when the task is IN_PROGRESS, owned by a registered agent * whose detected status is `needs_input` for >= `stuckAfterMs`. * Surfaces agent_attention_required: the worker may have finished * without closing, be waiting on an answer, or be sitting at a * prompt — this flag does not distinguish them, so a consumer * should read the pane. Backwards-compatible signal — callers * ignoring it see no behaviour change. */ stuck: boolean; } interface TaskWaitResult { /** Per-task state at exit time. Same length and order as the input * list. The caller derives all-reached / any-reached / elapsed * from this list (count `r.reachedTarget`) and from its own * startedAt clock — keeping the SDK return minimal. */ refs: TaskWaitTaskState[]; /** True when we exited because of the timeout, not because the wait * condition was met. Refs that did reach the target are still * reflected in `refs[i].reachedTarget` on partial-progress timeout. */ timedOut: boolean; } /** * Block until a set of tasks reaches `opts.status` (default CLOSED). * Returns a result describing the final state — the caller decides * whether to treat partial-progress timeouts as success or failure * (the CLI maps a clean exit to 0, a timeout to 5). * * Pre-flight: every task in `localIds` MUST exist; missing ones throw * TaskNotFoundError before any waiting begins. This is loud-fail by * design — a typo'd id silently waiting forever is the worst-case UX. */ declare function waitForTasks(db: Db, input: readonly TaskWaitRef[] | readonly string[], opts: TaskWaitOptions): Promise; interface FullDag { /** Root tasks: no incoming `blocks` edge (no blockers). */ roots: TaskRow[]; /** Edges map parent task name → child task names (what parent blocks). */ edges: Map; /** All tasks in the workstream, keyed by operator-facing name. */ tasks: Map; } type TaskStatusLabelFn = (task: TaskRow) => string; interface RenderTreeOptions { /** Include the task title after the name + status label. Default: true. */ includeTitle?: boolean; } interface LoadFullDagOptions { /** Optional visible-status filter. Omitted = every task status. */ statuses?: ReadonlySet; /** Optional row predicate, applied after `statuses` (e.g. the TUI's * substate toggles). Omitted = keep every row. */ include?: (task: TaskRow) => boolean; } declare function loadFullDag(db: Db, workstream: string, opts?: LoadFullDagOptions): FullDag; /** * Render a DAG forest in the same ASCII shape as `mu task tree --down`: * each root is printed as a header node, dependents are below it, and * DAG diamonds collapse after the first full subtree render with a * one-line recurrence marker. */ declare function renderForest(roots: readonly TaskRow[], edges: ReadonlyMap, statusFn: TaskStatusLabelFn, tasksByName?: ReadonlyMap, opts?: RenderTreeOptions): string; declare function renderTaskTree(db: Db, workstream: string, root: TaskRow, direction: "blockers" | "dependents", statusFn: TaskStatusLabelFn, opts?: RenderTreeOptions): string; /** * Identifies a generated shim on its own line, so re-linking (and * `inspectLinks`) can tell it from an older inlined copy without * depending on the import statement's exact shape. */ declare const MU_SHIM_MARKER = "// mu:shim"; type LinkState = "ok" | "missing" | "stale-copy" | "foreign" | "dangling"; interface LinkStatus { path: string; state: LinkState; target?: string; } interface LinkOptions { /** Root under which `.pi/` and `.agents/` live. Default `MU_PI_HOME ?? homedir()`. */ home?: string; } /** Raised when the destination holds something mu did not put there * (a real directory, or a symlink to another checkout without * `--force`). mu never deletes user files to make room. */ declare class LinkConflictError extends Error implements HasNextSteps { readonly path: string; readonly reason: "not-a-symlink" | "foreign-symlink"; readonly current?: string | undefined; constructor(path: string, reason: "not-a-symlink" | "foreign-symlink", current?: string | undefined); errorNextSteps(): NextStep[]; } /** Install the extension shim (or, with `copy`, an inlined copy pinned * to this version). `replacedCopy` is true when an existing file without * the shim marker was overwritten. */ declare function linkPi(opts?: LinkOptions & { copy?: boolean; }): { path: string; replacedCopy: boolean; }; /** Symlink `/.agents/skills/mu` to the package's `skills/mu`. * `previous` is the old link target when one was replaced. */ declare function linkSkill(opts?: LinkOptions & { force?: boolean; }): { path: string; target: string; previous?: string; }; /** Report the state of both installs without changing anything. */ declare function inspectLinks(opts?: LinkOptions): { extension: LinkStatus; skill: LinkStatus; }; 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 write` / 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; /** Undo group: every op of one operator action shares it. Surfaced so * `mu log` can print it and `mu log --group ` can filter to it * (undo discoverability). */ 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; } 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` / `mu agent send` lines, which are prose by nature and have * no state change to name. */ intent?: string; } /** * Append a log entry. Returns the inserted row (with assigned `seq`). * Constant-time. Single INSERT; safe to call from any state-changing * verb without a transaction wrapper. */ 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; /** 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` (the legacy `kind` column). */ kind?: string; /** Filter by structured `ops.intent`, e.g. 'task.close'. */ intent?: string; /** Filter to one undo 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 — that was the duplication v2-retire-log-shim deleted: 13 call * sites each writing a second, prose, intent-less copy of a change the * trigger had already captured properly. * * 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. * workspace.* create / free / refresh — `vcs_workspaces` * is machine-local (absolute paths). * 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 (v2-log-verb) switches on exactly these. */ type LocalIntent = "agent.spawn" | "agent.close" | "agent.adopt" | "agent.kick" | "agent.stall" | "workspace.create" | "workspace.free" | "workspace.refresh"; 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 WorkstreamSnapshot { workstreamName: string; view: LiveAgentsView; tracks: Track[]; ready: TaskRow[]; inProgress: TaskRow[]; blocked: TaskRow[]; recentClosed: TaskRow[]; /** OPEN/parked tasks in the workstream. The ready card names them * when nothing is ready, since parked work is otherwise invisible there. */ parkedCount: number; /** Populated only when callers explicitly pass `withAllTasks: true`. * The TUI dashboard fast tick leaves this empty and the all-tasks * popup reads its exhaustive list directly from SQLite while open. */ allTasks: TaskRow[]; workspaces: WorkspaceRow[]; workspaceOrphans: WorkspaceOrphan[]; recent: LogRow[]; /** Last N commits from the project root (process.cwd()), populated * when `loadWorkstreamSnapshot` is called with withRecentCommits. * This is intentionally NOT a per-agent workspace log. */ recentCommits: CommitSummary[]; /** Backend that produced recentCommits. Null when recent commits were * not requested or no VCS backend was detected. */ commitsBackend?: VcsBackendName | null; /** Populated when `loadWorkstreamSnapshot` is called with * `withDoctor: true`. Used by the TUI's slot-9 Doctor card to * render a glanceable health badge on the dashboard * (feat_card_9_doctor, workstream `tui-impl`). The static `mu * state` card and `mu doctor` itself don't consume it — they * read the textual doctor card directly. Null when not requested. */ doctor: DoctorSummary | null; } interface LoadWorkstreamSnapshotOptions { /** Recent-events cap (default 200). */ eventLimit?: number; /** When true, slow snapshot loading also populates `WorkspaceRow.dirty` * via decorateWithDirty (one `git status --porcelain` shellout per row, * capped at DECORATE_CONCURRENCY). The TUI caches this slow-tier value * and merges it into every fast SQL tick. */ withDirty?: boolean; /** When true, slow snapshot loading also populates * `WorkstreamSnapshot.doctor` via `loadDoctorSummary`. The summary is * cheap SQL, but it reports tmux/workspace drift from slow-tier fields, * so the TUI refreshes it with the subprocess tier. */ withDoctor?: boolean; /** Optional full task list for the TUI all-tasks popup. */ withAllTasks?: true; /** Optional recent-project-commits slice for the TUI Commits card / * popup. Uses process.cwd() as the project root on purpose: the TUI * is launched from the project checkout, while worker workspaces live * elsewhere under the mu state dir. */ withRecentCommits?: { limit: number; }; } interface WorkstreamSnapshotSlowFields { view: LiveAgentsView; /** Workspace rows decorated with slow-tier VCS observations * (`commitsBehindMain`, and `dirty` when requested). */ workspaces: WorkspaceRow[]; recentCommits: CommitSummary[]; commitsBackend?: VcsBackendName | null; doctor: DoctorSummary | null; } declare function loadWorkstreamSnapshotFast(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions): Promise; /** * Slow snapshot tier: fields backed by tmux / VCS subprocess probes (plus * doctor, which reports over those slow-tier observations). Returns only the * fields the fast snapshot deliberately leaves empty or undecorated. * * The slow snapshot tier runs full reconciliation: missing panes are * reaped, while mid-spawn placeholders remain protected by the prune * loop's pending-pane guard. */ declare function loadWorkstreamSnapshotSlow(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions, baseSnapshot?: WorkstreamSnapshot): Promise; /** Merge the latest slow-tier subprocess observations into a fresh fast tier. */ declare function mergeSnapshotFastSlow(fast: WorkstreamSnapshot, slow: WorkstreamSnapshotSlowFields | null): WorkstreamSnapshot; /** * Back-compat wrapper for non-TUI callers: return the historical union shape * by composing the new fast SQL tier with one slow subprocess tier. */ declare function loadWorkstreamSnapshot(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions): Promise; /** * ROI tiers used to colour task rows. Pure: returns the bucket name; the * consumer maps bucket → picocolors function (or ink text colour). * Magic numbers (≥100 high, ≥50 mid) lifted from the previous HUD impl. */ type RoiBucket = "high" | "mid" | "low" | "infinite"; declare function roiBucket(impact: number, effortDays: number): RoiBucket; /** Histogram of agents by runtime state. Pure derivation (no colour render). */ declare function agentStateHistogram(agents: readonly LiveAgent[]): ReadonlyMap; interface OwnedTasksSummary { /** Display token: "—" (none), task id (one), or GLYPH.multi + count (many). */ bit: string; /** Underlying count for callers that want their own format. */ count: number; /** The single owned task's local id, when count===1. */ onlyTaskId?: string; } /** * Per-agent task summary: condensed display token + raw count. Used by * both the static Agents table and the ink Agents card. Pure on the * input rows — caller (e.g. loadWorkstreamSnapshot consumer) does the * listTasksByOwner query upstream and feeds the rows in. */ declare function summarizeOwnedTasks(owned: readonly TaskRow[]): OwnedTasksSummary; type DoctorStatus = "ok" | "warn" | "fail"; interface DoctorCheck { /** Short, stable identifier — used as the row label. Lowercase * one-word tokens so the column-aligned card layout looks tidy. */ name: string; status: DoctorStatus; /** Free-form prose for the row's right-hand column. Kept short so * the card's CLIP column doesn't truncate it on common widths. */ detail: string; } interface DoctorSummary { /** Every check that ran, in stable display order. The card filters * to non-OK rows for its body but keeps the OK rows so the popup * (when it ships under feat_more_cards_umbrella) can render the * full list. */ checks: readonly DoctorCheck[]; /** Convenience: how many rows are warn or fail. Card subtitle * reads this directly. Pure derivation from `checks`. */ problemCount: number; } /** * Compute the doctor summary for a workstream. Pure-ish: runs cheap * synchronous DB queries + reads from the supplied snapshot. Callers * that don't want to compute a snapshot first (or are running inside * `loadWorkstreamSnapshot` mid-build) can omit `snapshot` — the * snapshot-derived checks (ghosts / orphans / workspace-orphans) are * skipped in that case. */ declare function loadDoctorSummary(db: Db, snapshot: WorkstreamSnapshot | null, agentStateSource?: "murmur" | "herdr"): DoctorSummary; /** Count of warn + fail rows. Pure; exported for unit tests. */ declare function countProblems(checks: readonly DoctorCheck[]): number; /** * Return the full check array (OK + warn + fail) in stable display * order. Used by the TUI's slot-9 Doctor popup * (feat_popup_9_doctor, workstream `tui-impl`) which renders every * row — not just the non-OK subset Card 9 surfaces. * * Thin wrapper over `loadDoctorSummary` so the SDK seam stays * single: `loadDoctorSummary` is the source of truth for the * check vocabulary, and the popup's `loadDoctorChecks` view is * just `.checks`. Pure-ish (same cheap synchronous DB reads as * `loadDoctorSummary`). */ declare function loadDoctorChecks(db: Db, snapshot: WorkstreamSnapshot | null): readonly DoctorCheck[]; /** * Map a check row to the most useful informational command the * operator might paste. Read-only by construction: `mu agent list`, * `mu workspace orphans`, `mu doctor` are all SELECT-shape verbs; * `# ...` lines are visibly inert. Per the slot-9 popup spec KEY * MAP block this is INFORMATIONAL, never a mutating recipe — so * even when the check is `fail`, we yank the diagnostic verb the * operator should RUN MANUALLY, not a fix command. * * Pure; exported for unit tests + SDK reuse. */ declare function yankCommandForCheck(check: Pick): string; /** * A short paragraph (one paragraph per check name) explaining the * shape of the failure / warning. Returned as a `readonly string[]` * so the popup's drill body can interleave the lines with other * content; CLI consumers can `.join("\n")` themselves. * * Pure; exported for unit tests + SDK reuse. */ declare function remediationParagraph(check: DoctorCheck): readonly 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. * * ~1ms on a 200-task DB: four indexed NOT EXISTS scans, no rebuild, no * temp file. Cheap enough to run on every `mu doctor`. * * 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 implements HasNextSteps { readonly totalDrift: number; readonly records: readonly DriftRecord[]; constructor(totalDrift: number, records: readonly DriftRecord[]); errorNextSteps(): NextStep[]; } /** 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 `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; } | 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[]; } /** * True iff `child` is inside `parent` (or is `parent`). * * Path-based, deliberately: the check must fire even when the DB file * does not exist yet, so it cannot rely on stat/inode identity. Both * sides are resolved to absolute form first, and 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 workspace 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.stale`, never for a * clock — so re-pointing the clock 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 workspace 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: ""; /** Workspace has uncommitted edits. */ readonly dirty: ""; /** Workspace is ≥ WORKSPACE_STALE_THRESHOLD commits behind main. */ readonly stale: ""; /** 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 three 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.reject" | "task.defer" | "task.claim" | "task.release" | "task.reap" | "task.block" | "task.unblock" | "task.reparent" | "workstream.init" | "workstream.teardown" | "workstream.destroy"; 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 write` / 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 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, same semantics as the old * `agent_logs.source`: an agent name, "user", "system". */ actor?: string | undefined; /** * Grouping for `mu undo`: * - 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, restoring the previous * context afterwards even if `fn` throws. * * 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 — the temp table is * shared per-connection, so two concurrent async scopes would clobber * each other with no way to tell whose intent won. Keeping this sync * makes that 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. * * v2-sync wraps its ingest loop in this. 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. * * Restores the previous value in a `finally`, so a throw mid-ingest * cannot leave capture permanently disabled on this connection — which * would be the worst possible failure mode, silently dropping every * subsequent local change. */ declare function withCaptureSuppressed(db: Db, fn: () => T): T; /** 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 implements HasNextSteps { readonly path: string; constructor(path: string); errorNextSteps(): NextStep[]; } /** 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 implements HasNextSteps { readonly path: string; constructor(path: string); errorNextSteps(): NextStep[]; } /** 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 / marker) are * copied but have no table to land in. */ opsProjected: number; /** Ops that changed a row when applied. Lower than opsProjected * whenever later ops superseded earlier ones — which is normal and * is exactly what makes the rebuild a merge rather than a diff. */ 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 a * per-connection temp table, so interleaved async scopes would clobber * each other's suppression flag. */ declare function rebuildInto(source: Db, opts: RebuildOptions): RebuildReport; /** Current segment line format. Bumped only on a breaking shape change; * a reader that sees a version it does not know REFUSES the line rather * than guessing at its meaning. */ declare const SEGMENT_FORMAT_VERSION = 1; /** 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; } /** Whole-file verification sidecar. */ interface SegmentManifest { v: number; machine: string; count: number; lastHlc: string | null; sha256: string; updatedAt: 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 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. */ 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; } /** * Append this machine's not-yet-flushed ops to its own segment. * * FILTERING IS LOAD-BEARING. Only ops whose entity is in * `SYNCED_ENTITIES` are written. Machine-local ops (agent.*, workspace.*) * are captured and DO appear in `mu log`, but they must never reach a * segment: they carry pane ids and absolute paths that are meaningless, * and frequently wrong, on another machine. "Not synced" is not "not * logged". * * Also filters `machine_id = `: a segment holds ONE machine's ops. * Ops ingested from a peer live in our `ops` table too, and re-flushing * them into our own segment would duplicate a peer's history under our * name — and would grow without bound as two machines echoed each other. * * The high-water mark is the last hlc already in the file (read from the * manifest when present, else derived by scanning), so flush is * incremental and idempotent: calling it twice appends nothing the second * time. */ declare function flushSegment(db: Db, dir?: string | null): 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; /** Read a segment's manifest, or null when absent/unparsable. */ declare function readManifest(segment: string): SegmentManifest | null; /** * 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; /** 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 a peer's segment we have applied. * * 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, machineId: string): number; declare function setWatermark(db: Db, machineId: string, value: number): 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, machineId: string): void; interface IngestResult { machineId: string; path: string; /** Lines read past the watermark. */ 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. * * 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): IngestResult; 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. * * This is the SDK seam `mu sync` (v2-sync) will call; 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; /** 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 undo ` 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); } /** 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; /** Lines of this peer's segment 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. */ declare function resolvePeerRef(peers: readonly PeerStatus[], ref: string): PeerStatus; /** Reset a peer's watermark so the next ingest re-reads its segment 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, and the Doctor card * is where a TUI operator learns about a broken sync dir. */ quiet?: boolean; } /** * 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. * * Runs under the cross-process file lock inside `flushSegment`, so two * concurrent mu processes cannot interleave partial lines. */ declare function ambientFlush(db: Db, opts?: AmbientOptions): 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; /** * Copy-pasteable transport for a stale peer, via the ordinary NextStep * convention. This is what mu does INSTEAD of moving bytes itself. * * `` is a literal placeholder on purpose: mu has no host list and * is not growing one (a peer list would be a config file with extra * steps, and one that must be kept consistent on every machine — the * very drift it looks like it solves). */ declare function transportNextSteps(dir: string, peers: readonly PeerStatus[]): NextStep[]; 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; type tmux_NewSessionOptions = NewSessionOptions; type tmux_NewSessionWithPaneOptions = NewSessionWithPaneOptions; type tmux_NewWindowOptions = NewWindowOptions; declare const tmux_PANE_ID_RE: typeof PANE_ID_RE; type tmux_PaneNotFoundError = PaneNotFoundError; declare const tmux_PaneNotFoundError: typeof PaneNotFoundError; type tmux_SendOptions = SendOptions; type tmux_SendWarning = SendWarning; type tmux_SplitWindowOptions = SplitWindowOptions; type tmux_TmuxError = TmuxError; declare const tmux_TmuxError: typeof TmuxError; type tmux_TmuxExecResult = TmuxExecResult; type tmux_TmuxExecutor = TmuxExecutor; type tmux_TmuxPane = TmuxPane; type tmux_TmuxSession = TmuxSession; type tmux_TmuxWindow = TmuxWindow; declare const tmux_assertValidPaneId: typeof assertValidPaneId; declare const tmux_attachCommands: typeof attachCommands; declare const tmux_attachHint: typeof attachHint; declare const tmux_awaitPaneQuiescence: typeof awaitPaneQuiescence; declare const tmux_capturePane: typeof capturePane; declare const tmux_currentAgentName: typeof currentAgentName; declare const tmux_currentPaneTitle: typeof currentPaneTitle; declare const tmux_currentSessionName: typeof currentSessionName; declare const tmux_defaultSendDelayMs: typeof defaultSendDelayMs; declare const tmux_defaultSendReadinessMs: typeof defaultSendReadinessMs; declare const tmux_enableMuPaneBorders: typeof enableMuPaneBorders; declare const tmux_enableMuPaneBordersForPane: typeof enableMuPaneBordersForPane; declare const tmux_enableMuPaneBordersForSession: typeof enableMuPaneBordersForSession; declare const tmux_getPaneTitle: typeof getPaneTitle; declare const tmux_getWindowIdForPane: typeof getWindowIdForPane; declare const tmux_hasWorkMarker: typeof hasWorkMarker; 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_listPanesInSession: typeof listPanesInSession; declare const tmux_listSessions: typeof listSessions; declare const tmux_listWindows: typeof listWindows; declare const tmux_newSession: typeof newSession; declare const tmux_newSessionWithPane: typeof newSessionWithPane; declare const tmux_newWindow: typeof newWindow; declare const tmux_paneExists: typeof paneExists; declare const tmux_paneNotFoundNextSteps: typeof paneNotFoundNextSteps; declare const tmux_paneTTY: typeof paneTTY; declare const tmux_parseAgentNameFromTitle: typeof parseAgentNameFromTitle; declare const tmux_resetSleep: typeof resetSleep; declare const tmux_resetTmuxExecutor: typeof resetTmuxExecutor; declare const tmux_selectLayout: typeof selectLayout; declare const tmux_sendToPane: typeof sendToPane; declare const tmux_sessionExists: typeof sessionExists; declare const tmux_setPaneTitle: typeof setPaneTitle; declare const tmux_setSleepForTests: typeof setSleepForTests; declare const tmux_setTmuxExecutor: typeof setTmuxExecutor; declare const tmux_sleep: typeof sleep; declare const tmux_splitWindow: typeof splitWindow; 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, type tmux_NewSessionOptions as NewSessionOptions, type tmux_NewSessionWithPaneOptions as NewSessionWithPaneOptions, type tmux_NewWindowOptions as NewWindowOptions, tmux_PANE_ID_RE as PANE_ID_RE, tmux_PaneNotFoundError as PaneNotFoundError, type tmux_SendOptions as SendOptions, type tmux_SendWarning as SendWarning, type tmux_SplitWindowOptions as SplitWindowOptions, tmux_TmuxError as TmuxError, type tmux_TmuxExecResult as TmuxExecResult, type tmux_TmuxExecutor as TmuxExecutor, type tmux_TmuxPane as TmuxPane, type tmux_TmuxSession as TmuxSession, type tmux_TmuxWindow as TmuxWindow, tmux_assertValidPaneId as assertValidPaneId, tmux_attachCommands as attachCommands, tmux_attachHint as attachHint, tmux_awaitPaneQuiescence as awaitPaneQuiescence, tmux_capturePane as capturePane, tmux_currentAgentName as currentAgentName, tmux_currentPaneTitle as currentPaneTitle, tmux_currentSessionName as currentSessionName, tmux_defaultSendDelayMs as defaultSendDelayMs, tmux_defaultSendReadinessMs as defaultSendReadinessMs, tmux_enableMuPaneBorders as enableMuPaneBorders, tmux_enableMuPaneBordersForPane as enableMuPaneBordersForPane, tmux_enableMuPaneBordersForSession as enableMuPaneBordersForSession, tmux_getPaneTitle as getPaneTitle, tmux_getWindowIdForPane as getWindowIdForPane, tmux_hasWorkMarker as hasWorkMarker, tmux_healthCheck as healthCheck, tmux_isValidPaneId as isValidPaneId, tmux_killPane as killPane, tmux_killSession as killSession, tmux_listPanes as listPanes, tmux_listPanesInSession as listPanesInSession, tmux_listSessions as listSessions, tmux_listWindows as listWindows, tmux_newSession as newSession, tmux_newSessionWithPane as newSessionWithPane, tmux_newWindow as newWindow, tmux_paneExists as paneExists, tmux_paneNotFoundNextSteps as paneNotFoundNextSteps, tmux_paneTTY as paneTTY, tmux_parseAgentNameFromTitle as parseAgentNameFromTitle, tmux_resetSleep as resetSleep, tmux_resetTmuxExecutor as resetTmuxExecutor, tmux_selectLayout as selectLayout, tmux_sendToPane as sendToPane, tmux_sessionExists as sessionExists, tmux_setPaneTitle as setPaneTitle, tmux_setSleepForTests as setSleepForTests, tmux_setTmuxExecutor as setTmuxExecutor, tmux_sleep as sleep, tmux_splitWindow as splitWindow, tmux$1 as tmux, tmux_tmuxBackend as tmuxBackend }; } /** What undoing one op will do. */ interface InverseOp { /** Entity of the row being restored/removed. */ entity: string; /** Natural key of that row. */ key: string; /** The inverse action. */ op: "put" | "del"; /** Field -> prior value, for a `put`. Empty for a `del`. */ fields: Record; /** Human summary, for the dry-run listing. */ summary: string; /** Fields that a LATER group has written since, with the group that * did. Non-empty means applying this inverse would clobber newer * work. */ supersededBy: Array<{ field: string; groupId: string; intent: string | null; }>; } interface UndoPlan { /** The group being undone. */ groupId: string; /** The intent(s) of the ops in the group — what the operator did. */ intents: readonly string[]; /** When the group was written (ISO, from the oldest op). */ when: string; /** Inverse ops, in the order they will be applied (FK-safe). */ inverses: readonly InverseOp[]; /** True iff any inverse would clobber a newer edit. */ superseded: boolean; /** Ops in the group that need no inverse (already reverted, or a * no-op), for an honest count. */ skipped: number; } interface UndoResult { plan: UndoPlan; /** Group id of the ops the UNDO itself wrote — pass this to * `mu undo` to redo. */ undoGroupId: string; /** Inverse ops that actually changed a row. */ applied: number; } /** Raised when the requested group does not exist. */ declare class UndoGroupNotFoundError extends Error implements HasNextSteps { readonly groupId: string; constructor(groupId: string); errorNextSteps(): NextStep[]; } /** Raised when there is nothing to undo at all. */ declare class NothingToUndoError extends Error implements HasNextSteps { constructor(); errorNextSteps(): NextStep[]; } /** Raised when the group's rows were changed by a later group, so * undoing would clobber that newer work. */ declare class UndoSupersededError extends Error implements HasNextSteps { readonly groupId: string; readonly conflicts: readonly { key: string; field: string; groupId: string; }[]; constructor(groupId: string, conflicts: readonly { key: string; field: string; groupId: string; }[]); errorNextSteps(): NextStep[]; } /** * The value `field` held for `key` immediately BEFORE `hlc`. * * This is the provenance query src/apply.ts uses for per-field LWW, * pointed backwards: the newest op strictly older than `hlc` that NAMED * this field. `json_type(...) IS NOT NULL` rather than * `json_extract(...) IS NOT NULL` for the same reason apply does it — * json_extract returns SQL NULL both for an absent key and for a * present-but-null one, so a set-to-NULL would look absent and we would * restore the wrong (older) value. * * Returns `{ found: false }` when no earlier op named the field, which * means the field had no value before this op — so there is nothing to * restore and the op must have been part of the row's creation. */ declare function priorFieldValue(db: Db, entity: string, key: string, hlc: string, field: string): { found: true; value: string | number | null; } | { found: false; }; interface GroupSummary { groupId: string; /** Distinct intents in the group, in first-seen order. */ intents: readonly string[]; actor: string | null; /** Ops in the group. */ ops: number; /** ISO timestamp of the group's oldest op. */ when: string; /** Newest HLC in the group, for ordering. */ hlc: string; } /** * Recent groups, newest first. This is how group ids become DISCOVERABLE: * `mu undo` with no argument lists these, so the operator never has to * know a uuid to use the verb. * * Only groups that touched a portable table are listed, because those are * the only ones with anything to invert. */ declare function listRecentGroups(db: Db, limit?: number): GroupSummary[]; /** The most recent undoable group, or null when there is none. */ declare function mostRecentGroup(db: Db): GroupSummary | null; /** * Resolve a possibly-abbreviated group id to a full one, or raise. * * Delegates to `groupIdFromPrefix` (src/logs.ts) so `mu undo` and * `mu log --group` accept EXACTLY the same identifiers. They used to * disagree: undo resolved prefixes, `mu log --group` compared the column * literally and silently returned nothing * (bug_group_id_prefix_asymmetry). One rule, two verbs. * * Ambiguity surfaces as `GroupIdAmbiguousError` (exit 4, a conflict the * operator resolves) rather than being folded into not-found. */ declare function resolveGroupId(db: Db, prefix: string): string; /** * Compute what undoing `groupId` would do, WITHOUT doing it. * * Pure with respect to the DB: reads only. `mu undo ` calls this * for its dry run and `undoGroup` calls it again before applying, so the * preview and the action can never diverge. */ declare function planUndo(db: Db, groupId: string): UndoPlan; interface UndoOptions { /** Apply even when the group has been superseded, discarding the * newer edits to those fields. */ force?: boolean; /** Actor recorded on the undo's own ops. */ actor?: string | undefined; } /** * Apply the inverse of `groupId`. * * Everything happens inside ONE `withOpContext` scope with * `group: "new"`, so: * * every inverse write lands in a single new group (making the undo * itself one undoable unit), and * * the capture triggers record each write with a fresh HLC, so the * undo is an ordinary op that syncs and shows up in `mu log`. * * Wrapped in one transaction: a half-applied undo is worse than none, * because the operator would not know which half. */ declare function undoGroup(db: Db, groupId: string, opts?: UndoOptions): UndoResult; declare function isValidWorkstreamName(name: string): boolean; /** Thrown by `ensureWorkstream` and `mu workstream init` when the name * doesn't match the rules. */ declare class WorkstreamExistsError extends Error implements HasNextSteps { readonly workstream: string; readonly name: string; constructor(workstream: string); errorNextSteps(): NextStep[]; } declare class WorkstreamNameInvalidError extends Error implements HasNextSteps { readonly attempted: string; readonly name = "WorkstreamNameInvalidError"; constructor(attempted: string); errorNextSteps(): NextStep[]; } /** * 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 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; /** Rows in `vcs_workspaces` for this workstream. Surfaced so the * teardown dry-run can warn about per-agent worktrees that need * cleanup before the FK cascade silently nukes their rows. */ workspaceCount: 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 `agents` rows deleted. */ deletedAgents: number; /** Number of `tasks` rows deleted (edges/notes cascade via FK). */ deletedTasks: number; /** Number of `task_notes` deleted by the cascade — informational. */ deletedNotes: number; /** Number of `task_edges` deleted by the cascade — informational. */ deletedEdges: number; /** Number of vcs_workspaces whose on-disk path was actually * removed by the backend on this teardown. Excludes * `alreadyGoneWorkspaces` (those were no-ops on disk). */ freedWorkspaces: number; /** Number of vcs_workspaces whose registry row existed but * whose on-disk path was already gone (manual rm -rf or a prior * interrupted teardown). The DB row was cascade-deleted; the * backend did no filesystem work. Tracked separately so the * teardown report doesn't lie about how much cleanup it actually * performed. */ alreadyGoneWorkspaces: number; /** Workspaces whose backend cleanup failed (e.g. `git worktree * remove` refused because of uncommitted changes). The DB row * was still cascade-deleted; the on-disk path remains and needs * manual cleanup. */ failedWorkspaces: WorkspaceFailure[]; } interface WorkspaceFailure { agent: string; backend: string; path: string; error: string; } interface WorkstreamOptions { workstream: string; /** Override the mux session name. Defaults to `mu-`. */ muxSession?: string; /** Override the per-name VcsBackend resolver. Defaults to * `backendByName`. Lets tests inject a fake backend (e.g. one whose * `freeWorkspace` throws) without mutating the exported singletons — * same pattern as `createWorkspace`'s `opts.backend` accepting a * pre-built `VcsBackend` object. Production callers leave this * unset. */ resolveBackend?: (name: VcsBackendName) => VcsBackend; } interface TeardownWorkstreamOptions extends WorkstreamOptions { } declare function listWorkstreams(db: Db): Promise; declare function summarizeWorkstream(db: Db, opts: WorkstreamOptions): Promise; /** * Tear down a workstream: kill its mux session and delete every DB row * tagged with its name. Cascades on `tasks` clean up `task_edges` and * `task_notes` automatically (FK ON DELETE CASCADE in the schema). * * Idempotent: safe to call against a workstream that never existed; safe * to call repeatedly. Returns counts so the caller can print a useful * summary. */ declare function teardownWorkstream(db: Db, opts: TeardownWorkstreamOptions): Promise; export { AGENT_STATE_GLYPH, type AbortAgentOptions, type AbortResult, type AddNoteOptions, type AddTaskOptions, type AdoptAgentOptions, type AdoptAgentResult, AgentAbortNeedsCtlError, AgentAbortTimeoutError, AgentBusyError, AgentCtlUnreachableError, AgentDiedOnSpawnError, AgentExistsError, AgentFreshNeedsCtlError, AgentNotFoundError, AgentNotInWorkstreamError, type AgentRow, AgentSlashCommandUnsupportedError, AgentSpawnCliNotFoundError, AgentSpawnStartupError, type AmbientOptions, type AmbientResult, type AppendLogOptions, type ApplyResult, type BlockEdgeResult, CAPTURE_TRIGGER_DDL, CURRENT_SCHEMA_VERSION, type CaptureDb, type CaptureIntent, type CaptureOptions, type CaseCollision, type CheapDriftReport, type ClaimResult, type ClaimTaskOptions, ClaimerNotRegisteredError, type CloseAgentOptions, type CloseAgentResult, type CloseSubstate, type CloseTaskResult, type CommandResolutionResult, type CommandResolver, CrossWorkstreamEdgeError, type CtlLink, CycleError, DEFAULT_SUBSTATE, DRIFT_REPORT_CAP, type Db, type DeleteTaskResult, type DoctorCheck, type DoctorStatus, type DoctorSummary, DriftDetectedError, type DriftRecord, type DriftReport, EXPECTED_TABLES, type EvidenceOption, type FileLockOptions, type FleetHazard, type FlushResult, type FsProbe, type FullDag, GLYPH, type GroupSummary, type HazardSeverity, type Hlc, HlcOverflowError, HlcParseError, HomeDirAsProjectRootError, type IdFromTitleResult, type IngestFromDbResult, type IngestResult, type InsertAgentInput, InvalidSubstateError, type InverseOp, KNOWN_INTENTS, KNOWN_VERBS, type KickAgentOptions, type KickAgentResult, type KickProcessExecutor, type KickSignal, type KnownIntent, LinkConflictError, type LinkState, type LinkStatus, type ListLiveAgentsOptions, type ListLogsOptions, type ListNotesOptions, type ListReadyOptions, type ListTasksOptions, type LiveAgent, type LiveAgentsView, type LoadFullDagOptions, type LoadWorkstreamSnapshotOptions, type LocalIntent, type LogKind, type LogRow, MACHINE_LOCAL_ENTITIES, MACHINE_LOCAL_TABLES, MU_SHIM_MARKER, MachineIdentityMissingError, type MachineLocalEntity, type MachineLocalLoss, type MachineLocalTable, type MuxBackend, type MuxBackendName, MuxError, type MuxPane, type MuxPaneStatus, type MuxSession, type MuxWindow, type NewSessionOptions, type NewWindowOptions, NoForegroundProcessError, NoMultiplexerError, NothingToUndoError, OP_CTX_DDL, type Op, type OpContext, OpEntityNotSyncedError, OpKeyMalformedError, type OpenDbOptions, type OwnedTasksSummary, 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 ReleaseResult, type ReleaseTaskOptions, type RemoveBlockEdgeResult, type RenderableOp, type RenderedOp, type ReparentTaskResult, type RoiBucket, type RuntimeState, SEGMENT_FORMAT_VERSION, SYNCED_ENTITIES, SchemaTooOldError, type SegmentDefect, type SegmentDefectKind, type SegmentLine, type SegmentManifest, type SendOptions, type SendResult, type SendWarning, type SetStatusOptions, type SetStatusResult, type SlugifyResult, type SpawnAgentOptions, type SplitWindowOptions, type StateReading, type StateSource, type StrandedWorkspaceOrphan, SubstateReasonRequiredError, type SyncPassResult, SyncPeerNotFoundError, SyncPeerRefAmbiguousError, SyncSourceNotFoundError, type SyncedEntity, TASK_STATUSES, TASK_STATUS_LIST, TASK_SUBSTATES, TASK_SUBSTATE_ROWS, TaskAlreadyOwnedError, type TaskEdgeWithStatus, type TaskEdges, type TaskEdgesWithStatus, TaskExistsError, TaskNotFoundError, TaskNotInWorkstreamError, type TaskNoteRow, type TaskPair, TaskParkStateError, TaskParkedError, type TaskRow, type TaskStatus, type TaskSubstate, type TaskWaitOptions, type TaskWaitRef, type TaskWaitResult, type TaskWaitTaskState, type TeardownResult, TmuxError, type TmuxExecResult, type TmuxExecutor, type TmuxPane, type TmuxSession, type TmuxWindow, type Track, type Transport, UNKNOWN_REASON, UndoGroupNotFoundError, type UndoOptions, type UndoPlan, type UndoResult, UndoSupersededError, type UpdateTaskOptions, type UpdateTaskResult, type VcsBackend, type VcsBackendName, WORKSPACE_STALE_THRESHOLD, WorkspaceExistsError, type WorkspaceFailure, WorkspaceNotFoundError, type WorkspaceOrphan, WorkspacePathNotEmptyError, WorkspacePreservedError, type WorkspaceRow, type WorkspaceStaleness, WorkstreamExistsError, WorkstreamNameInvalidError, type WorkstreamOptions, type WorkstreamSnapshot, type WorkstreamSnapshotSlowFields, type WorkstreamSummary, abortAgent, activeMux, addBlockEdge, addNote, addTask, adoptAgent, agentKey, agentStateGlyph, agentStateHistogram, ambientFlush, ambientIngest, ambientSyncPass, appendLog, applyIncomingOp, applyOp, applyOps, assertValidPaneId, awaitPaneQuiescence, backendByName, capturePane, checkCaseCollisions, checkCheapDriftInvariant, checkCommandResolvable, checkDbInsideSyncDir, checkDrift, checkFleetHazards, checkNetworkMount, claimTask, classifyFsType, closeAgent, closeTask, compareHlc, composeAgentTitle, countProblems, createWorkspace, ctlRuntimeState, currentAgentName, currentOpContext, currentPaneTitle, decorateWithDirty, decorateWithStaleness, defaultDbPath, defaultSendDelayMs, defaultSendReadinessMs, defaultSpawnLivenessMs, defaultStateDir, deleteAgent, deleteTask, detectBackend, detectMux, discoverPeers, driftRemediation, emitEvent, ensureWorkstream, envVarNameForCli, expectsCtl, findCaseCollisions, flushSegment, foregroundPgid, formatDriftRecord, formatHlc, formatPair, freeWorkspace, getAgent, getAgentByPane, getParallelTracks, getPrerequisites, getTask, getTaskEdges, getTaskEdgesWithStatus, getWaitPollCount, getWatermark, getWorkspaceForAgent, getWorkspaceStaleness, gitBackend, hasWorkMarker, idFromTitle, idFromTitleVerbose, ingestFromDb, ingestSegment, insertAgent, inspectLinks, installCapture, isKickSignal, isPathInside, isTaskStatus, isValidAgentName, isValidPair, isValidPaneId, isValidTaskId, isValidWorkstreamName, isWorkspaceStale, jjBackend, kickAgent, killPane, killSession, latestSeq, linkPi, linkSkill, listAgents, listAllOrphanWorkspaces, listBlocked, listGoals, listInProgress, listLiveAgents, listLogs, listNotes, listPanes, listPanesInSession, listReady, listRecentClosed, listRecentGroups, listSessions, listTasks, listTasksByOwner, listWindows, listWorkspaceOrphans, listWorkspaces, listWorkstreams, loadDoctorChecks, loadDoctorSummary, loadFullDag, loadWorkstreamSnapshot, loadWorkstreamSnapshotFast, loadWorkstreamSnapshotSlow, localMachineId, locksDir, mapLegacyStatus, mergeSnapshotFastSlow, mostRecentGroup, murmurAvailable, mux, muxByName, newSession, newSessionWithPane, newWindow, nextHlc, noneBackend, opSubject, openDb, openTask, paneExists, paneTTY, parkTask, parseAgentNameFromTitle, parseHlc, parseOpKey, parsePsTtyOutput, peerStatuses, planUndo, priorFieldValue, probeFilesystem, readAgent, readAgentStates, readFileLockMeta, readManifest, rebuildInto, receiveHlc, reconcile, refreshAgentTitle, releaseTask, remediationParagraph, removeBlockEdge, renderForest, renderOp, renderOpLine, renderTaskTree, repairPeer, reparentTask, resetCommandResolverForTests, resetKickProcessExecutor, resetMux, resetSleep, resetTmuxExecutor, resetWaitPollCount, resetWatermark, resolveActorIdentity, resolveCliCommand, resolveCliCommandWithSource, resolveGroupId, resolvePair, resolvePeerRef, roiBucket, segmentLineCount, segmentPath, selectLayout, sendToAgent, sendToPane, sendViaTransport, sessionExists, setCommandResolverForTests, setKickProcessExecutor, setMuxForTests, setPaneTitle, setSleepForTests, setTaskStatus, setTmuxExecutor, setWaitSleepForTests, setWaitStuckWarnForTests, setWatermark, slBackend, sleep, slugifyTitle, slugifyTitleVerbose, spawnAgent, splitWindow, summarizeOwnedTasks, summarizeWorkstream, syncDir, syncEnabled, syncPass, teardownWorkstream, tmux, tmuxBackend, transportNextSteps, undoGroup, unparkTask, updateTask, verifyAgainstManifest, waitForTasks, withCaptureSuppressed, withFileLock, withOpContext, workspacePath, workspacesRoot, yankCommandForCheck };