@martintrojer/mu 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3130 @@
1
+ import { Database } from 'better-sqlite3';
2
+
3
+ /**
4
+ * One actionable next step. The `intent` is human-prose ("Drop notes
5
+ * as you work"); the `command` is a literal shell command the user (or
6
+ * an LLM) can copy-paste or `eval` directly.
7
+ *
8
+ * Used both for success-path hints (post-verb) and for typed-error
9
+ * resolutions (in the error message + JSON output).
10
+ */
11
+ interface NextStep {
12
+ /** Short human-prose label, e.g. "Drop notes as you work". */
13
+ intent: string;
14
+ /** Literal shell command, e.g. `mu task note foo "..."`. */
15
+ command: string;
16
+ }
17
+ /**
18
+ * Marker interface for typed errors that carry actionable resolutions.
19
+ * The handler checks this with a duck-typed `typeof err.errorNextSteps
20
+ * === "function"` rather than instanceof so cross-realm errors (e.g.
21
+ * thrown from a different module instance after a hot-reload) still
22
+ * surface their nextSteps.
23
+ */
24
+ interface HasNextSteps {
25
+ errorNextSteps(): NextStep[];
26
+ }
27
+
28
+ type Db = Database;
29
+ interface OpenDbOptions {
30
+ /**
31
+ * Absolute path to the SQLite file. Defaults to MU_DB_PATH env var or
32
+ * the XDG state path (see `defaultDbPath`). Use a per-test temp path
33
+ * in tests.
34
+ */
35
+ path?: string;
36
+ /**
37
+ * If true, opens the DB read-only. Used by `mu sql` and similar read-only
38
+ * surfaces to enforce no-mutation guarantees at the connection level.
39
+ */
40
+ readonly?: boolean;
41
+ }
42
+ /**
43
+ * Resolve the canonical mu state directory:
44
+ * MU_STATE_DIR > $XDG_STATE_HOME/mu > ~/.local/state/mu
45
+ */
46
+ declare function defaultStateDir(): string;
47
+ /**
48
+ * Resolve the canonical DB path:
49
+ * MU_DB_PATH > <state-dir>/mu.db
50
+ */
51
+ declare function defaultDbPath(): string;
52
+ /**
53
+ * Per-workstream artifact directory: <state-dir>/workstreams/<workstream>/
54
+ *
55
+ * Created lazily by callers. 0.1.0 doesn't write to it yet — reserved
56
+ * for future snapshots / tracing logs / forensic pane captures. The DB
57
+ * stays canonical and shared; this directory is only for things that
58
+ * naturally don't need cross-workstream queries.
59
+ */
60
+ declare function workstreamStateDir(workstream: string): string;
61
+ /**
62
+ * Open the mu database. Creates the parent directory and applies the schema
63
+ * idempotently on every open. Safe to call from many short-lived processes
64
+ * concurrently — WAL mode handles cross-process writes.
65
+ */
66
+ declare function openDb(options?: OpenDbOptions): Db;
67
+ declare class SchemaTooOldError extends Error implements HasNextSteps {
68
+ readonly detectedVersion: number;
69
+ readonly requiredVersion: number;
70
+ readonly name = "SchemaTooOldError";
71
+ constructor(detectedVersion: number, requiredVersion: number);
72
+ errorNextSteps(): NextStep[];
73
+ }
74
+ /** Test seam: ensure a workstream's artifact dir exists. Unused today. */
75
+ declare function ensureWorkstreamStateDir(workstream: string): string;
76
+ /** The schema version a fresh DB starts at. v7 drops the
77
+ * `approvals` table on top of v6 (which added 5 archive_* tables
78
+ * on top of v5's surrogate-PK substrate; docs/ARCHITECTURE.md §
79
+ * Surrogate-PK + SDK-boundary discipline). The refusal floor is
80
+ * v5 — pre-v5 DBs throw `SchemaTooOldError`; v5 → v6 → v7 DBs
81
+ * are forward-bumped in place by `applySchema`. */
82
+ declare const CURRENT_SCHEMA_VERSION = 7;
83
+ /** Tables a healthy DB must contain. Single source of truth so
84
+ * `mu doctor` and any other consumer don't drift. Adding a new table
85
+ * = one new entry here AND a CREATE TABLE in CURRENT_SCHEMA. (Schema
86
+ * changes that aren't compatible with prior schemas bump
87
+ * CURRENT_SCHEMA_VERSION and ship with a one-shot script under
88
+ * scripts/ (the v4→v5 transition was the canonical example
89
+ * before the script was deleted post-landing). */
90
+ declare const EXPECTED_TABLES: readonly string[];
91
+
92
+ /**
93
+ * The full agent lifecycle status. Most values are scrollback-derived
94
+ * (`DetectedStatus`); the rest are set by the lifecycle layer.
95
+ */
96
+ type AgentStatus = "spawning" | "busy" | "needs_input" | "needs_permission" | "free" | "unreachable" | "terminated";
97
+ /** Status that can be inferred from pane scrollback. */
98
+ type DetectedStatus = "busy" | "needs_input" | "needs_permission";
99
+ /**
100
+ * Run the detector against pane scrollback and return the inferred
101
+ * status. Permission overrides busy.
102
+ */
103
+ declare function detectPiStatus(scrollback: string): DetectedStatus;
104
+ /**
105
+ * Public for tests — extract the tail window the detector actually
106
+ * inspects. Take last TAIL_WINDOW_LINES, strip trailing blanks, take
107
+ * last TAIL_LINES.
108
+ */
109
+ declare function extractTail(scrollback: string): string;
110
+
111
+ declare class TmuxError extends Error implements HasNextSteps {
112
+ readonly args: readonly string[];
113
+ readonly stderr: string;
114
+ readonly stdout: string;
115
+ readonly exitCode: number | null;
116
+ constructor(args: readonly string[], stderr: string, stdout: string, exitCode: number | null);
117
+ errorNextSteps(): NextStep[];
118
+ }
119
+ /**
120
+ * Thrown when a verb references a tmux pane id that doesn't exist on
121
+ * the running tmux server. Distinct from TmuxError (which wraps any
122
+ * tmux command failure) so callers can map it to a specific exit code
123
+ * (`mu` maps it to 5 — substrate failure — alongside other tmux
124
+ * issues, but the message is more actionable than a raw tmux stderr).
125
+ */
126
+ declare class PaneNotFoundError extends Error implements HasNextSteps {
127
+ readonly paneId: string;
128
+ readonly name = "PaneNotFoundError";
129
+ constructor(paneId: string);
130
+ errorNextSteps(): NextStep[];
131
+ }
132
+ /**
133
+ * Stable tmux pane IDs are of the form `%N` (e.g. "%15"). They never change
134
+ * for the lifetime of the pane. **Pane indexes** (0, 1, 2…) are volatile and
135
+ * shift when other panes close — never store or pass them.
136
+ */
137
+ declare const PANE_ID_RE: RegExp;
138
+ declare function isValidPaneId(s: string): boolean;
139
+ declare function assertValidPaneId(s: string): void;
140
+ /**
141
+ * Delay between bracketed-paste and Enter, in milliseconds. Claude/Codex/pi
142
+ * process pasted text asynchronously; without this delay, Enter can arrive
143
+ * before the agent has ingested the text. Defaults to 500; lower for tests,
144
+ * raise for slow remotes via `MU_SEND_DELAY_MS`.
145
+ */
146
+ declare function defaultSendDelayMs(): number;
147
+ interface TmuxExecResult {
148
+ stdout: string;
149
+ stderr: string;
150
+ exitCode: number | null;
151
+ }
152
+ type TmuxExecutor = (args: readonly string[]) => Promise<TmuxExecResult>;
153
+ /**
154
+ * Install a custom executor (for tests). Returns the previous executor so
155
+ * tests can restore it cleanly. Production code should never call this.
156
+ */
157
+ declare function setTmuxExecutor(executor: TmuxExecutor): TmuxExecutor;
158
+ /** Restore the real (execa-backed) executor. */
159
+ declare function resetTmuxExecutor(): void;
160
+ /**
161
+ * Run an arbitrary tmux command. The single point of contact with the
162
+ * tmux binary; every higher-level operation in this module goes through it.
163
+ *
164
+ * Throws `TmuxError` on non-zero exit. Returns stdout on success.
165
+ */
166
+ declare function tmux(args: readonly string[]): Promise<string>;
167
+ declare function setSleepForTests(impl: (ms: number) => Promise<void>): (ms: number) => Promise<void>;
168
+ declare function resetSleep(): void;
169
+ /** Test-aware sleep — honours `setSleepForTests`. Public so other modules
170
+ * (notably `agents.ts` for spawn liveness polling) get free no-op-ing in
171
+ * tests without re-implementing the swap. */
172
+ declare function sleep(ms: number): Promise<void>;
173
+ interface TmuxSession {
174
+ name: string;
175
+ }
176
+ interface TmuxWindow {
177
+ /** tmux window id, e.g. "@1". */
178
+ id: string;
179
+ name: string;
180
+ /** Session this window belongs to (only set by cross-session listings). */
181
+ sessionName?: string;
182
+ }
183
+ interface TmuxPane {
184
+ /** Stable tmux pane id, e.g. "%15". */
185
+ paneId: string;
186
+ /** Pane title set via `select-pane -T`. The agent's name in mu's convention. */
187
+ title: string;
188
+ /** Current foreground command (e.g. "claude", "node", "bash"). */
189
+ command: string;
190
+ /** Window this pane lives in. Only set by cross-window listings. */
191
+ windowId?: string;
192
+ /** Session this pane lives in. Only set by cross-session listings. */
193
+ sessionName?: string;
194
+ }
195
+ declare function listSessions(): Promise<TmuxSession[]>;
196
+ declare function sessionExists(name: string): Promise<boolean>;
197
+ interface NewSessionOptions {
198
+ detached?: boolean;
199
+ windowName?: string;
200
+ command?: string;
201
+ /** Initial working directory for the first pane (`-c <path>`). */
202
+ cwd?: string;
203
+ /** Extra env vars to set in the new pane via tmux `-e KEY=VALUE`.
204
+ * Available since tmux 3.0; sets the variable in the new pane's
205
+ * environment without polluting the tmux server's global env. */
206
+ env?: Record<string, string>;
207
+ }
208
+ declare function newSession(name: string, opts?: NewSessionOptions): Promise<void>;
209
+ interface NewSessionWithPaneOptions {
210
+ windowName: string;
211
+ command: string;
212
+ cwd?: string;
213
+ detached?: boolean;
214
+ /** Extra env vars to set in the new pane via tmux `-e KEY=VALUE`. */
215
+ env?: Record<string, string>;
216
+ }
217
+ /**
218
+ * Create a tmux session AND its first window+pane in one atomic call.
219
+ * Returns the new pane's stable id. Used by mu when spawning the first
220
+ * agent in a workstream so we never end up with an empty `mu-<workstream>`
221
+ * session left behind by a failed spawn.
222
+ */
223
+ declare function newSessionWithPane(name: string, opts: NewSessionWithPaneOptions): Promise<string>;
224
+ /** Idempotent: succeeds even if the session is already gone. */
225
+ declare function killSession(name: string): Promise<void>;
226
+ declare function listWindows(session?: string): Promise<TmuxWindow[]>;
227
+ interface NewWindowOptions {
228
+ /** Target session. Required if invoking outside an existing tmux client. */
229
+ session?: string;
230
+ /** Window name. Maps to the agent's `tab:` value (or its name if no tab). */
231
+ name: string;
232
+ /** Command to run in the first pane. */
233
+ command: string;
234
+ /** If true, do not switch focus. Defaults to true. */
235
+ detached?: boolean;
236
+ /** Initial working directory (`-c <path>`). */
237
+ cwd?: string;
238
+ /** Extra env vars to set in the new pane via tmux `-e KEY=VALUE`. */
239
+ env?: Record<string, string>;
240
+ }
241
+ /**
242
+ * Create a new tmux window with one pane. Returns the new pane's stable
243
+ * pane id (e.g. `%15`).
244
+ */
245
+ declare function newWindow(opts: NewWindowOptions): Promise<string>;
246
+ /**
247
+ * List ALL panes in a tmux session (across every window). Used by
248
+ * reconciliation to find every pane in the workstream's session.
249
+ *
250
+ * Note `list-panes -t <session>` (no -s) lists panes in the current
251
+ * *window* of that session, not the whole session — a common gotcha.
252
+ * `-s` is the flag that says "all panes in this session."
253
+ *
254
+ * Returns `[]` (not throws) when the session doesn't exist or has no
255
+ * panes. tmux destroys a session as soon as its last pane closes, so the
256
+ * "session was just here a moment ago" case is normal during reconcile.
257
+ * tmux's error wording in this case varies ("can't find session" or
258
+ * "can't find window"), so we match either.
259
+ */
260
+ declare function listPanesInSession(session: string): Promise<TmuxPane[]>;
261
+ /**
262
+ * List panes in the current session, a specific window/session target, or
263
+ * all panes across all sessions when `target` is the literal "*".
264
+ */
265
+ declare function listPanes(target?: string): Promise<TmuxPane[]>;
266
+ interface SplitWindowOptions {
267
+ /** Target window or pane (e.g. ":Backend" or "%15"). */
268
+ target: string;
269
+ command: string;
270
+ /** Horizontal split (side-by-side). Default true. */
271
+ horizontal?: boolean;
272
+ detached?: boolean;
273
+ /** Initial working directory for the new pane (`-c <path>`). */
274
+ cwd?: string;
275
+ /** Extra env vars to set in the new pane via tmux `-e KEY=VALUE`. */
276
+ env?: Record<string, string>;
277
+ }
278
+ /**
279
+ * Split a window and run a command in the new pane. Returns the new pane's
280
+ * stable pane id.
281
+ */
282
+ declare function splitWindow(opts: SplitWindowOptions): Promise<string>;
283
+ /** Idempotent: succeeds even if the pane is already gone. */
284
+ declare function killPane(paneId: string): Promise<void>;
285
+ declare function paneExists(paneId: string): Promise<boolean>;
286
+ declare function setPaneTitle(paneId: string, title: string): Promise<void>;
287
+ /**
288
+ * Look up the window id (e.g. `@42`) that contains a given pane id
289
+ * (e.g. `%15`). Used by spawn so we can apply window-scoped options
290
+ * (`pane-border-status`) to the freshly created window.
291
+ *
292
+ * Returns undefined if the pane no longer exists.
293
+ */
294
+ declare function getWindowIdForPane(paneId: string): Promise<string | undefined>;
295
+ /**
296
+ * Apply the mu pane border (status=top, format='[mu] #{pane_title}')
297
+ * to EVERY window currently in `session`. Idempotent. Best-effort:
298
+ * windows that have vanished mid-iteration are silently skipped. Used
299
+ * by `mu workstream init` (covers the placeholder `_mu` window plus
300
+ * any windows that already exist, e.g. on re-init of an upgraded
301
+ * mu-pre-border session) and by `mu agent spawn` (covers the
302
+ * just-created window so the border shows immediately on attach).
303
+ *
304
+ * No-op (returns 0) when `MU_BANNER_QUIET=1`.
305
+ *
306
+ * Returns the number of windows that received the option.
307
+ */
308
+ declare function enableMuPaneBordersForSession(session: string): Promise<number>;
309
+ /**
310
+ * Apply the mu pane border to the window containing `paneId`. This is
311
+ * the spawn/adopt shape: callers have a pane id (from `new-window` or
312
+ * from an adopt target), and need to resolve the enclosing window
313
+ * before calling `enableMuPaneBorders` (a window-scoped option).
314
+ *
315
+ * Self-checks `MU_BANNER_QUIET` and swallows tmux errors — the border
316
+ * is decorative; failing to set it is never load-bearing.
317
+ */
318
+ declare function enableMuPaneBordersForPane(paneId: string): Promise<void>;
319
+ /**
320
+ * Enable a one-line top pane border on a specific window/session target,
321
+ * showing `[mu] <pane-title>`. Idempotent (set-option is a write, not
322
+ * a toggle).
323
+ *
324
+ * IMPORTANT: tmux's `pane-border-status` and `pane-border-format` are
325
+ * **window** options, not session options. `set-option -t <session>`
326
+ * only updates the active window at call time — windows created later
327
+ * inherit from the GLOBAL value (which is `off` by default and which
328
+ * we deliberately do NOT touch, since changing the global would
329
+ * affect every other tmux session on the user's machine, including
330
+ * dotfile-curated ones).
331
+ *
332
+ * Therefore mu must call this twice:
333
+ * 1. At `mu workstream init` time on the placeholder `_mu` window
334
+ * (so an attached operator sees a border immediately).
335
+ * 2. On every `mu agent spawn` (which calls `tmux new-window`),
336
+ * against the new window's id.
337
+ *
338
+ * The border is tmux chrome, not pane content: it doesn't scroll, it
339
+ * survives copy-mode, and the inner CLI never sees it.
340
+ *
341
+ * Designed in roadmap-v0-2 hud_visual_cue_design (note #283); shipped
342
+ * in hud_visual_cue_impl.
343
+ */
344
+ declare function enableMuPaneBorders(target: string): Promise<void>;
345
+ declare function getPaneTitle(paneId: string): Promise<string | undefined>;
346
+ /**
347
+ * Read the title of the *current* pane (the one whose shell is running this
348
+ * process), via $TMUX_PANE. Returns undefined when not inside tmux. Used by
349
+ * `mu claim` to derive the agent identity from the pane title — the claim
350
+ * protocol's zero-config identity step.
351
+ */
352
+ declare function currentPaneTitle(): Promise<string | undefined>;
353
+ /**
354
+ * Read the *current* pane's interior size (`pane_width` x `pane_height`)
355
+ * via $TMUX_PANE. Returns undefined when not inside tmux or when the
356
+ * tmux call fails. Used by `mu hud` to size its tables when stdout
357
+ * isn't a TTY (e.g. when running under `watch -n 5 mu hud -w X` or
358
+ * `tmux display-popup -E 'mu hud -w X'`, both of which strip TTY-ness
359
+ * but still run inside a tmux pane whose dimensions matter).
360
+ */
361
+ declare function currentPaneSize(): Promise<{
362
+ width: number;
363
+ height: number;
364
+ } | undefined>;
365
+ /**
366
+ * Extract the agent-name token from a (possibly composed) pane title.
367
+ * mu's composeAgentTitle renders titles as `name · <glyph> · task_id`,
368
+ * where <glyph> is a Nerd Font codepoint from STATUS_EMOJI (see
369
+ * src/agents.ts). The agent name is always the first ' · '-separated
370
+ * token. Adopted panes that haven't been re-titled by mu have just the
371
+ * name (one token) — still parses.
372
+ *
373
+ * Returns trimmed name, or the input unchanged if no separator.
374
+ */
375
+ declare function parseAgentNameFromTitle(title: string): string;
376
+ /**
377
+ * Convenience: read the current pane's title and extract the agent name.
378
+ */
379
+ declare function currentAgentName(): Promise<string | undefined>;
380
+ declare function selectLayout(window: string, layout: string): Promise<void>;
381
+ interface SendOptions {
382
+ /** Override the default delay between paste and Enter, in ms. */
383
+ delayMs?: number;
384
+ }
385
+ /**
386
+ * Send a single line of text to a pane and submit it.
387
+ *
388
+ * Sequence:
389
+ * 1. exit copy mode (silent if not in copy mode)
390
+ * 2. load text into a uniquely-named tmux buffer
391
+ * 3. paste with bracketed-paste mode (-p) so apps treat as literal text;
392
+ * delete buffer after paste (-d); preserve LF (-r)
393
+ * 4. wait MU_SEND_DELAY_MS (default 500) so the agent ingests the text
394
+ * 5. send Enter as a real key event
395
+ *
396
+ * Naive `send-keys "<text>"` would let characters like /, ?, f, : be
397
+ * interpreted by the agent's TUI or by tmux's copy mode. Always use this.
398
+ */
399
+ declare function sendToPane(paneId: string, text: string, opts?: SendOptions): Promise<void>;
400
+ interface CaptureOptions {
401
+ /**
402
+ * Number of trailing lines to capture. Omitted = full scrollback.
403
+ * 0 = visible pane only.
404
+ */
405
+ lines?: number;
406
+ }
407
+ /**
408
+ * Read pane scrollback as plain text (no ANSI escapes).
409
+ *
410
+ * - No options: full scrollback (`-S - -E -`)
411
+ * - `lines: 0`: visible pane only
412
+ * - `lines: N`: last N lines (`-S -N`)
413
+ */
414
+ declare function capturePane(paneId: string, opts?: CaptureOptions): Promise<string>;
415
+
416
+ type tmux$1_CaptureOptions = CaptureOptions;
417
+ type tmux$1_NewSessionOptions = NewSessionOptions;
418
+ type tmux$1_NewSessionWithPaneOptions = NewSessionWithPaneOptions;
419
+ type tmux$1_NewWindowOptions = NewWindowOptions;
420
+ declare const tmux$1_PANE_ID_RE: typeof PANE_ID_RE;
421
+ type tmux$1_PaneNotFoundError = PaneNotFoundError;
422
+ declare const tmux$1_PaneNotFoundError: typeof PaneNotFoundError;
423
+ type tmux$1_SendOptions = SendOptions;
424
+ type tmux$1_SplitWindowOptions = SplitWindowOptions;
425
+ type tmux$1_TmuxError = TmuxError;
426
+ declare const tmux$1_TmuxError: typeof TmuxError;
427
+ type tmux$1_TmuxExecResult = TmuxExecResult;
428
+ type tmux$1_TmuxExecutor = TmuxExecutor;
429
+ type tmux$1_TmuxPane = TmuxPane;
430
+ type tmux$1_TmuxSession = TmuxSession;
431
+ type tmux$1_TmuxWindow = TmuxWindow;
432
+ declare const tmux$1_assertValidPaneId: typeof assertValidPaneId;
433
+ declare const tmux$1_capturePane: typeof capturePane;
434
+ declare const tmux$1_currentAgentName: typeof currentAgentName;
435
+ declare const tmux$1_currentPaneSize: typeof currentPaneSize;
436
+ declare const tmux$1_currentPaneTitle: typeof currentPaneTitle;
437
+ declare const tmux$1_defaultSendDelayMs: typeof defaultSendDelayMs;
438
+ declare const tmux$1_enableMuPaneBorders: typeof enableMuPaneBorders;
439
+ declare const tmux$1_enableMuPaneBordersForPane: typeof enableMuPaneBordersForPane;
440
+ declare const tmux$1_enableMuPaneBordersForSession: typeof enableMuPaneBordersForSession;
441
+ declare const tmux$1_getPaneTitle: typeof getPaneTitle;
442
+ declare const tmux$1_getWindowIdForPane: typeof getWindowIdForPane;
443
+ declare const tmux$1_isValidPaneId: typeof isValidPaneId;
444
+ declare const tmux$1_killPane: typeof killPane;
445
+ declare const tmux$1_killSession: typeof killSession;
446
+ declare const tmux$1_listPanes: typeof listPanes;
447
+ declare const tmux$1_listPanesInSession: typeof listPanesInSession;
448
+ declare const tmux$1_listSessions: typeof listSessions;
449
+ declare const tmux$1_listWindows: typeof listWindows;
450
+ declare const tmux$1_newSession: typeof newSession;
451
+ declare const tmux$1_newSessionWithPane: typeof newSessionWithPane;
452
+ declare const tmux$1_newWindow: typeof newWindow;
453
+ declare const tmux$1_paneExists: typeof paneExists;
454
+ declare const tmux$1_parseAgentNameFromTitle: typeof parseAgentNameFromTitle;
455
+ declare const tmux$1_resetSleep: typeof resetSleep;
456
+ declare const tmux$1_resetTmuxExecutor: typeof resetTmuxExecutor;
457
+ declare const tmux$1_selectLayout: typeof selectLayout;
458
+ declare const tmux$1_sendToPane: typeof sendToPane;
459
+ declare const tmux$1_sessionExists: typeof sessionExists;
460
+ declare const tmux$1_setPaneTitle: typeof setPaneTitle;
461
+ declare const tmux$1_setSleepForTests: typeof setSleepForTests;
462
+ declare const tmux$1_setTmuxExecutor: typeof setTmuxExecutor;
463
+ declare const tmux$1_sleep: typeof sleep;
464
+ declare const tmux$1_splitWindow: typeof splitWindow;
465
+ declare const tmux$1_tmux: typeof tmux;
466
+ declare namespace tmux$1 {
467
+ export { type tmux$1_CaptureOptions as CaptureOptions, type tmux$1_NewSessionOptions as NewSessionOptions, type tmux$1_NewSessionWithPaneOptions as NewSessionWithPaneOptions, type tmux$1_NewWindowOptions as NewWindowOptions, tmux$1_PANE_ID_RE as PANE_ID_RE, tmux$1_PaneNotFoundError as PaneNotFoundError, type tmux$1_SendOptions as SendOptions, type tmux$1_SplitWindowOptions as SplitWindowOptions, tmux$1_TmuxError as TmuxError, type tmux$1_TmuxExecResult as TmuxExecResult, type tmux$1_TmuxExecutor as TmuxExecutor, type tmux$1_TmuxPane as TmuxPane, type tmux$1_TmuxSession as TmuxSession, type tmux$1_TmuxWindow as TmuxWindow, tmux$1_assertValidPaneId as assertValidPaneId, tmux$1_capturePane as capturePane, tmux$1_currentAgentName as currentAgentName, tmux$1_currentPaneSize as currentPaneSize, tmux$1_currentPaneTitle as currentPaneTitle, tmux$1_defaultSendDelayMs as defaultSendDelayMs, tmux$1_enableMuPaneBorders as enableMuPaneBorders, tmux$1_enableMuPaneBordersForPane as enableMuPaneBordersForPane, tmux$1_enableMuPaneBordersForSession as enableMuPaneBordersForSession, tmux$1_getPaneTitle as getPaneTitle, tmux$1_getWindowIdForPane as getWindowIdForPane, tmux$1_isValidPaneId as isValidPaneId, tmux$1_killPane as killPane, tmux$1_killSession as killSession, tmux$1_listPanes as listPanes, tmux$1_listPanesInSession as listPanesInSession, tmux$1_listSessions as listSessions, tmux$1_listWindows as listWindows, tmux$1_newSession as newSession, tmux$1_newSessionWithPane as newSessionWithPane, tmux$1_newWindow as newWindow, tmux$1_paneExists as paneExists, tmux$1_parseAgentNameFromTitle as parseAgentNameFromTitle, tmux$1_resetSleep as resetSleep, tmux$1_resetTmuxExecutor as resetTmuxExecutor, tmux$1_selectLayout as selectLayout, tmux$1_sendToPane as sendToPane, tmux$1_sessionExists as sessionExists, tmux$1_setPaneTitle as setPaneTitle, tmux$1_setSleepForTests as setSleepForTests, tmux$1_setTmuxExecutor as setTmuxExecutor, tmux$1_sleep as sleep, tmux$1_splitWindow as splitWindow, tmux$1_tmux as tmux };
468
+ }
469
+
470
+ /**
471
+ * What kind of reconciliation pass to run.
472
+ *
473
+ * "full" Default for `mu agent list`. Prunes ghosts (deleting
474
+ * the registry row, which fires the deleteAgent reaper
475
+ * that flips IN_PROGRESS tasks back to OPEN with
476
+ * [reaper] notes), runs status detection against
477
+ * surviving panes, surfaces orphans.
478
+ *
479
+ * "status-only" The "freshen the operator's view" mode. Runs status
480
+ * detection (DB writes that update agent status +
481
+ * pane title — desired side-effects of a refresh) and
482
+ * orphan surface. Does NOT prune (so a dead pane's
483
+ * row stays visible until a real `mu agent list`) and
484
+ * does NOT reap. Used by `mu state`, `mu hud`, bare
485
+ * `mu`, and `mu agent attach` — the verbs an operator
486
+ * polls to answer "is worker-X busy or idle right
487
+ * now?". Status detection skips placeholder agents
488
+ * whose pane id starts with `%pending-` (mid-spawn,
489
+ * no usable scrollback yet).
490
+ *
491
+ * "report-only" Pure observation. Counts would-be-pruned ghosts
492
+ * without deleting; skips status detection entirely
493
+ * (no DB writes, no tmux title writes); surfaces
494
+ * orphans (pure read). Used by `mu undo` (the
495
+ * post-restore pass MUST NOT delete rows the snapshot
496
+ * just restored — see
497
+ * snap_undo_reconcile_destroys_recovered_agents) and
498
+ * `mu doctor` (read-only diagnostic).
499
+ *
500
+ * Surfaced live by bug_pane_title_glyph_stuck_at_needs_input: the
501
+ * old `dryRun: boolean` flag conflated "don't prune" with "don't
502
+ * detect status", so `mu state` / `mu hud` showed stale status
503
+ * indefinitely. Splitting prune-suppression from status-suppression
504
+ * is the fix.
505
+ */
506
+ type ReconcileMode = "full" | "status-only" | "report-only";
507
+ interface ReconcileOptions {
508
+ /** The workstream whose registry rows we're reconciling. */
509
+ workstream: string;
510
+ /**
511
+ * Override the tmux session name. Defaults to `mu-<workstream>`. Useful
512
+ * for tests and for the rare case where a workstream's tmux session was
513
+ * created with a non-default name.
514
+ */
515
+ tmuxSession?: string;
516
+ /**
517
+ * Which kind of pass to run. Default is `"full"` (the documented
518
+ * mutating behaviour `mu agent list` has always had). See
519
+ * `ReconcileMode` for the full per-mode contract.
520
+ *
521
+ * BREAKING: this replaces the previous `dryRun?: boolean` flag.
522
+ * Migration: `dryRun: true` → `mode: "report-only"`; default
523
+ * (`dryRun: false` / unset) → `mode: "full"`.
524
+ */
525
+ mode?: ReconcileMode;
526
+ }
527
+ interface ReconcileReport {
528
+ /** Number of registry rows whose pane was gone. In status-only and
529
+ * report-only modes this is the count of rows that WOULD have
530
+ * been pruned; in `full` mode it's the count actually deleted. */
531
+ prunedGhosts: number;
532
+ /** Number of agents whose status was changed by scrollback detection.
533
+ * Always 0 in `report-only` mode (status detection is skipped). */
534
+ statusChanges: number;
535
+ /** Panes in the workstream's tmux session that look like agents but
536
+ * aren't in the registry. NOT auto-adopted. */
537
+ orphans: TmuxPane[];
538
+ /** Which mode this report was generated in. Lets callers switch their
539
+ * output text ("agents pruned" vs "would-be-pruned (suppressed)")
540
+ * without re-deriving from options. */
541
+ mode: ReconcileMode;
542
+ }
543
+ declare function reconcile(db: Db, opts: ReconcileOptions): Promise<ReconcileReport>;
544
+
545
+ declare class AgentExistsError extends Error implements HasNextSteps {
546
+ readonly agentName: string;
547
+ readonly name = "AgentExistsError";
548
+ constructor(agentName: string);
549
+ errorNextSteps(): NextStep[];
550
+ }
551
+ declare class AgentNotFoundError extends Error implements HasNextSteps {
552
+ readonly agentName: string;
553
+ /** Optional workstream context. When set, the message is enriched
554
+ * with `(in workstream <ws>)` so the verb that hit the miss
555
+ * (e.g. `mu workspace create <agent> -w <ws>`) doesn't leave the
556
+ * operator guessing which scope was searched. Optional so existing
557
+ * call sites that only know the agent name keep their original
558
+ * one-line message. */
559
+ readonly workstream?: string | undefined;
560
+ readonly name = "AgentNotFoundError";
561
+ constructor(agentName: string,
562
+ /** Optional workstream context. When set, the message is enriched
563
+ * with `(in workstream <ws>)` so the verb that hit the miss
564
+ * (e.g. `mu workspace create <agent> -w <ws>`) doesn't leave the
565
+ * operator guessing which scope was searched. Optional so existing
566
+ * call sites that only know the agent name keep their original
567
+ * one-line message. */
568
+ workstream?: string | undefined);
569
+ errorNextSteps(): NextStep[];
570
+ }
571
+ /**
572
+ * Thrown when an entity-targeted verb is invoked with `-w/--workstream
573
+ * <name>` but the named agent lives in a different workstream.
574
+ * Mirrors `TaskNotInWorkstreamError`. Maps to exit code 4 (conflict /
575
+ * wrong scope). Distinguishes "the user typo'd the workstream" from
576
+ * "the agent doesn't exist anywhere" (which surfaces as
577
+ * `AgentNotFoundError`).
578
+ */
579
+ declare class AgentNotInWorkstreamError extends Error implements HasNextSteps {
580
+ readonly agentName: string;
581
+ readonly expectedWorkstream: string;
582
+ readonly actualWorkstream: string;
583
+ readonly name = "AgentNotInWorkstreamError";
584
+ constructor(agentName: string, expectedWorkstream: string, actualWorkstream: string);
585
+ errorNextSteps(): NextStep[];
586
+ }
587
+ /**
588
+ * Thrown when an agent's pane is created and titled successfully but the
589
+ * spawned process exits within the liveness window (default 1500ms;
590
+ * configurable via `MU_SPAWN_LIVENESS_MS`). The most common cause is the
591
+ * underlying CLI failing fast: a wrapper CLI blocking on a single-instance
592
+ * lock, `claude` rejecting an invalid API key, etc. The agent's last
593
+ * scrollback (when capturable) is attached to help diagnose.
594
+ */
595
+ declare class AgentDiedOnSpawnError extends Error implements HasNextSteps {
596
+ readonly agentName: string;
597
+ readonly paneId: string;
598
+ readonly scrollback: string | undefined;
599
+ readonly name = "AgentDiedOnSpawnError";
600
+ constructor(agentName: string, paneId: string, scrollback: string | undefined);
601
+ errorNextSteps(): NextStep[];
602
+ }
603
+ /**
604
+ * Thrown when `closeAgent` is called on an agent that has an associated
605
+ * workspace AND the caller didn't explicitly opt into discarding it.
606
+ *
607
+ * Background: the FK on `vcs_workspaces.agent` cascades on agent
608
+ * delete, so a naive `closeAgent` drops the workspace registry row
609
+ * but leaves the on-disk dir orphaned (mu can't see it via
610
+ * `mu workspace list / free / path` afterwards). Surfaced during
611
+ * the multi-agent dogfood teardown when three workspaces went
612
+ * orphaned silently.
613
+ *
614
+ * The fix: refuse close if a workspace exists; force the caller to
615
+ * decide. Two actionable resolutions:
616
+ * - `mu workspace free <agent>` first, then close cleanly.
617
+ * - `mu agent close <agent> --discard-workspace` to free the
618
+ * workspace AND close the agent in one shot (lossy: pending
619
+ * changes in the workspace are gone).
620
+ *
621
+ * Maps to exit code 4 (conflict) via the cli.ts handler.
622
+ */
623
+ declare class WorkspacePreservedError extends Error implements HasNextSteps {
624
+ readonly agentName: string;
625
+ readonly workspacePath: string;
626
+ readonly name = "WorkspacePreservedError";
627
+ constructor(agentName: string, workspacePath: string);
628
+ errorNextSteps(): NextStep[];
629
+ }
630
+
631
+ type VcsBackendName = "jj" | "sl" | "git" | "none";
632
+ interface RebaseResult {
633
+ /** The ref the workspace was actually rebased onto (resolved
634
+ * symbolic-or-revset → concrete name). For git that is the
635
+ * resolveGitMainRef() symbolic ref; for jj/sl it's the literal
636
+ * `trunk()` revset (or whatever the operator passed via fromRef). */
637
+ fromRef: string;
638
+ /** Commit subjects (or descriptions) that got replayed, oldest-first.
639
+ * Empty when the workspace was already at fromRef (no-op). */
640
+ replayed: string[];
641
+ /** Files / commits that conflicted during the rebase. Always
642
+ * empty for a successful rebase — a non-empty conflicts list
643
+ * means we threw WorkspaceConflictError before returning. The
644
+ * field exists so the error's serialised payload can carry it. */
645
+ conflicts: string[];
646
+ }
647
+ interface CommitSummary {
648
+ /** Full commit / change id. */
649
+ sha: string;
650
+ /** First-line description / subject. */
651
+ subject: string;
652
+ /** Remainder of the commit message (may be empty). */
653
+ body: string;
654
+ /** ISO-8601 author / commit timestamp. */
655
+ authorDate: string;
656
+ }
657
+ interface CreateWorkspaceOptions$1 {
658
+ /** The repository being branched from. Absolute path. */
659
+ projectRoot: string;
660
+ /** Where to place the new workspace. Absolute path; must NOT exist. */
661
+ workspacePath: string;
662
+ /** Optional commit / branch / changeset id to base off. Backend-specific:
663
+ * git uses it as a `git worktree add`'s ref, jj as a revset, sl as a
664
+ * rev. Undefined = current head. */
665
+ parentRef?: string;
666
+ }
667
+ interface CreateWorkspaceResult {
668
+ /** The actual ref the workspace points at (resolved to a stable id
669
+ * when possible). Stored on the row; useful for `mu workspace list`
670
+ * and for `--commit` flows. May be null for backends that don't
671
+ * expose a meaningful parent (e.g. `none`). */
672
+ parentRef: string | null;
673
+ }
674
+ interface FreeWorkspaceOptions$1 {
675
+ workspacePath: string;
676
+ /** If true, attempt to commit any pending changes BEFORE removal.
677
+ * Backend-specific: jj auto-commits via `jj describe + jj new`, git
678
+ * needs an explicit commit on the worktree, sl needs `sl commit`,
679
+ * none has nothing to commit. If pending changes exist and `commit`
680
+ * is false, the on-disk directory still gets removed and changes are
681
+ * lost \u2014 the verb prints a clear warning. */
682
+ commit: boolean;
683
+ }
684
+ interface FreeWorkspaceResult$1 {
685
+ /** The commit id that captured the pending changes, when `commit` was
686
+ * true and there was something to commit. Otherwise undefined. */
687
+ committedRef?: string;
688
+ /** True iff the on-disk path was actually removed (vs. already gone). */
689
+ removed: boolean;
690
+ }
691
+ interface VcsBackend {
692
+ readonly name: VcsBackendName;
693
+ /** True iff this backend should handle `projectRoot`. Implementations
694
+ * check for the relevant marker dir (`.jj`, `.sl`, `.git`); `none`
695
+ * always returns true and is consulted last. */
696
+ detect(projectRoot: string): Promise<boolean>;
697
+ createWorkspace(opts: CreateWorkspaceOptions$1): Promise<CreateWorkspaceResult>;
698
+ freeWorkspace(opts: FreeWorkspaceOptions$1): Promise<FreeWorkspaceResult$1>;
699
+ /**
700
+ * Count commits that the project's default branch ("main") has but
701
+ * `ref` does not — i.e. how many commits `ref` is BEHIND main.
702
+ *
703
+ * Used by `mu workspace list` and `mu state` to surface staleness
704
+ * (bug_workspace_stale_parent_silent_drift). Cheap, pure-observation:
705
+ * NO automatic fetch. We compare against whatever main resolves to in
706
+ * the workspace's LOCAL refs cache. The user can `git fetch` (or
707
+ * equivalent) themselves if they want a fresher number.
708
+ *
709
+ * Returns null when:
710
+ * - main / trunk cannot be resolved (no origin/HEAD, no origin/main,
711
+ * no origin/master, no trunk() bookmark, etc.)
712
+ * - the underlying VCS command fails for any reason (detached worktree,
713
+ * missing refs, the `none` backend which has no notion of "main")
714
+ *
715
+ * Callers treat null as "unknown — render — — and don't warn".
716
+ */
717
+ commitsBehind(workspacePath: string, ref: string): Promise<number | null>;
718
+ /**
719
+ * Rebase the workspace onto `fromRef` (or the backend's tracked
720
+ * base when undefined: `origin/HEAD` for git, `trunk()` for jj/sl).
721
+ * Returns the resolved ref + replayed commits + conflicts list.
722
+ *
723
+ * Backend-specific behaviour:
724
+ * - git: refuses on dirty WC (WorkspaceDirtyError); fetches first;
725
+ * `git rebase <ref>`. On conflict, aborts the rebase and throws
726
+ * WorkspaceConflictError so the operator resolves manually.
727
+ * - jj: always-snapshotted, so dirty is never an issue. After
728
+ * `jj rebase -d <ref>` the conflict-set is queried via
729
+ * `jj log -r 'conflict()'`. Conflicts surface as
730
+ * WorkspaceConflictError without an abort (jj's conflict markers
731
+ * persist as commits; the operator resolves in-place).
732
+ * - sl: similar to jj. `sl rebase -d <ref>`; conflicts via
733
+ * `sl resolve -l`. On dirty WC sl errors itself; we wrap that
734
+ * into WorkspaceDirtyError.
735
+ * - none: throws WorkspaceVcsRequiredError unconditionally.
736
+ *
737
+ * Surfaced by fb_workspace_recycle_verb: dogfood between waves
738
+ * needed `close → free → spawn` to refresh a worker against new
739
+ * main; that killed the worker's LLM context. `refresh` updates
740
+ * the on-disk dir without touching the agent or pane.
741
+ */
742
+ rebaseTo(workspacePath: string, fromRef?: string): Promise<RebaseResult>;
743
+ /**
744
+ * List commits the workspace has on top of `baseRef`, oldest-first.
745
+ * Used by `mu workspace commits` (fb_workspace_commits_verb) to
746
+ * promote the dogfood-painful
747
+ * cd $(mu workspace path X) && git log <base>..HEAD
748
+ * incantation into a typed verb that knows the workspace's
749
+ * parent_ref. The CommitSummary fields survive subjects/bodies with
750
+ * embedded newlines (NUL-delimited record format on the wire).
751
+ *
752
+ * `none` throws WorkspaceVcsRequiredError. Returns `[]` when the
753
+ * workspace is exactly at baseRef (no commits since fork). Throws
754
+ * on backend command failure (unknown ref, missing repo).
755
+ */
756
+ commitsSinceBase(workspacePath: string, baseRef: string): Promise<CommitSummary[]>;
757
+ }
758
+ declare const noneBackend: VcsBackend;
759
+ declare const gitBackend: VcsBackend;
760
+ declare const jjBackend: VcsBackend;
761
+ declare const slBackend: VcsBackend;
762
+ /** Return the backend that should handle projectRoot. Walks BACKENDS
763
+ * in precedence order; never returns undefined because noneBackend
764
+ * always claims. */
765
+ declare function detectBackend(projectRoot: string): Promise<VcsBackend>;
766
+ /** Look up a backend by name. Throws on unknown name. Used by
767
+ * `mu workspace create --backend ...` to honour an explicit override. */
768
+ declare function backendByName(name: VcsBackendName): VcsBackend;
769
+
770
+ /**
771
+ * Resolve the actual executable to launch in an agent's pane for a given
772
+ * `cli`. Honours the env var `MU_<UPPER_CLI>_COMMAND` (e.g. `MU_PI_COMMAND=
773
+ * pi-alt` makes `--cli pi` actually exec `pi-alt`). Falls back to the cli
774
+ * name itself, which is what users expect when their `pi` binary is on
775
+ * `$PATH` under that exact name.
776
+ *
777
+ * Used by `spawnAgent` to pick the spawned command, and by reconcile's
778
+ * orphan detector so externally-spawned panes running the resolved binary
779
+ * are still recognised as agents.
780
+ */
781
+ declare function resolveCliCommand(cli: string): string;
782
+ interface SpawnAgentOptions {
783
+ name: string;
784
+ workstream: string;
785
+ /** Defaults to "pi". 0.1.0 only really supports "pi" but the column
786
+ * accepts any string for forward-compat with future multi-CLI support
787
+ * (claude/codex). */
788
+ cli?: string;
789
+ /** The actual command to run in the pane. Defaults to the cli value. */
790
+ command?: string;
791
+ /** Window name to group this agent under. Defaults to the agent's name
792
+ * (so each agent gets its own window). Multiple agents sharing a `tab`
793
+ * share a window with multiple panes. */
794
+ tab?: string;
795
+ /** "full-access" (default) or "read-only". The schema stores it; today
796
+ * the role isn't enforced (deferred to a future capabilities pass). */
797
+ role?: string;
798
+ /** Initial working directory for the spawned pane (`tmux -c <path>`).
799
+ * When `workspace: true` is passed, this is ignored — the workspace
800
+ * path is used instead. */
801
+ cwd?: string;
802
+ /** Override the tmux session name. Defaults to `mu-<workstream>`. */
803
+ tmuxSession?: string;
804
+ /** Auto-create a VCS workspace for this agent before spawning the
805
+ * pane and use the workspace path as cwd. Backend defaults to
806
+ * detection (jj > sl > git > none). */
807
+ workspace?: boolean;
808
+ /** Force a specific VCS backend (only meaningful with `workspace: true`). */
809
+ workspaceBackend?: VcsBackendName;
810
+ /** Optional ref to base the workspace on (only meaningful with
811
+ * `workspace: true`). Backend-specific. */
812
+ workspaceFrom?: string;
813
+ /** Project root the workspace branches from (only meaningful with
814
+ * `workspace: true`). Defaults to `process.cwd()`. */
815
+ workspaceProjectRoot?: string;
816
+ }
817
+ /**
818
+ * Spawn a new agent in its tmux pane and register it in the DB.
819
+ *
820
+ * Phases:
821
+ * 1. Validate name + uniqueness.
822
+ * 2. If --workspace: prestageWorkspace() (placeholder agent row +
823
+ * workspace dir + workspace row).
824
+ * 3. createOrReusePane() in the workspace path (or opts.cwd).
825
+ * 4. setPaneTitle + enableMuPaneBordersForPane.
826
+ * 5. finalizeAgentRow() — patch placeholder pane_id to real (workspace
827
+ * path), or insert a fresh agent row (no-workspace path).
828
+ * 6. awaitSpawnLiveness().
829
+ *
830
+ * Failure between any of (3)–(6) calls rollbackSpawn() to undo the
831
+ * pane + row + workspace. The caller-visible error is preserved.
832
+ */
833
+ declare function spawnAgent(db: Db, opts: SpawnAgentOptions): Promise<AgentRow>;
834
+ /**
835
+ * Default liveness window in milliseconds. 0 disables the check (useful
836
+ * for fast tests that don't want to wait). Override via env var
837
+ * `MU_SPAWN_LIVENESS_MS`.
838
+ */
839
+ declare function defaultSpawnLivenessMs(): number;
840
+
841
+ interface AdoptAgentOptions {
842
+ /** tmux pane id (e.g. '%15'). Must already exist on the tmux server. */
843
+ paneId: string;
844
+ /** Workstream to adopt the pane into. The pane MUST be in the
845
+ * matching tmux session (`mu-<workstream>`); cross-session adopt is
846
+ * rejected. */
847
+ workstream: string;
848
+ /** Override the pane's title with this name. When omitted, the pane's
849
+ * current title becomes the agent name (zero-config adoption). */
850
+ name?: string;
851
+ /** Defaults to 'pi' via the schema DEFAULT. */
852
+ cli?: string;
853
+ /** 'full-access' (default) or 'read-only'. */
854
+ role?: string;
855
+ /** Override the tmux session lookup. Defaults to `mu-<workstream>`. */
856
+ tmuxSession?: string;
857
+ }
858
+ interface AdoptAgentResult {
859
+ agent: AgentRow;
860
+ /** True when the pane already had a matching agents row — the call
861
+ * was a no-op (idempotent). */
862
+ alreadyAdopted: boolean;
863
+ /** The pane title before adopt, or null if the pane had no title. */
864
+ previousTitle: string | null;
865
+ /** The title the pane was set to (== agent.name post-adopt). Equal to
866
+ * previousTitle when no retitle happened. */
867
+ paneTitleSetTo: string;
868
+ }
869
+ /**
870
+ * Register an existing tmux pane as a managed mu agent. Inverse of the
871
+ * 'orphan' state surfaced by `mu agent list`: a pane that looks like an
872
+ * agent (running pi/claude/codex) but has no DB row.
873
+ *
874
+ * Identity contract (matches the claim protocol invariant):
875
+ * - Post-adopt, the pane's title equals the agent's name.
876
+ * - When `name` is omitted, the pane's existing title becomes the
877
+ * agent name verbatim. Adopting a pane titled 'pi' would fail name
878
+ * validation — caller must supply --name in that case.
879
+ *
880
+ * Idempotent: adopting the same pane twice with the same name is a
881
+ * no-op (returns alreadyAdopted=true). Adopting a different pane under
882
+ * an existing agent name throws AgentExistsError.
883
+ *
884
+ * Validation order (matches the design in note #100):
885
+ * 1. Pane id format -> assertValidPaneId via paneExists / setPaneTitle
886
+ * 2. Pane exists -> PaneNotFoundError
887
+ * 3. Pane is in session -> AgentNotInWorkstreamError (cross-session)
888
+ * 4. Resolved name OK -> isValidAgentName / Error('agent name invalid')
889
+ * 5. Idempotent check -> if pane already owned by an agent of this
890
+ * name, return alreadyAdopted=true
891
+ * 6. Name not taken -> AgentExistsError (else)
892
+ * 7. Insert + retitle.
893
+ *
894
+ * Status starts at 'free' — reconcile/detect will update it on the next
895
+ * `mu agent list` based on actual pane content (the pi prompt yields
896
+ * 'free'; an agent mid-thought yields 'busy'; etc.). We don't run
897
+ * detection inline here because the caller may not have $TMUX, and
898
+ * adoption shouldn't depend on a captured-pane probe succeeding.
899
+ */
900
+ declare function adoptAgent(db: Db, opts: AdoptAgentOptions): Promise<AdoptAgentResult>;
901
+
902
+ interface AgentRow {
903
+ name: string;
904
+ /** Foreign-name reference to the owning workstream. */
905
+ workstreamName: string;
906
+ cli: string;
907
+ paneId: string;
908
+ status: AgentStatus;
909
+ role: string;
910
+ /** Window name; null when the agent has its own window named after itself. */
911
+ tab: string | null;
912
+ /** ISO 8601 timestamp. */
913
+ createdAt: string;
914
+ /** ISO 8601 timestamp. */
915
+ updatedAt: string;
916
+ /**
917
+ * Derived 'idle but assigned' flag (idle_assigned_agent_detection).
918
+ * Set ONLY by `listLiveAgents` (and the helper `computeAgentIdle`);
919
+ * never stored in the DB. Predicate:
920
+ * status === 'needs_input'
921
+ * AND owns ≥1 IN_PROGRESS task in this workstream
922
+ * AND (now - updated_at) >= MU_IDLE_THRESHOLD_MS (default 300_000ms)
923
+ *
924
+ * Surfaces the third lifecycle state (alive but assigned, no recent
925
+ * progress) to `mu state` renders + `mu state --json`. Omitted (i.e.
926
+ * absent — NOT `false`) when the predicate doesn't fire, so JSON
927
+ * consumers can do a simple `if (agent.idle)` check and the field
928
+ * stays out of the way for callers that don't care.
929
+ */
930
+ idle?: boolean;
931
+ }
932
+ interface InsertAgentInput {
933
+ name: string;
934
+ workstream: string;
935
+ paneId: string;
936
+ status: AgentStatus;
937
+ /** Defaults to "pi" via schema DEFAULT. */
938
+ cli?: string;
939
+ /** Defaults to "full-access" via schema DEFAULT. */
940
+ role?: string;
941
+ tab?: string | null;
942
+ }
943
+ declare function insertAgent(db: Db, input: InsertAgentInput): AgentRow;
944
+ /**
945
+ * Look up an agent by its tmux pane id (e.g. `%4`). Returns undefined if
946
+ * no agent currently owns that pane. Used by `mu me` and friends to
947
+ * answer "which agent am I?" from `$TMUX_PANE` without the LLM having to
948
+ * remember its own name.
949
+ *
950
+ * Note: `pane_id` is not declared UNIQUE in the schema (a managed agent
951
+ * could in theory be re-spawned into the same recycled pane id) but in
952
+ * practice tmux pane ids are unique within a server's lifetime, and
953
+ * reconcile prunes ghosts. We return the first match.
954
+ */
955
+ declare function getAgentByPane(db: Db, paneId: string): AgentRow | undefined;
956
+ declare function getAgent(db: Db, name: string, workstream: string): AgentRow | undefined;
957
+ declare function listAgents(db: Db, opts?: {
958
+ workstream?: string;
959
+ }): AgentRow[];
960
+ /**
961
+ * Update an agent's status. Returns true if a row was matched.
962
+ * Also bumps updated_at. Workstream is required (v5: agents.name is
963
+ * per-workstream unique).
964
+ */
965
+ declare function updateAgentStatus(db: Db, name: string, status: AgentStatus, workstream: string): boolean;
966
+ /** Plain-text emoji map for the agent status. Mirrors statusIcon in
967
+ * cli.ts but without picocolors (tmux pane titles don't render ANSI
968
+ * colour). 'spawning' is omitted on purpose — the title gets the
969
+ * initial render before status detection runs, and 'spawning' is a
970
+ * transient state. */
971
+ declare const STATUS_EMOJI: Record<AgentStatus, string>;
972
+ /** Build the pane title for `agent` based on current DB state.
973
+ * Pure (no tmux side effect; no DB write). Read-only on the DB. */
974
+ declare function composeAgentTitle(db: Db, agent: AgentRow): string;
975
+ /** Push a fresh pane title for `agentName`. Best-effort — a missing
976
+ * agent, a placeholder pane id, or a tmux failure are all swallowed
977
+ * silently (titles are decorative; never block the calling verb). */
978
+ declare function refreshAgentTitle(db: Db, agentName: string, workstream: string): Promise<void>;
979
+ /**
980
+ * Delete an agent row. Returns true if a row was matched. Idempotent;
981
+ * deleting an agent that doesn't exist returns false without throwing.
982
+ *
983
+ * Reaper side-effect: any task that was IN_PROGRESS owned by this
984
+ * agent gets flipped back to OPEN with a `[reaper]` task_note and a
985
+ * `task reap` event in `agent_logs`. The FK on `tasks.owner` is
986
+ * `ON DELETE SET NULL` so the owner column resets automatically; the
987
+ * extra step here is the status revert. Without this an agent that
988
+ * crashed (or was explicitly closed mid-task) leaves the task graph
989
+ * in a wrong state — IN_PROGRESS forever, with no owner to release.
990
+ */
991
+ declare function deleteAgent(db: Db, name: string, workstream: string): boolean;
992
+ declare function isValidAgentName(name: string): boolean;
993
+ /**
994
+ * Send a single line of text to an agent's pane and submit it. Uses the
995
+ * canonical bracketed-paste protocol from src/tmux.ts.
996
+ */
997
+ declare function sendToAgent(db: Db, name: string, text: string, opts: SendOptions & {
998
+ workstream: string;
999
+ }): Promise<void>;
1000
+ /**
1001
+ * Read scrollback from an agent's pane. With no options, returns the full
1002
+ * scrollback (`-S - -E -`); with `lines: N`, returns only the last N lines.
1003
+ */
1004
+ declare function readAgent(db: Db, name: string, opts: CaptureOptions & {
1005
+ workstream: string;
1006
+ }): Promise<string>;
1007
+ interface FreeAgentResult {
1008
+ /** Status before the call. */
1009
+ previousStatus: AgentStatus;
1010
+ /** Status after the call (always 'free' on success). */
1011
+ status: AgentStatus;
1012
+ /** True iff the row actually changed. False on idempotent no-op. */
1013
+ changed: boolean;
1014
+ }
1015
+ /**
1016
+ * Mark an agent's status as `free` — the explicit "I'm done with you
1017
+ * for now; you're available" signal. The agent's pane and DB row are
1018
+ * untouched; reconcile treats `free` as sticky (only flips back to busy
1019
+ * on real activity, never on an idle prompt) so this verb composes
1020
+ * cleanly with the existing scrollback detector.
1021
+ *
1022
+ * Idempotent: setting an already-free agent to free is a no-op (returns
1023
+ * `changed: false`). Throws AgentNotFoundError on missing.
1024
+ */
1025
+ declare function freeAgent(db: Db, name: string, workstream: string): FreeAgentResult;
1026
+ interface CloseAgentOptions {
1027
+ /**
1028
+ * When true, free the agent's workspace BEFORE deleting the agent
1029
+ * (so we control the order rather than relying on FK cascade, which
1030
+ * leaves the on-disk dir orphaned). Lossy: any pending changes in
1031
+ * the workspace are gone unless the caller frees with `--commit`
1032
+ * separately first.
1033
+ *
1034
+ * When false (default) and a workspace exists, throws
1035
+ * WorkspacePreservedError so the caller has to decide explicitly.
1036
+ * Surfaced as a real bug in the multi-agent dogfood teardown.
1037
+ */
1038
+ discardWorkspace?: boolean;
1039
+ }
1040
+ interface CloseAgentResult {
1041
+ killedPane: boolean;
1042
+ deletedRow: boolean;
1043
+ /** True iff the agent had an associated workspace AND the caller
1044
+ * passed `discardWorkspace: true` so we proactively freed it.
1045
+ * False on the no-workspace path (nothing to free) and on the
1046
+ * refused path (we threw before doing anything). */
1047
+ workspaceFreed: boolean;
1048
+ }
1049
+ /**
1050
+ * Close an agent: kill its tmux pane and remove its DB row. Idempotent:
1051
+ * - if the agent doesn't exist in the DB, returns a no-op result
1052
+ * - if the tmux pane is already gone, killPane swallows the error
1053
+ *
1054
+ * Workspace handling: closing an agent and freeing its workspace are
1055
+ * separate concerns (agent lifecycle vs disk artifacts), so by default
1056
+ * `closeAgent` REFUSES if the agent has a workspace — you'd otherwise
1057
+ * orphan the on-disk dir (the FK cascade drops the registry row but
1058
+ * not the directory). Two ways to proceed:
1059
+ *
1060
+ * 1. `freeWorkspace(db, name)` first, then `closeAgent(db, name)`.
1061
+ * Preserves the option to `--commit` pending changes.
1062
+ * 2. `closeAgent(db, name, { discardWorkspace: true })`. One-shot;
1063
+ * lossy.
1064
+ *
1065
+ * The CLI surfaces these as the two actionable nextSteps on the
1066
+ * `WorkspacePreservedError` thrown by the refuse path.
1067
+ */
1068
+ declare function closeAgent(db: Db, name: string, opts: CloseAgentOptions & {
1069
+ workstream: string;
1070
+ }): Promise<CloseAgentResult>;
1071
+ interface ListLiveAgentsOptions {
1072
+ workstream: string;
1073
+ tmuxSession?: string;
1074
+ /**
1075
+ * Which kind of reconciliation pass to run. Forwarded to
1076
+ * `reconcile()`'s same-name option. Default `"full"` (the
1077
+ * documented mutating behaviour `mu agent list` has always had).
1078
+ *
1079
+ * Read-only callers split two ways:
1080
+ * - `mu hud`, `mu state`, bare `mu`, `mu agent attach` →
1081
+ * `"status-only"`: refresh status + title (writes to DB),
1082
+ * skip prune + reap. The operator's primary signal
1083
+ * (busy/needs_input) stays fresh without a periodic poll
1084
+ * racing in-flight spawns.
1085
+ * - `mu doctor`, `mu undo` → `"report-only"`: count drift,
1086
+ * mutate nothing. `mu undo` MUST use this so a post-restore
1087
+ * reconcile doesn't delete the rows the snapshot just
1088
+ * restored (snap_undo_reconcile_destroys_recovered_agents).
1089
+ *
1090
+ * Skipping prune is what protects mid-spawn placeholders (pane
1091
+ * id `%pending-<name>`) from being treated as ghosts and pruned
1092
+ * out from under `createWorkspace`'s FK insert
1093
+ * (bug_agent_spawn_workspace_fk_failure).
1094
+ *
1095
+ * BREAKING: this replaces the previous `dryRun?: boolean`
1096
+ * option. Migration: `dryRun: true` → `mode: "report-only"`;
1097
+ * default (`dryRun: false` / unset) → `mode: "full"`.
1098
+ */
1099
+ mode?: ReconcileMode;
1100
+ }
1101
+ interface LiveAgentsView {
1102
+ /** All registered agents in the workstream, post-reconcile. */
1103
+ agents: AgentRow[];
1104
+ /** Panes in the tmux session that look like agents but aren't registered. */
1105
+ orphans: TmuxPane[];
1106
+ /** Diagnostic numbers from the reconcile pass; useful for `mu doctor`. */
1107
+ report: ReconcileReport;
1108
+ }
1109
+ /**
1110
+ * Return the live, reality-reconciled view of agents in a workstream.
1111
+ * `mu agent list` calls this with `mode: "full"` (mutating); status
1112
+ * pollers (`mu hud`, `mu state`, bare `mu`, `mu agent attach`) call
1113
+ * it with `mode: "status-only"` to refresh status without pruning;
1114
+ * read-only diagnostic / restore paths (`mu doctor`, `mu undo`)
1115
+ * call it with `mode: "report-only"` to mutate nothing at all.
1116
+ */
1117
+ declare function listLiveAgents(db: Db, opts: ListLiveAgentsOptions): Promise<LiveAgentsView>;
1118
+
1119
+ /** True iff `label` matches the archive-label rule. Pure predicate. */
1120
+ declare function isValidArchiveLabel(label: string): boolean;
1121
+ declare class ArchiveNotFoundError extends Error implements HasNextSteps {
1122
+ readonly label: string;
1123
+ readonly name = "ArchiveNotFoundError";
1124
+ constructor(label: string);
1125
+ errorNextSteps(): NextStep[];
1126
+ }
1127
+ declare class ArchiveAlreadyExistsError extends Error implements HasNextSteps {
1128
+ readonly label: string;
1129
+ readonly name = "ArchiveAlreadyExistsError";
1130
+ constructor(label: string);
1131
+ errorNextSteps(): NextStep[];
1132
+ }
1133
+ declare class ArchiveLabelInvalidError extends Error implements HasNextSteps {
1134
+ readonly attempted: string;
1135
+ readonly name = "ArchiveLabelInvalidError";
1136
+ constructor(attempted: string);
1137
+ errorNextSteps(): NextStep[];
1138
+ }
1139
+ interface Archive {
1140
+ /** Surrogate INTEGER id. Internal — operators identify by label. */
1141
+ id: number;
1142
+ /** Globally-unique operator-facing TEXT label. */
1143
+ label: string;
1144
+ /** Optional one-liner description set at create time. */
1145
+ description: string | null;
1146
+ /** ISO 8601, set when the archive was first created. */
1147
+ createdAt: string;
1148
+ /** ISO 8601, bumped on every successful add. */
1149
+ lastAddedAt: string;
1150
+ }
1151
+ interface ArchiveSourceSummary {
1152
+ /** TEXT name of the source workstream this snapshot came from. */
1153
+ name: string;
1154
+ /** Number of archived_tasks rows from this workstream in this archive. */
1155
+ taskCount: number;
1156
+ /** Earliest archived_at among this workstream's rows in this archive. */
1157
+ addedAt: string;
1158
+ }
1159
+ interface ArchiveSummary extends Archive {
1160
+ /** One row per source workstream that contributed to this archive,
1161
+ * sorted by source workstream name. */
1162
+ sourceWorkstreams: ArchiveSourceSummary[];
1163
+ /** Total archived_tasks rows across every source workstream. */
1164
+ totalTasks: number;
1165
+ }
1166
+ interface ArchivedTaskRow {
1167
+ /** Surrogate id of the archived_tasks row. */
1168
+ id: number;
1169
+ /** Operator-facing label of the parent archive. */
1170
+ archiveLabel: string;
1171
+ /** TEXT name of the source workstream (intentionally not an FK). */
1172
+ sourceWorkstream: string;
1173
+ /** The local_id the task had in its source workstream. */
1174
+ originalLocalId: string;
1175
+ title: string;
1176
+ /** Status as stored at archive time. */
1177
+ status: string;
1178
+ impact: number;
1179
+ effortDays: number;
1180
+ /** Owner agent name as snapshotted at archive time. */
1181
+ ownerName: string | null;
1182
+ /** Status at the moment of archive (pinned for re-add semantics). */
1183
+ archivedAtStatus: string;
1184
+ /** ISO 8601, when this row was added to the archive. */
1185
+ archivedAt: string;
1186
+ /** Original tasks.created_at preserved for retrospect ordering. */
1187
+ originalCreatedAt: string;
1188
+ /** Original tasks.updated_at preserved for retrospect ordering. */
1189
+ originalUpdatedAt: string;
1190
+ }
1191
+ interface AddToArchiveResult {
1192
+ /** Number of new archived_tasks rows actually inserted. Zero on a
1193
+ * re-run against the same workstream (idempotency). */
1194
+ addedTasks: number;
1195
+ /** Tasks present in the source workstream that were already in the
1196
+ * archive (skipped by the OR IGNORE). */
1197
+ skippedTasks: number;
1198
+ /** Number of new archived_edges rows actually inserted. */
1199
+ addedEdges: number;
1200
+ /** Number of new archived_notes rows inserted. (Notes have no
1201
+ * natural unique key, so this matches the count of notes attached
1202
+ * to NEW archived_tasks rows; existing rows' notes are not
1203
+ * duplicated because note copy is gated on at-least-one new task
1204
+ * for the (archive, source_workstream) pair.) */
1205
+ addedNotes: number;
1206
+ /** Number of new archived_events rows inserted (one per kind='event'
1207
+ * agent_logs row in the source workstream). */
1208
+ addedEvents: number;
1209
+ }
1210
+ interface RemoveFromArchiveResult {
1211
+ /** Number of archived_tasks rows deleted (cascade cleans the rest). */
1212
+ removedTasks: number;
1213
+ /** Number of archived_edges rows removed by the cascade. */
1214
+ removedEdges: number;
1215
+ /** Number of archived_notes rows removed by the cascade. */
1216
+ removedNotes: number;
1217
+ /** Number of archived_events rows directly deleted. */
1218
+ removedEvents: number;
1219
+ }
1220
+ /**
1221
+ * Create a new archive bucket. Throws `ArchiveAlreadyExistsError` if
1222
+ * the label is already in use; throws `ArchiveLabelInvalidError` for
1223
+ * malformed labels.
1224
+ *
1225
+ * The archive starts EMPTY: created_at and last_added_at both equal
1226
+ * now(). Use `addToArchive(label, workstream)` to populate it.
1227
+ */
1228
+ declare function createArchive(db: Db, label: string, description?: string): Archive;
1229
+ /**
1230
+ * List every archive on this machine, summarised with per-source-
1231
+ * workstream counts. Sorted by label ascending. Pure read; safe to
1232
+ * call against an empty DB (returns []).
1233
+ */
1234
+ declare function listArchives(db: Db): ArchiveSummary[];
1235
+ /**
1236
+ * Look up a single archive by label. Throws `ArchiveNotFoundError`
1237
+ * on miss.
1238
+ */
1239
+ declare function getArchive(db: Db, label: string): ArchiveSummary;
1240
+ /**
1241
+ * Delete an archive and every row that references it. The FK
1242
+ * CASCADE chain (archives → archived_tasks → archived_edges /
1243
+ * archived_notes; archives → archived_events) cleans every row in
1244
+ * one statement.
1245
+ *
1246
+ * Idempotent: throws `ArchiveNotFoundError` rather than silently
1247
+ * succeeding on a missing label (operator confusion safeguard).
1248
+ *
1249
+ * Mirror of `destroyWorkstream`'s safety story but cheaper: archives
1250
+ * have no on-disk artifacts (no tmux session, no workspaces). The
1251
+ * pre-delete snapshot is the operator's recovery path if they run
1252
+ * this verb by mistake (handled in the CLI wrapper, Phase 2).
1253
+ */
1254
+ declare function deleteArchive(db: Db, label: string): void;
1255
+ /**
1256
+ * Add every task in `workstream` to the archive identified by `label`.
1257
+ *
1258
+ * Idempotency invariant: re-running with the same (label, workstream)
1259
+ * pair is a no-op for tasks already present. The
1260
+ * (archive_id, source_workstream, original_local_id) UNIQUE on
1261
+ * archived_tasks is the lever; we INSERT OR IGNORE and skip notes /
1262
+ * events for the (archive, source_workstream) pair entirely when the
1263
+ * task copy added zero new rows. This makes addToArchive
1264
+ * coarse-grained idempotent: the only way to get duplicate notes is
1265
+ * to add a NEW task to the source workstream and re-run, which
1266
+ * legitimately copies the new task's notes.
1267
+ *
1268
+ * Throws:
1269
+ * - `ArchiveNotFoundError` if the label doesn't exist (call
1270
+ * `createArchive` first).
1271
+ * - `WorkstreamNotFoundError` if the source workstream is gone
1272
+ * (you must archive BEFORE destroy).
1273
+ *
1274
+ * The whole operation runs in a transaction so a partial failure
1275
+ * leaves the archive untouched.
1276
+ */
1277
+ declare function addToArchive(db: Db, label: string, workstream: string): AddToArchiveResult;
1278
+ /**
1279
+ * Remove every row contributed by `sourceWorkstream` from the named
1280
+ * archive. Other source workstreams' contributions are untouched
1281
+ * (additive accumulation invariant). Throws `ArchiveNotFoundError`
1282
+ * if the label doesn't exist; returns all-zero counts (no error)
1283
+ * when the source workstream never contributed to this archive.
1284
+ */
1285
+ declare function removeFromArchive(db: Db, label: string, sourceWorkstream: string): RemoveFromArchiveResult;
1286
+ interface ListArchivedTasksOptions {
1287
+ /** Filter by source workstream. Omit to return every source's
1288
+ * contribution, sorted by (source_workstream, original_local_id). */
1289
+ sourceWorkstream?: string;
1290
+ }
1291
+ /**
1292
+ * List archived task rows in a single archive. Throws
1293
+ * `ArchiveNotFoundError` on missing label.
1294
+ *
1295
+ * Default order: source_workstream ASC, then original_local_id ASC,
1296
+ * so the output is deterministic and groups each workstream's
1297
+ * contribution together.
1298
+ */
1299
+ interface ArchiveSearchHit {
1300
+ /** Operator-facing label of the parent archive. */
1301
+ archiveLabel: string;
1302
+ /** TEXT name of the source workstream this row came from. */
1303
+ sourceWorkstream: string;
1304
+ /** local_id the task had in its source workstream. */
1305
+ originalLocalId: string;
1306
+ /** Snapshotted title (always present, even on a note match). */
1307
+ title: string;
1308
+ /** Where the match was found: the title column, or one of this
1309
+ * task's archived_notes.content rows. Title matches win when
1310
+ * both apply (the dedup pass below picks one row per task). */
1311
+ matchKind: "title" | "note";
1312
+ /** Up to ~120 chars of context centered on the FIRST occurrence
1313
+ * of the pattern in the matching field. Case-insensitive index;
1314
+ * the snippet itself preserves original casing. */
1315
+ matchSnippet: string;
1316
+ }
1317
+ interface SearchArchivesOptions {
1318
+ /** LIKE-style needle. Wrapped in `%…%` automatically; `_` and `%`
1319
+ * inside the pattern are still SQL LIKE wildcards (matches the
1320
+ * `searchTasks` convention in src/tasks.ts). Empty / whitespace-
1321
+ * only patterns throw — the CLI is the canonical caller and
1322
+ * enforces it via UsageError before we get here, but the SDK
1323
+ * guards it too so direct programmatic callers don't accidentally
1324
+ * match every row. */
1325
+ pattern: string;
1326
+ /** Restrict to one archive label; undefined = search every
1327
+ * archive. Throws ArchiveNotFoundError on miss. */
1328
+ label?: string;
1329
+ /** Cap on hits returned. Default 50; values below 1 fall back to
1330
+ * the default. There is no `--all` escape hatch — for unbounded
1331
+ * exports use `mu sql`. */
1332
+ limit?: number;
1333
+ }
1334
+ /**
1335
+ * LIKE-search archived task titles AND archived note content. The
1336
+ * pattern is bound as a SQL parameter (never concatenated): an
1337
+ * archive label like `'); DROP TABLE archives; --` round-trips
1338
+ * through `?` without touching the DDL surface.
1339
+ *
1340
+ * Behaviour:
1341
+ * - One row per (archive, source_workstream, original_local_id)
1342
+ * pair. When a task matches via BOTH title and note, the title
1343
+ * row wins (matchKind='title'); only note matches stand on
1344
+ * their own as matchKind='note'.
1345
+ * - With `opts.label`, restricts to that archive. Resolves the
1346
+ * label up-front via the helper; throws ArchiveNotFoundError
1347
+ * on miss.
1348
+ * - Results sorted by (archive label, source workstream,
1349
+ * original_local_id) — the same order `mu archive show` uses,
1350
+ * so a search hit lines up with the show output.
1351
+ * - `limit` defaults to 50 and caps the result set. There is no
1352
+ * unbounded mode (use `mu sql` for raw extracts).
1353
+ */
1354
+ declare function searchArchives(db: Db, opts: SearchArchivesOptions): ArchiveSearchHit[];
1355
+ declare function listArchivedTasks(db: Db, label: string, opts?: ListArchivedTasksOptions): ArchivedTaskRow[];
1356
+
1357
+ type TaskStatus = "OPEN" | "IN_PROGRESS" | "CLOSED" | "REJECTED" | "DEFERRED";
1358
+ /** Every legal task status, in canonical order (matches the schema
1359
+ * CHECK clause). Exported so CLI surfaces (`--status` validators,
1360
+ * --help text, error messages) name them all in one place; missing
1361
+ * one used to silently lie about the supported set. */
1362
+ declare const TASK_STATUSES: readonly TaskStatus[];
1363
+ /** Statuses that count as 'no longer scheduled work' — used by the
1364
+ * goals view and by the dependent-check on reject/defer.
1365
+ *
1366
+ * (The complement — 'statuses that satisfy a blocked-by edge' — is
1367
+ * just `["CLOSED"]` and is hardcoded inline in the SQL views in
1368
+ * src/db.ts. A constant for it was tried and reverted: a one-element
1369
+ * array doesn't earn its keep, and parameterising the SQL views from
1370
+ * a TS const would be brittle.) */
1371
+ declare const STATUSES_TERMINAL_OR_PARKED: readonly TaskStatus[];
1372
+ declare function isTaskStatus(s: string): s is TaskStatus;
1373
+ /** Pipe-separated list of every legal status, e.g.
1374
+ * 'OPEN | IN_PROGRESS | CLOSED | REJECTED | DEFERRED'. Single source
1375
+ * of truth for --help text and error messages so adding a new status
1376
+ * doesn't leave stale lists rotting in the CLI surface. */
1377
+ declare const TASK_STATUS_LIST: string;
1378
+
1379
+ declare class TaskNotFoundError extends Error implements HasNextSteps {
1380
+ readonly taskId: string;
1381
+ readonly name = "TaskNotFoundError";
1382
+ constructor(taskId: string);
1383
+ errorNextSteps(): NextStep[];
1384
+ }
1385
+ declare class TaskExistsError extends Error implements HasNextSteps {
1386
+ readonly taskId: string;
1387
+ readonly name = "TaskExistsError";
1388
+ constructor(taskId: string);
1389
+ errorNextSteps(): NextStep[];
1390
+ }
1391
+ /**
1392
+ * Thrown when a verb is invoked with `-w/--workstream <name>` but the
1393
+ * named task lives in a different workstream. Distinguishes "the user
1394
+ * typo'd the workstream" from "the task doesn't exist anywhere"
1395
+ * (which surfaces as `TaskNotFoundError`). Maps to exit code 4
1396
+ * (conflict / wrong scope).
1397
+ */
1398
+ declare class TaskNotInWorkstreamError extends Error implements HasNextSteps {
1399
+ readonly taskId: string;
1400
+ readonly expectedWorkstream: string;
1401
+ readonly actualWorkstream: string;
1402
+ readonly name = "TaskNotInWorkstreamError";
1403
+ constructor(taskId: string, expectedWorkstream: string, actualWorkstream: string);
1404
+ errorNextSteps(): NextStep[];
1405
+ }
1406
+ declare class TaskAlreadyOwnedError extends Error implements HasNextSteps {
1407
+ readonly taskId: string;
1408
+ readonly currentOwner: string;
1409
+ readonly name = "TaskAlreadyOwnedError";
1410
+ constructor(taskId: string, currentOwner: string);
1411
+ errorNextSteps(): NextStep[];
1412
+ }
1413
+ /**
1414
+ * Thrown by `rejectTask` / `deferTask` when the target task has
1415
+ * dependents that are still OPEN or IN_PROGRESS. Rejecting or
1416
+ * deferring such a task would silently strand the dependents (they'd
1417
+ * remain blocked by a prereq that's never going to satisfy the edge),
1418
+ * so we refuse and force an explicit decision: pass `--cascade` to
1419
+ * apply the same status to every transitive dependent, drop the
1420
+ * blocking edge first with `mu task unblock`, or address the
1421
+ * dependents individually. Maps to exit code 4.
1422
+ */
1423
+ declare class TaskHasOpenDependentsError extends Error implements HasNextSteps {
1424
+ readonly taskId: string;
1425
+ readonly verb: "reject" | "defer";
1426
+ readonly dependents: readonly string[];
1427
+ readonly name = "TaskHasOpenDependentsError";
1428
+ constructor(taskId: string, verb: "reject" | "defer", dependents: readonly string[]);
1429
+ errorNextSteps(): NextStep[];
1430
+ }
1431
+ /**
1432
+ * Thrown when `mu task claim` resolves a claimer agent name (from the
1433
+ * pane title or --for) that has no matching row in the agents table.
1434
+ *
1435
+ * The FK on `tasks.owner` references `agents.name`; without this guard
1436
+ * the claim attempt would fail with the unhelpful 'FOREIGN KEY constraint
1437
+ * failed' from SQLite. This typed error gives the user actionable next
1438
+ * steps (run `mu adopt <pane-id>` to register, or use --for to pick a
1439
+ * different agent).
1440
+ *
1441
+ * Maps to exit code 4 (conflict) via the cli.ts handler.
1442
+ */
1443
+ declare class ClaimerNotRegisteredError extends Error implements HasNextSteps {
1444
+ readonly agentName: string;
1445
+ readonly paneId: string | null;
1446
+ readonly name = "ClaimerNotRegisteredError";
1447
+ constructor(agentName: string, paneId: string | null);
1448
+ /**
1449
+ * Three actionable resolutions in expected-frequency order:
1450
+ * 1. --self : orchestrator pattern (working directly)
1451
+ * 2. --for : dispatcher pattern (assigning to a worker)
1452
+ * 3. mu adopt: registration pattern (promote pane to worker)
1453
+ */
1454
+ errorNextSteps(): NextStep[];
1455
+ }
1456
+ declare class CycleError extends Error implements HasNextSteps {
1457
+ readonly from: string;
1458
+ readonly to: string;
1459
+ readonly name = "CycleError";
1460
+ constructor(from: string, to: string);
1461
+ errorNextSteps(): NextStep[];
1462
+ }
1463
+ declare class CrossWorkstreamEdgeError extends Error implements HasNextSteps {
1464
+ readonly blocker: string;
1465
+ readonly blockerWorkstream: string;
1466
+ readonly dependent: string;
1467
+ readonly dependentWorkstream: string;
1468
+ readonly name = "CrossWorkstreamEdgeError";
1469
+ constructor(blocker: string, blockerWorkstream: string, dependent: string, dependentWorkstream: string);
1470
+ errorNextSteps(): NextStep[];
1471
+ }
1472
+
1473
+ declare function setWaitSleepForTests(impl: ((ms: number) => Promise<void>) | undefined): (ms: number) => Promise<void>;
1474
+ /** Test seam: swap the stderr writer used by the stuck-task warning so
1475
+ * unit tests can capture warnings without spying on process.stderr. */
1476
+ declare function setWaitStuckWarnForTests(impl: ((msg: string) => void) | undefined): (msg: string) => void;
1477
+ /** Total number of polls performed across all `waitForTasks` calls in this
1478
+ * process. Tests typically reset before exercising and read after. */
1479
+ declare function getWaitPollCount(): number;
1480
+ declare function resetWaitPollCount(): void;
1481
+ /** A single task ref the wait verb is watching. Cross-workstream
1482
+ * waits arrive as a heterogeneous list of (workstream, name) pairs;
1483
+ * the legacy single-workstream call passes the same workstream on
1484
+ * every ref. task_wait_cross_workstream. */
1485
+ interface TaskWaitRef {
1486
+ /** The workstream the task lives in. Each ref carries its own so
1487
+ * the SDK doesn't need a single "the workstream" — cross-ws waits
1488
+ * pass refs from multiple workstreams in one call. */
1489
+ workstreamName: string;
1490
+ /** The task's per-workstream-unique local id. */
1491
+ name: string;
1492
+ }
1493
+ interface TaskWaitOptions {
1494
+ /** Target status. Default 'CLOSED'. */
1495
+ status?: TaskStatus;
1496
+ /** When true, succeed as soon as ONE listed task reaches the target.
1497
+ * Default false: every listed task must reach the target. */
1498
+ any?: boolean;
1499
+ /** Maximum time to wait, in milliseconds. Default 600_000 (10 min).
1500
+ * Pass 0 to wait forever. */
1501
+ timeoutMs?: number;
1502
+ /** Polling interval. Default 1000ms; overridable for tests. */
1503
+ pollMs?: number;
1504
+ /** Workstream context applied to bare-string ids. Required when the
1505
+ * caller passes `string[]`; ignored when the caller passes
1506
+ * `TaskWaitRef[]` (each ref carries its own ws). The legacy
1507
+ * single-ws SDK call site keeps its today's shape; the cross-ws
1508
+ * callers (CLI verb) pass `TaskWaitRef[]` and omit `workstream`.
1509
+ * task_wait_cross_workstream. */
1510
+ workstream?: string;
1511
+ /** Emit a yellow STUCK warning to stderr (once per task per wait call)
1512
+ * when an IN_PROGRESS task's owner has been in `needs_input` for at
1513
+ * least this many milliseconds since the agent row's last update.
1514
+ * Default 300_000 (5 min). Pass 0 to disable.
1515
+ *
1516
+ * Surfaced by agent_close_discipline_gap in mufeedback: workers
1517
+ * occasionally finish + commit + go idle without running
1518
+ * `mu task close <id>`, leaving wait blocked indefinitely. The
1519
+ * warning is observation-only — wait keeps polling so the operator
1520
+ * (or a wrapping policy) decides whether to force-close, re-prompt,
1521
+ * or escalate. */
1522
+ stuckAfterMs?: number;
1523
+ /** What to do when the `--stuck-after` predicate fires on a watched
1524
+ * task. `'warn'` (default) = today's behaviour: yellow STUCK line
1525
+ * to stderr (deduped per task per wait call) + corroborating
1526
+ * `kind='event'` agent_logs row; wait keeps polling. `'exit'` =
1527
+ * same emit + persist, but THEN throw `StallDetectedDuringWaitError`
1528
+ * so the CLI wrapper exits 7 (STALL_DETECTED). The exit-action is
1529
+ * the unattended-orchestrator escape: a wrapping policy can branch
1530
+ * on 7 (idle, ambiguous — operator decides poke vs release) vs 6
1531
+ * (dead pane, unambiguous — re-dispatch).
1532
+ *
1533
+ * Carve-out (lives at the call site, not here): the CLI passes
1534
+ * `'exit'` only when the wait target is CLOSED — mirrors exit-6's
1535
+ * reaper-flip suppression. With `--status OPEN` the worker reaching
1536
+ * needs_input might BE the success path. See
1537
+ * task_wait_stall_action_flag. */
1538
+ onStall?: "warn" | "exit";
1539
+ /** Optional async hook run BEFORE every snapshot (initial + each
1540
+ * poll iteration). The CLI uses this to reconcile the workstream
1541
+ * each tick (reaper flips IN_PROGRESS → OPEN for dead-pane
1542
+ * workers) and to throw a typed error when a reaper-flip on a
1543
+ * watched task should abandon the wait — see
1544
+ * task_wait_reconcile_dead_panes. Throwing from `beforePoll`
1545
+ * propagates out of `waitForTasks` unchanged.
1546
+ *
1547
+ * Kept as a generic seam (not a `--reconcile`-shaped option) so
1548
+ * the SDK module stays free of tmux/reconcile imports — that
1549
+ * layering belongs above the SDK in the CLI wrapper. */
1550
+ beforePoll?: () => Promise<void>;
1551
+ }
1552
+ interface TaskWaitTaskState {
1553
+ /** The workstream this task lives in. Cross-workstream waits
1554
+ * return a mixed list; the workstream is part of identity.
1555
+ * task_wait_cross_workstream. */
1556
+ workstreamName: string;
1557
+ /** The task's per-workstream-unique name. */
1558
+ name: string;
1559
+ /** Current status (at the moment we exit). */
1560
+ status: TaskStatus;
1561
+ /** Owner at exit time (NULL when unowned, after release, or after
1562
+ * the reaper flipped IN_PROGRESS → OPEN due to a dead pane). */
1563
+ owner: string | null;
1564
+ /** True when this task's status equals the target. */
1565
+ reachedTarget: boolean;
1566
+ /** True when the task is IN_PROGRESS, owned by a registered agent
1567
+ * whose detected status is `needs_input` for >= `stuckAfterMs`.
1568
+ * Surfaces the agent_close_discipline_gap pattern: worker finished +
1569
+ * committed but skipped `mu task close <id>`. Backwards-compatible
1570
+ * signal — callers ignoring it see no behaviour change. */
1571
+ stuck: boolean;
1572
+ }
1573
+ interface TaskWaitResult {
1574
+ /** Per-task state at exit time. Same length and order as the input list. */
1575
+ tasks: TaskWaitTaskState[];
1576
+ /** True when EVERY task reached the target (the --all condition). */
1577
+ allReached: boolean;
1578
+ /** True when AT LEAST ONE task reached the target (the --any condition). */
1579
+ anyReached: boolean;
1580
+ /** Wall-clock time spent waiting, in ms (always >= 0). */
1581
+ elapsedMs: number;
1582
+ /** True when we exited because of the timeout, not because the wait
1583
+ * condition was met. allReached / anyReached can still be true on
1584
+ * partial progress when timedOut is true. */
1585
+ timedOut: boolean;
1586
+ }
1587
+ /**
1588
+ * Block until a set of tasks reaches `opts.status` (default CLOSED).
1589
+ * Returns a result describing the final state — the caller decides
1590
+ * whether to treat partial-progress timeouts as success or failure
1591
+ * (the CLI maps a clean exit to 0, a timeout to 5).
1592
+ *
1593
+ * Pre-flight: every task in `localIds` MUST exist; missing ones throw
1594
+ * TaskNotFoundError before any waiting begins. This is loud-fail by
1595
+ * design — a typo'd id silently waiting forever is the worst-case UX.
1596
+ */
1597
+ declare function waitForTasks(db: Db, input: readonly TaskWaitRef[] | readonly string[], opts: TaskWaitOptions): Promise<TaskWaitResult>;
1598
+
1599
+ interface SetStatusResult {
1600
+ /** Status before the call. */
1601
+ previousStatus: TaskStatus;
1602
+ /** Status after the call (== requested status). */
1603
+ status: TaskStatus;
1604
+ /** True iff the row actually changed. False on idempotent no-op. */
1605
+ changed: boolean;
1606
+ }
1607
+ /**
1608
+ * Optional evidence string carried on lifecycle verbs (close / open /
1609
+ * claim / release). Lands in the auto-emitted `kind='event'` payload
1610
+ * verbatim, prefixed with `evidence=`. The first inch of distinguishing
1611
+ * "observed" from "claimed" state per an internal critique: the
1612
+ * verb still trusts the caller (it's not a verifier), but the audit
1613
+ * trail records what the caller said it relied on.
1614
+ */
1615
+ interface EvidenceOption {
1616
+ evidence?: string;
1617
+ }
1618
+ /**
1619
+ * Flip a task's status to any of OPEN / IN_PROGRESS / CLOSED.
1620
+ * Idempotent: setting a task to its current status is a no-op (returns
1621
+ * `changed: false`) rather than throwing. Owner is unchanged.
1622
+ */
1623
+ declare function setTaskStatus(db: Db, localId: string, status: TaskStatus, opts: EvidenceOption & {
1624
+ workstream: string;
1625
+ }): SetStatusResult;
1626
+ /** Result of `closeTask` when called with `ifReady: true` and the
1627
+ * task is NOT yet ready to close (still has at least one OPEN /
1628
+ * IN_PROGRESS blocker). Distinguished from a regular `SetStatusResult`
1629
+ * by the literal `skipped` field; the CLI keys on it to switch
1630
+ * between the "closed" and "waiting" rendering paths.
1631
+ *
1632
+ * Surfaced in `fb_umbrella_no_auto_close` (impact=60): a wave umbrella
1633
+ * with N blockers stayed OPEN after every blocker reached a terminal
1634
+ * status. `--if-ready` is the cheap fix: bare `mu task close` is
1635
+ * unchanged (closes regardless), `--if-ready` is a no-op unless every
1636
+ * blocker is in a terminal status (CLOSED / REJECTED / DEFERRED).
1637
+ * Reject and defer satisfy the predicate too because `--if-ready`'s
1638
+ * job is to fire when the umbrella has nothing left to wait for, and
1639
+ * a rejected/deferred blocker is no longer being waited on. */
1640
+ interface CloseSkippedResult {
1641
+ /** Always 'not_ready' when set; future cause-codes can extend this
1642
+ * without reshaping the JSON payload (the literal-union narrows
1643
+ * safely in the CLI rendering path). */
1644
+ skipped: "not_ready";
1645
+ /** Status before the call (always the current status, no change). */
1646
+ previousStatus: TaskStatus;
1647
+ /** Status after the call (== previousStatus, since we no-op). */
1648
+ status: TaskStatus;
1649
+ /** Always false on a skip (no row mutated). */
1650
+ changed: false;
1651
+ /** Local ids of every blocker still in OPEN or IN_PROGRESS, sorted
1652
+ * alphabetically for deterministic rendering. Empty list is
1653
+ * impossible on this branch — the no-op only fires when ≥1
1654
+ * blocker is non-terminal. */
1655
+ blockingIds: string[];
1656
+ }
1657
+ interface CloseTaskOptions extends EvidenceOption {
1658
+ workstream: string;
1659
+ /** When true, no-op the close unless every blocker is in a terminal
1660
+ * status (CLOSED / REJECTED / DEFERRED). Returns a
1661
+ * `CloseSkippedResult` carrying the still-blocking ids; the CLI
1662
+ * renders the skip with a Next: hint pointing at `mu task wait`.
1663
+ * When false / omitted, behaves as bare `closeTask` (closes
1664
+ * regardless of blocker status). */
1665
+ ifReady?: boolean;
1666
+ }
1667
+ /** Convenience: setTaskStatus(db, id, "CLOSED"). Accepts evidence.
1668
+ * Pre-snapshots the DB (snap_design §CAPTURE STRATEGY > WHEN). Skipped
1669
+ * for the idempotent no-op (already CLOSED) so we don't accumulate
1670
+ * empty-delta snapshots on retry loops.
1671
+ *
1672
+ * With `ifReady: true`, returns a `CloseSkippedResult` (no mutation,
1673
+ * no snapshot) when any blocker is still OPEN / IN_PROGRESS. Used by
1674
+ * `mu task close --if-ready` so an orchestrator can fire-and-forget
1675
+ * the umbrella close after every blocker resolves without first
1676
+ * re-querying the graph. */
1677
+ declare function closeTask(db: Db, localId: string, opts: CloseTaskOptions): SetStatusResult | CloseSkippedResult;
1678
+ /** Convenience: setTaskStatus(db, id, "OPEN"). Owner intentionally NOT
1679
+ * cleared — use `releaseTask` for that. Accepts evidence. */
1680
+ declare function openTask(db: Db, localId: string, opts: EvidenceOption & {
1681
+ workstream: string;
1682
+ }): SetStatusResult;
1683
+ interface RejectDeferOptions extends EvidenceOption {
1684
+ /** Workstream context for the root task. All internal task lookups
1685
+ * (including the dependent walk) scope to this workstream. */
1686
+ workstream: string;
1687
+ /** If true, walk the transitive dependent closure and (with `yes`)
1688
+ * apply the same status to every dependent, atomically. Without
1689
+ * `yes`, runs as a dry-run: returns the list of tasks that WOULD
1690
+ * be swept (changedIds) with `dryRun: true` and changes nothing.
1691
+ * Logs one event per task (via setTaskStatus) on commit. */
1692
+ cascade?: boolean;
1693
+ /** Required to actually commit a `cascade` operation. Without it,
1694
+ * cascade is dry-run only — prints the affected dependents so the
1695
+ * caller can verify before sweeping. Mirrors `mu workstream destroy
1696
+ * --yes`. Surfaced in mufeedback bug_cascade_reject_too_aggressive
1697
+ * when an accidentally-cascaded reject swept hud_dogfood (which had
1698
+ * independent merit and needed reopening). */
1699
+ yes?: boolean;
1700
+ }
1701
+ interface RejectDeferResult {
1702
+ /** Tasks that actually changed status, in cascade order (root first). */
1703
+ changedIds: string[];
1704
+ /** The status now stamped on every changedId. */
1705
+ status: TaskStatus;
1706
+ /** True iff anything changed. False on a clean idempotent no-op
1707
+ * (root task already in target status, no dependents). */
1708
+ changed: boolean;
1709
+ /** True iff this was a `cascade` dry-run (cascade requested without
1710
+ * `yes`). In that case `changedIds` lists tasks that WOULD be
1711
+ * swept; the DB is unchanged. */
1712
+ dryRun: boolean;
1713
+ /** Tasks that would be touched by a cascade. Same as `changedIds`
1714
+ * on a dry-run; populated even on a commit so the caller can
1715
+ * report what was swept. */
1716
+ affectedIds: string[];
1717
+ }
1718
+ /** Reject a task: terminal 'won't do' (out of scope, duplicate, wontfix).
1719
+ * Refuses if dependents are open unless `--cascade`.
1720
+ * Pre-snapshots once at the verb level so a cascade onto N children
1721
+ * produces a single snapshot, not N. Skipped for the idempotent no-op. */
1722
+ declare function rejectTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
1723
+ /** Defer a task: parked, may revisit. Same dependent-stranding semantics
1724
+ * as reject (DEFERRED also doesn't satisfy a `--blocked-by` edge).
1725
+ * Pre-snapshots once at the verb level. Skipped for the idempotent no-op. */
1726
+ declare function deferTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
1727
+
1728
+ interface ReleaseResult {
1729
+ /** The previous owner (null if the task was already unowned). */
1730
+ previousOwnerName: string | null;
1731
+ /** Status before the release. */
1732
+ previousStatus: TaskStatus;
1733
+ /** Status after the release. */
1734
+ status: TaskStatus;
1735
+ /** True iff owner OR status actually changed. */
1736
+ changed: boolean;
1737
+ }
1738
+ interface ReleaseTaskOptions extends EvidenceOption {
1739
+ /** Workstream context for the task (v5: tasks.local_id is
1740
+ * per-workstream unique). */
1741
+ workstream: string;
1742
+ /** Force `status = OPEN` regardless of the current status. Without
1743
+ * this flag, `IN_PROGRESS` is also flipped to `OPEN` automatically
1744
+ * (so a released task isn't left structurally stranded with
1745
+ * `owner=NULL, status=IN_PROGRESS`); CLOSED / REJECTED / DEFERRED
1746
+ * are preserved. `--reopen` is the override for the rarer "un-
1747
+ * close and hand back to the pool" workflow. */
1748
+ reopen?: boolean;
1749
+ }
1750
+ /**
1751
+ * Release a task: clear `tasks.owner`.
1752
+ *
1753
+ * Status side-effects (review_release_open_in_progress_inconsistency):
1754
+ * - IN_PROGRESS → OPEN automatically (without it, the task is
1755
+ * stranded: no owner to drive it forward, but `mu task next`
1756
+ * skips it because it's not OPEN).
1757
+ * - OPEN / CLOSED / REJECTED / DEFERRED preserved.
1758
+ * - `--reopen` forces OPEN regardless of current status — the
1759
+ * escape hatch for un-closing a CLOSED owned task in one verb.
1760
+ *
1761
+ * Idempotent: releasing an already-unowned task with no `--reopen` and
1762
+ * no IN_PROGRESS status is a no-op (returns `changed: false`).
1763
+ * Throws TaskNotFoundError on missing.
1764
+ */
1765
+ declare function releaseTask(db: Db, localId: string, opts: ReleaseTaskOptions): ReleaseResult;
1766
+ interface ClaimTaskOptions extends EvidenceOption {
1767
+ /** Workstream context for both the task and the claiming agent.
1768
+ * v5: agents.name and tasks.local_id are per-workstream unique;
1769
+ * the task lookup AND the agent FK lookup scope to this
1770
+ * workstream so a same-named task or worker elsewhere can't be
1771
+ * silently picked. The CLI always passes this from the resolved
1772
+ * -w / $MU_SESSION. */
1773
+ workstream: string;
1774
+ /**
1775
+ * Override the agent name. If omitted, derived from the current pane's
1776
+ * title via `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`.
1777
+ *
1778
+ * Mutually exclusive with `self: true`.
1779
+ */
1780
+ agentName?: string;
1781
+ /**
1782
+ * Workstream that the claimer agent lives in. When omitted, defaults
1783
+ * to `opts.workstream` (today's same-workstream behaviour). Set by
1784
+ * the CLI when `mu task claim X -w A --for B/worker-1` qualifies the
1785
+ * `--for` ref with a different workstream prefix
1786
+ * (`task_claim_for_cross_workstream`).
1787
+ *
1788
+ * Cross-workstream ownership is structurally allowed by the schema:
1789
+ * `tasks.owner_id` is an INTEGER FK to `agents.id` with no
1790
+ * workstream qualifier on the agent side. The per-workstream UNIQUE
1791
+ * on `agents(workstream_id, name)` is what previously made the
1792
+ * SDK's name → id lookup scope to one workstream; this option
1793
+ * widens that lookup to a different workstream when the operator
1794
+ * dispatches across a workstream boundary. The agent's own
1795
+ * workstream remains unchanged — only the task's `owner_id` points
1796
+ * out-of-workstream.
1797
+ */
1798
+ agentWorkstream?: string;
1799
+ /**
1800
+ * Anonymous claim: write `owner = NULL` instead of resolving an agent
1801
+ * name and checking the FK. Use when the actor is the orchestrator
1802
+ * (or a script, or a human) doing direct work in a workstream they
1803
+ * aren't a registered worker in.
1804
+ *
1805
+ * The actor name is still recorded — it ends up in `agent_logs.source`
1806
+ * for the auto-emitted `task claim` event — so provenance is preserved.
1807
+ * Just not in the FK column.
1808
+ *
1809
+ * Resolution order for the actor name (used as the log source):
1810
+ * 1. `actor` if explicitly passed.
1811
+ * 2. Current pane title (when `$TMUX_PANE` is set).
1812
+ * 3. `$USER`.
1813
+ * 4. The literal string 'unknown'.
1814
+ *
1815
+ * Mutually exclusive with `agentName` (the two are alternative
1816
+ * answers to "who's the actor for this claim?"). Passing both is a
1817
+ * usage error.
1818
+ */
1819
+ self?: boolean;
1820
+ /**
1821
+ * Override the actor name used for the log source when `self: true`.
1822
+ * Ignored when `self: false`. Useful when the orchestrator wants to
1823
+ * attribute the work to a meaningful name rather than the pane
1824
+ * title (e.g. "deploy-bot" rather than "pi-mu").
1825
+ */
1826
+ actor?: string;
1827
+ }
1828
+ interface ClaimResult {
1829
+ /** The agent now owning the task, or null when the claim was anonymous (--self). */
1830
+ ownerName: string | null;
1831
+ /** The actor recorded in the agent_logs event — the agent name for a
1832
+ * registered-worker claim, or the resolved actor for --self. */
1833
+ actorName: string;
1834
+ /** The previous owner (null if it was unowned). */
1835
+ previousOwnerName: string | null;
1836
+ /** The status BEFORE the claim; post-claim is IN_PROGRESS unless was CLOSED. */
1837
+ previousStatus: TaskStatus;
1838
+ /** The status AFTER the claim. */
1839
+ status: TaskStatus;
1840
+ }
1841
+ /**
1842
+ * Claim a task. Two modes:
1843
+ *
1844
+ * Worker claim (default):
1845
+ * Resolve an agent name from `opts.agentName` or from $TMUX_PANE's
1846
+ * pane title. The name MUST exist in the agents table (FK on
1847
+ * tasks.owner). Sets `owner = <name>`. This is what mu-spawned
1848
+ * workers do, and what `mu task claim --for <worker>` does for
1849
+ * orchestrator dispatch.
1850
+ *
1851
+ * Anonymous claim (--self):
1852
+ * Skip the name -> agents FK lookup entirely. Sets `owner = NULL`.
1853
+ * Records the actor in `agent_logs.source` instead. This is the
1854
+ * orchestrator-doing-direct-work path — the actor is logged but
1855
+ * not registered as a worker pane.
1856
+ *
1857
+ * Status side-effect: OPEN -> IN_PROGRESS; IN_PROGRESS / CLOSED unchanged.
1858
+ *
1859
+ * Concurrency: the worker-claim path uses a single-statement CAS UPDATE
1860
+ * with `WHERE owner IS NULL OR owner = ?` so two workers racing to
1861
+ * claim the same task can't both win. The anonymous path uses
1862
+ * `WHERE owner IS NULL` (anonymous claims don't 'own' the task in any
1863
+ * exclusive sense; if it's already owned by anyone, the anonymous claim
1864
+ * is a TaskAlreadyOwnedError just like a worker claim would be).
1865
+ */
1866
+ declare function claimTask(db: Db, localId: string, opts: ClaimTaskOptions): Promise<ClaimResult>;
1867
+ /**
1868
+ * Resolve the current actor's identity for attribution in task notes,
1869
+ * --self claims, and any other write that wants 'who did this?'.
1870
+ *
1871
+ * Resolution order:
1872
+ * 1. $MU_AGENT_NAME env var (set by mu spawnAgent on every managed
1873
+ * pane; surfaced from the f3d4bdd commit). Authoritative when
1874
+ * present — you're inside a mu-spawned worker, no ambiguity.
1875
+ * 2. tmux pane title (the pane-title identity step). Works
1876
+ * when running inside any pane mu manages OR adopted.
1877
+ * 3. $USER (when running outside tmux entirely).
1878
+ * 4. The literal 'orchestrator' as a last-resort default.
1879
+ *
1880
+ * Why prefer env over pane title: pane titles are a tmux-server-wide
1881
+ * resource that anything can rewrite. The env var is set per-pane at
1882
+ * spawn time and is unforgeable from outside without explicit
1883
+ * `--actor` override. Pane title is the only identity available for
1884
+ * adopted panes that didn't go through mu's spawn path.
1885
+ */
1886
+ declare function resolveActorIdentity(): Promise<string>;
1887
+
1888
+ interface TaskRow {
1889
+ /** Per-workstream-unique TEXT name. The operator-facing identifier. */
1890
+ name: string;
1891
+ /** Alias for `name` — the per-workstream-unique TEXT id. Emitted alongside
1892
+ * `name` so JSON consumers can dot-access the canonical field name without
1893
+ * having to know that, for tasks specifically, `name` plays the localId
1894
+ * role. Always equal to `name`. */
1895
+ localId: string;
1896
+ /** Foreign-name reference to the owning workstream. */
1897
+ workstreamName: string;
1898
+ title: string;
1899
+ status: TaskStatus;
1900
+ impact: number;
1901
+ effortDays: number;
1902
+ /** Foreign-name reference to the owning agent (NULL when unowned). */
1903
+ ownerName: string | null;
1904
+ createdAt: string;
1905
+ updatedAt: string;
1906
+ }
1907
+ interface TaskNoteRow {
1908
+ author: string | null;
1909
+ content: string;
1910
+ createdAt: string;
1911
+ }
1912
+ declare function isValidTaskId(id: string): boolean;
1913
+ /**
1914
+ * Lowercase title; collapse non-alnum runs into single `_`; trim
1915
+ * leading/trailing `_`; prefix `t_` if the result starts with a digit
1916
+ * (schema requires first char letter); apply the soft cap with
1917
+ * word-boundary trim (cut at the last `_` at-or-before SLUG_SOFT_CAP
1918
+ * when one exists, else hard-truncate). Mirrors `tg`'s `id_from_title`
1919
+ * but adds the soft cap.
1920
+ *
1921
+ * Throws if `title` yields an empty slug after stripping.
1922
+ */
1923
+ declare function slugifyTitle(title: string): string;
1924
+ /**
1925
+ * Result of `slugifyTitleVerbose`: the slug plus enough metadata for
1926
+ * the CLI to decide whether to warn the user that meaning was lost.
1927
+ *
1928
+ * slug — the same string `slugifyTitle` returns.
1929
+ * strippedLength — length of the post-strip pre-cap slug. When this
1930
+ * exceeds the SLUG_SOFT_CAP the verbose form had to
1931
+ * cut at a word boundary (or hard-truncate); the
1932
+ * cut clauses are gone with no in-band signal.
1933
+ * truncated — true iff `slug.length < strippedLength` AFTER the
1934
+ * `t_` digit-prefix correction, i.e. real bytes were
1935
+ * dropped. False for any title that fits under the
1936
+ * soft cap or whose only diff vs the stripped slug
1937
+ * is the `t_` prefix.
1938
+ *
1939
+ * The CLI's `mu task add` uses `truncated` to print a one-line stderr
1940
+ * hint pointing at the `<id>` positional override
1941
+ * (slugifytitle_silently_drops_clauses).
1942
+ */
1943
+ interface SlugifyResult {
1944
+ slug: string;
1945
+ strippedLength: number;
1946
+ truncated: boolean;
1947
+ }
1948
+ /**
1949
+ * Verbose sibling of `slugifyTitle`: returns the slug AND a
1950
+ * `truncated` flag so the CLI can hint to the user when the soft cap
1951
+ * dropped clauses (the meaning-shift hazard documented in
1952
+ * slugifytitle_silently_drops_clauses).
1953
+ *
1954
+ * Algorithm is byte-for-byte identical to `slugifyTitle`; this just
1955
+ * surfaces the metadata that the plain form throws away.
1956
+ */
1957
+ declare function slugifyTitleVerbose(title: string): SlugifyResult;
1958
+ /**
1959
+ * Generate a unique task id from a title. v5: tasks.local_id is
1960
+ * per-workstream unique, so the collision check scopes to one
1961
+ * workstream. On collision, appends `_2`, `_3`, … until unique.
1962
+ */
1963
+ declare function idFromTitle(db: Db, workstream: string, title: string): string;
1964
+ /**
1965
+ * Result of `idFromTitleVerbose`: the unique-in-workstream id plus the
1966
+ * truncated flag from the underlying slugify pass. Used by `mu task
1967
+ * add` to decide whether to surface the stderr hint about lost clauses
1968
+ * (slugifytitle_silently_drops_clauses).
1969
+ */
1970
+ interface IdFromTitleResult {
1971
+ id: string;
1972
+ truncated: boolean;
1973
+ }
1974
+ /**
1975
+ * Verbose sibling of `idFromTitle`: returns both the unique id and the
1976
+ * `truncated` flag from the slugify pass. Collision-suffixing (`_2`,
1977
+ * `_3`, …) does not flip `truncated` — the underlying slug's lossiness
1978
+ * is what the CLI hint cares about.
1979
+ */
1980
+ declare function idFromTitleVerbose(db: Db, workstream: string, title: string): IdFromTitleResult;
1981
+ declare function getTask(db: Db, localId: string, workstream: string): TaskRow | undefined;
1982
+ /**
1983
+ * List tasks. With no `workstream` arg returns every row — used by `mu sql`
1984
+ * and by tests; CLI surfaces always pass a workstream so users only see
1985
+ * their own.
1986
+ */
1987
+ interface ListTasksOptions {
1988
+ /** Filter to one or more lifecycle statuses. Omitted = all statuses. */
1989
+ status?: TaskStatus | readonly TaskStatus[];
1990
+ }
1991
+ declare function listTasks(db: Db, workstream?: string, opts?: ListTasksOptions): TaskRow[];
1992
+ /** Options for listReady. The optional `statuses` filter composes
1993
+ * on top of the `ready` view (which itself constrains to
1994
+ * `status='OPEN'`); passing only OPEN is identical to today's no-
1995
+ * filter shape, passing only non-OPEN values returns []. Exists so
1996
+ * `mu task next --status` can mirror the multi-status flag shape
1997
+ * shipped on `mu task list` (task_list_multi_status_union). */
1998
+ interface ListReadyOptions {
1999
+ status?: TaskStatus | readonly TaskStatus[];
2000
+ }
2001
+ declare function listReady(db: Db, workstream: string, opts?: ListReadyOptions): TaskRow[];
2002
+ declare function listBlocked(db: Db, workstream: string): TaskRow[];
2003
+ declare function listGoals(db: Db, workstream: string): TaskRow[];
2004
+ /** All IN_PROGRESS tasks in a workstream, most-recently-touched first.
2005
+ * Used by `mu state` and `mu hud` to populate their in-progress slice;
2006
+ * exposed as a named SDK helper so those CLI verbs don't re-derive
2007
+ * the row-shape conversion (review_code_raw_task_state_duplicate). */
2008
+ declare function listInProgress(db: Db, workstream: string): TaskRow[];
2009
+ /** Most-recently-closed tasks in a workstream, newest first, capped at
2010
+ * `limit` (default 5). Used by `mu state` for its 'recent closed'
2011
+ * slice; exposed as a named SDK helper so the CLI no longer needs the
2012
+ * raw-row type that was duplicating RawTaskRow
2013
+ * (review_code_raw_task_state_duplicate). */
2014
+ declare function listRecentClosed(db: Db, workstream: string, limit?: number): TaskRow[];
2015
+ /** List notes for a task. Operator-facing local_id; resolves to the
2016
+ * surrogate task id via taskIdFor (with optional workstream scope). */
2017
+ declare function listNotes(db: Db, taskLocalId: string, workstream: string): TaskNoteRow[];
2018
+ /**
2019
+ * All tasks currently owned by `agent` in a given workstream
2020
+ * (v5: agents.name is per-workstream unique). Sorted by local_id.
2021
+ *
2022
+ * Defaults to **excluding CLOSED** since the verb's purpose is "what
2023
+ * is X currently working on?" and a closed task is no longer being
2024
+ * worked on. closeTask intentionally preserves `owner` as a
2025
+ * historical record (so audit/notes can attribute decisions); pass
2026
+ * `{ includeClosed: true }` to surface that history.
2027
+ */
2028
+ declare function listTasksByOwner(db: Db, workstream: string, owner: string, opts?: {
2029
+ includeClosed?: boolean;
2030
+ }): TaskRow[];
2031
+ interface SearchTasksOptions {
2032
+ /** Restrict to one workstream; undefined = search across all. */
2033
+ workstream?: string;
2034
+ /** Also search `task_notes.content` (default false: titles + ids only). */
2035
+ includeNotes?: boolean;
2036
+ }
2037
+ /**
2038
+ * Substring search on task `title` and `local_id`, case-insensitive.
2039
+ * With `includeNotes: true` also searches `task_notes.content`. The
2040
+ * pattern is wrapped in `%...%` automatically so callers don't need
2041
+ * SQL LIKE knowledge — for explicit globs (or regex), use `mu sql`.
2042
+ */
2043
+ declare function searchTasks(db: Db, pattern: string, opts?: SearchTasksOptions): TaskRow[];
2044
+ interface TaskEdges {
2045
+ /** Tasks that must close before this one can start (blockers). */
2046
+ blockers: string[];
2047
+ /** Tasks that this one blocks (dependents). */
2048
+ dependents: string[];
2049
+ }
2050
+ /** One end of an edge with the neighbour's current status attached.
2051
+ * Used by `mu task show` to group blockers/dependents into
2052
+ * "still gating" vs "satisfied" buckets without making the renderer
2053
+ * do a second round-trip to the DB per neighbour. */
2054
+ interface TaskEdgeWithStatus {
2055
+ name: string;
2056
+ status: TaskStatus;
2057
+ }
2058
+ interface TaskEdgesWithStatus {
2059
+ /** Tasks that must close before this one can start (blockers),
2060
+ * carrying each blocker's current status. */
2061
+ blockers: TaskEdgeWithStatus[];
2062
+ /** Tasks that this one blocks (dependents), carrying each
2063
+ * dependent's current status. */
2064
+ dependents: TaskEdgeWithStatus[];
2065
+ }
2066
+ /**
2067
+ * Direct (one-hop) edges for a task. For transitive prerequisites, use
2068
+ * `getPrerequisites()`; this helper is the immediate-neighbour view used
2069
+ * by `mu task show`.
2070
+ */
2071
+ declare function getTaskEdges(db: Db, taskLocalId: string, workstream: string): TaskEdges;
2072
+ /**
2073
+ * Same one-hop edge view as `getTaskEdges`, but each neighbour is
2074
+ * returned as `{ name, status }` so callers can group / colour by
2075
+ * status without an N+1 round-trip. Used by `mu task show` to split
2076
+ * "blocked by" (still-gating) from "satisfied" (already-CLOSED)
2077
+ * blockers, and the symmetric split on the dependents side
2078
+ * (task_show_blocked_by_renders_closed). The status is the neighbour's
2079
+ * full TaskStatus, not just OPEN/CLOSED — REJECTED/DEFERRED still
2080
+ * gate downstream work, so the renderer keeps them in the
2081
+ * still-gating bucket.
2082
+ */
2083
+ declare function getTaskEdgesWithStatus(db: Db, taskLocalId: string, workstream: string): TaskEdgesWithStatus;
2084
+ /**
2085
+ * All tasks transitively reachable from `taskId` via reverse-edge
2086
+ * traversal (i.e. the set of tasks that block this one), including the
2087
+ * task itself.
2088
+ */
2089
+ declare function getPrerequisites(db: Db, taskLocalId: string, workstream: string): Set<string>;
2090
+ interface AddTaskOptions {
2091
+ localId: string;
2092
+ workstream: string;
2093
+ title: string;
2094
+ /** 1..100; enforced by schema CHECK. */
2095
+ impact: number;
2096
+ /** > 0; enforced by schema CHECK. */
2097
+ effortDays: number;
2098
+ /**
2099
+ * Tasks that block this one. Edges inserted as `blocker -> newTask`.
2100
+ * Each blocker must already exist AND share this task's workstream
2101
+ * (cross-workstream edges are forbidden); cycle check guards each
2102
+ * edge. The CLI surfaces this as `--blocked-by`; the SDK key matches.
2103
+ */
2104
+ blockedBy?: string[];
2105
+ }
2106
+ /**
2107
+ * Atomically create a task and (optionally) its incoming blocked-by
2108
+ * edges.
2109
+ *
2110
+ * The task insert + every edge insert + cycle check happen inside one
2111
+ * SQLite transaction. If any blocker is missing or any edge would
2112
+ * create a cycle, the entire add rolls back.
2113
+ *
2114
+ * Cycle check for `addTask` is structurally trivial (a fresh task has
2115
+ * no outgoing edges, so `to -> ... -> from` is impossible). It's still
2116
+ * called here so the same primitive is exercised by tests.
2117
+ */
2118
+ declare function addTask(db: Db, opts: AddTaskOptions): TaskRow;
2119
+ interface AddNoteOptions {
2120
+ /** Free-form author label. Convention: agent name, "user", or "orchestrator". */
2121
+ author?: string;
2122
+ /** Workstream context (operator-facing name). v5: tasks.local_id is
2123
+ * per-workstream unique, so this is required to disambiguate. */
2124
+ workstream: string;
2125
+ }
2126
+ declare function addNote(db: Db, taskLocalId: string, content: string, opts: AddNoteOptions): TaskNoteRow;
2127
+ interface BlockEdgeResult {
2128
+ /** True iff a row was actually inserted (vs. already present). */
2129
+ added: boolean;
2130
+ }
2131
+ /**
2132
+ * Add the edge `blocker → blocked` ('blocker blocks blocked').
2133
+ * Idempotent (existing edge → `added: false`). Validates:
2134
+ *
2135
+ * - both tasks exist
2136
+ * - same workstream (cross-workstream edges forbidden)
2137
+ * - no cycle (the new edge wouldn't form a path blocked → ... → blocker)
2138
+ * - blocker ≠ blocked (no self-reference)
2139
+ */
2140
+ declare function addBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): BlockEdgeResult;
2141
+ interface RemoveBlockEdgeResult {
2142
+ /** True iff a row was actually deleted (vs. no such edge). */
2143
+ removed: boolean;
2144
+ }
2145
+ /**
2146
+ * Remove the edge `blocker → blocked`. Idempotent (no edge →
2147
+ * `removed: false`). Does NOT validate task existence — if the
2148
+ * edge is gone there's nothing to do, regardless of whether the
2149
+ * tasks are gone too.
2150
+ */
2151
+ declare function removeBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): RemoveBlockEdgeResult;
2152
+ interface DeleteTaskResult {
2153
+ /** True iff the row existed and was deleted. False on a dry-run
2154
+ * (preview) AND on the idempotent missing-row case. */
2155
+ deleted: boolean;
2156
+ /** Number of `task_edges` rows cascaded out (informational). On a
2157
+ * dry-run, this is the would-be count. */
2158
+ deletedEdges: number;
2159
+ /** Number of `task_notes` rows cascaded out (informational). On a
2160
+ * dry-run, this is the would-be count. */
2161
+ deletedNotes: number;
2162
+ /** True iff this was a dry-run (`opts.dryRun: true`). On a
2163
+ * dry-run `deleted` is false and the counts are the would-be
2164
+ * counts; the DB is unchanged. Always false on a commit / on a
2165
+ * missing-row idempotent no-op. */
2166
+ dryRun: boolean;
2167
+ /** True iff a matching task row was found at the time of the
2168
+ * call. Discriminator for the CLI: a dry-run that found nothing
2169
+ * (`present: false`) renders differently from a dry-run that
2170
+ * found an existing task with zero edges and zero notes
2171
+ * (`present: true, deletedEdges: 0, deletedNotes: 0`). */
2172
+ present: boolean;
2173
+ }
2174
+ interface DeleteTaskOptions {
2175
+ /** When true, return the cascade preview (would-be edge / note
2176
+ * counts) without mutating and without snapshotting. The CLI uses
2177
+ * this to power the bare `mu task delete <id>` two-phase pattern
2178
+ * (mirrors `mu workstream destroy` / `mu archive delete` /
2179
+ * `mu snapshot prune`). Surfaced by feedback ws task
2180
+ * fb_task_delete_no_yes (impact=30): a dogfood report typed
2181
+ * `mu task delete X --yes` (mirroring workstream destroy) and got
2182
+ * 'unknown option --yes' — the verb took no confirmation flag at
2183
+ * all. Two failed deletes left long-named tasks lingering. */
2184
+ dryRun?: boolean;
2185
+ }
2186
+ /**
2187
+ * Delete a task. FK CASCADE on `task_edges` (from + to) and
2188
+ * `task_notes` cleans the joined rows automatically. Idempotent on
2189
+ * a missing task (returns `deleted: false`).
2190
+ *
2191
+ * Pre-counts the cascade victims for reporting because SQLite's
2192
+ * `changes()` only reports rows directly affected by the DELETE.
2193
+ *
2194
+ * With `opts.dryRun: true`, returns the would-be counts without
2195
+ * touching the DB and without taking a snapshot (no mutation = no
2196
+ * snapshot — same reasoning that gates the closeTask snap on the
2197
+ * idempotent no-op path). The CLI bare `mu task delete <id>` form
2198
+ * uses this; `--yes` calls through with `dryRun: false`.
2199
+ */
2200
+ declare function deleteTask(db: Db, localId: string, workstream: string, opts?: DeleteTaskOptions): DeleteTaskResult;
2201
+ interface UpdateTaskOptions {
2202
+ title?: string;
2203
+ /** 1..100; enforced by schema CHECK. */
2204
+ impact?: number;
2205
+ /** > 0; enforced by schema CHECK. */
2206
+ effortDays?: number;
2207
+ }
2208
+ interface UpdateTaskResult {
2209
+ /** True iff at least one field actually changed. */
2210
+ updated: boolean;
2211
+ /** The fields whose values differ post-update (in `UpdateTaskOptions`'s
2212
+ * camelCase shape). Empty when `updated: false`. */
2213
+ changedFields: string[];
2214
+ }
2215
+ /**
2216
+ * Update scalar fields on a task. Each option is independently optional;
2217
+ * passing none is a typed no-op (returns `updated: false, changedFields: []`).
2218
+ * Fields whose new value equals the current value are skipped (no row change).
2219
+ *
2220
+ * NOT for status (use `closeTask` / `openTask` / `setTaskStatus`), owner
2221
+ * (use `claimTask` / `releaseTask`), local_id (rename is deferred), or
2222
+ * workstream (cross-workstream moves are deferred).
2223
+ */
2224
+ interface UpdateTaskScopeOption {
2225
+ workstream: string;
2226
+ }
2227
+ declare function updateTask(db: Db, localId: string, opts: UpdateTaskOptions, scope: UpdateTaskScopeOption): UpdateTaskResult;
2228
+ interface ReparentTaskResult {
2229
+ /** Edges removed (i.e. all incoming `to_task = taskId` edges). */
2230
+ removedEdges: number;
2231
+ /** Edges added (== blockers.length on success). */
2232
+ addedEdges: number;
2233
+ }
2234
+ /**
2235
+ * Atomically replace every incoming edge of `taskId` with new ones
2236
+ * `blocker[i] → taskId`. Pass an empty `blockers` array to clear all
2237
+ * incoming edges (the task becomes ready iff its status allows).
2238
+ *
2239
+ * Validates ALL new blockers up-front (existence + same workstream +
2240
+ * cycle check); if any fails, no DELETE happens — the call is fully
2241
+ * atomic via a single transaction.
2242
+ *
2243
+ * Cycle reasoning: removing the existing incoming edges to `taskId`
2244
+ * doesn't change `taskId`'s OUTGOING reachability, so
2245
+ * `wouldCreateCycle(db, blocker, taskId)` evaluated against the
2246
+ * pre-state gives the right answer for each new edge.
2247
+ */
2248
+ declare function reparentTask(db: Db, taskLocalId: string, blockers: readonly string[], scope: {
2249
+ workstream: string;
2250
+ }): ReparentTaskResult;
2251
+
2252
+ interface Track {
2253
+ /** Goal tasks (no outgoing edges) belonging to this track. */
2254
+ roots: TaskRow[];
2255
+ /** Every task id reachable as a prerequisite of any root in this track. */
2256
+ taskIds: ReadonlySet<string>;
2257
+ /** Number of READY tasks (per the SQL view) within this track's subgraph. */
2258
+ readyCount: number;
2259
+ }
2260
+ /**
2261
+ * Identify independent task subtrees suitable for parallel assignment
2262
+ * within a workstream. Open goals only; CLOSED goals are excluded as
2263
+ * they no longer represent work to schedule.
2264
+ *
2265
+ * Scoping: only goals belonging to `workstream` are considered.
2266
+ * Cross-workstream edges are forbidden by addTask, so a goal's
2267
+ * prerequisite subgraph is naturally workstream-internal.
2268
+ */
2269
+ declare function getParallelTracks(db: Db, workstream: string): Track[];
2270
+
2271
+ /** One per-task summary inside a per-source-ws section of the manifest. */
2272
+ interface ExportTaskEntry {
2273
+ /** Task local_id == filename stem (`<id>.md`). */
2274
+ id: string;
2275
+ /** Path relative to the bucket root (e.g. `auth/tasks/design.md`). */
2276
+ path: string;
2277
+ /** sha256 of the markdown body bytes; idempotency key. */
2278
+ sha256: string;
2279
+ /** ISO timestamp of the first observed export at which the task
2280
+ * was missing from the source. Absent for tasks still present. */
2281
+ deletedAt?: string;
2282
+ }
2283
+ /** Per-source-ws entry under `manifest.sources`. */
2284
+ interface ExportSourceManifest {
2285
+ /** ISO timestamp the source was first added to the bucket. */
2286
+ addedAt: string;
2287
+ /** ISO timestamp of the most recent re-export of this source. */
2288
+ lastReExportedAt: string;
2289
+ /** `latestSeq(db)` at the most recent re-export; for live workstreams
2290
+ * this is the live `agent_logs.seq` cursor. For archive sources
2291
+ * there is no equivalent live counter — we record the seq at
2292
+ * archive-add time when available, else 0. */
2293
+ eventsSeqAtExport: number;
2294
+ /** Per-task entries; sorted by id for stable diffs. */
2295
+ tasks: ExportTaskEntry[];
2296
+ }
2297
+ /** Top-level bucket manifest. `bucketVersion: 2` — the v0.3 shape.
2298
+ * v1 (bucketVersion absent + top-level `workstream` field) is the
2299
+ * legacy single-source shape and is rejected at write time. */
2300
+ interface ExportManifest {
2301
+ /** Schema discriminator. Always 2 in this codebase. */
2302
+ bucketVersion: 2;
2303
+ /** Operator-chosen bucket label (an archive label, or null for a
2304
+ * one-shot `mu workstream export`). Surfaced in README only. */
2305
+ bucketLabel: string | null;
2306
+ bucketCreatedAt: string;
2307
+ bucketLastUpdatedAt: string;
2308
+ muVersion: string;
2309
+ /** Per-source-ws map; key is the source workstream's TEXT name. */
2310
+ sources: Record<string, ExportSourceManifest>;
2311
+ }
2312
+ /** One source's worth of input: the per-task data the renderer needs.
2313
+ * Both entry points (workstream / archive) collapse to this shape. */
2314
+ interface ExportSource {
2315
+ /** Source workstream name. Becomes the subdirectory name. */
2316
+ name: string;
2317
+ tasks: TaskRow[];
2318
+ /** Per-task edges keyed on task name. Missing keys → no edges. */
2319
+ edges: Map<string, {
2320
+ blockers: string[];
2321
+ dependents: string[];
2322
+ }>;
2323
+ /** Per-task notes keyed on task name. Missing keys → no notes. */
2324
+ notes: Map<string, TaskNoteRow[]>;
2325
+ /** `agent_logs.seq` cursor at this source's snapshot moment. 0 for
2326
+ * archive sources (no live cursor). */
2327
+ eventsSeqAtExport: number;
2328
+ }
2329
+ interface RenderBucketInput {
2330
+ sources: ExportSource[];
2331
+ /** Operator-chosen archive label, or null for a workstream export. */
2332
+ bucketLabel: string | null;
2333
+ outDir: string;
2334
+ }
2335
+ interface RenderBucketResult {
2336
+ outDir: string;
2337
+ /** Per-source-ws stat: how many task files were rewritten across
2338
+ * every source in this call. */
2339
+ written: number;
2340
+ /** Per-source-ws stat: how many task files were sha256-skipped. */
2341
+ unchanged: number;
2342
+ /** Per-source-ws stat: how many task files exist for a task that
2343
+ * has since vanished from the source. Banner is added once. */
2344
+ preserved: number;
2345
+ manifestPath: string;
2346
+ manifest: ExportManifest;
2347
+ }
2348
+ /** Thrown when the operator points an export at a directory whose
2349
+ * existing manifest predates bucket layout (v1, single-source). The
2350
+ * fix is destructive (remove and re-export) so we refuse to touch
2351
+ * it in-place — the legacy directory may be checked into git and
2352
+ * the operator should choose between rebuilding it and picking a
2353
+ * new --out. */
2354
+ declare class LegacyExportLayoutError extends Error {
2355
+ readonly outDir: string;
2356
+ readonly name = "LegacyExportLayoutError";
2357
+ constructor(outDir: string);
2358
+ }
2359
+ /**
2360
+ * Render `input.sources` to disk under `input.outDir` in the v0.3
2361
+ * bucket layout. Idempotent + additive:
2362
+ * - If the bucket doesn't exist, scaffold it.
2363
+ * - If it does exist with bucketVersion 2, MERGE: each source in
2364
+ * `input.sources` either appends (new) or refreshes (existing)
2365
+ * its subdirectory; sources NOT in `input.sources` are left
2366
+ * untouched.
2367
+ * - If it exists but with a legacy (v1) manifest, throw
2368
+ * `LegacyExportLayoutError`.
2369
+ *
2370
+ * Per-task idempotency is sha256-keyed: a re-export of the same
2371
+ * source against an unchanged DB rewrites zero task files. Tasks
2372
+ * that disappear from a source between re-exports are preserved on
2373
+ * disk with a one-time `> **Deleted from DB on <ts>**` banner.
2374
+ */
2375
+ declare function renderToBucket(input: RenderBucketInput): RenderBucketResult;
2376
+ /** Construct an ExportSource for one live workstream by reading the
2377
+ * current DB. Pure data assembly; renderer does the I/O. */
2378
+ declare function exportSourceForWorkstream(db: Db, workstream: string): ExportSource;
2379
+ /** Construct ExportSources for every source workstream that
2380
+ * contributed to an archive label. One ExportSource per
2381
+ * (archive_id, source_workstream) partition. The TaskRow shapes are
2382
+ * reconstructed from archived_* rows; `workstreamName` is set to
2383
+ * the source workstream so the rendered frontmatter reflects the
2384
+ * task's original home. */
2385
+ declare function exportSourcesForArchive(db: Db, label: string): ExportSource[];
2386
+ interface ExportArchiveOptions {
2387
+ label: string;
2388
+ /** Output directory (the bucket). Created if missing. */
2389
+ outDir: string;
2390
+ }
2391
+ interface ExportArchiveResult extends RenderBucketResult {
2392
+ archiveLabel: string;
2393
+ /** Number of source workstreams the renderer wrote / refreshed. */
2394
+ sourceCount: number;
2395
+ }
2396
+ /** Render every source-ws in an archive to a bucket directory.
2397
+ * Throws `ArchiveNotFoundError` (via listArchivedTasks) when the
2398
+ * label doesn't exist. */
2399
+ declare function exportArchive(db: Db, opts: ExportArchiveOptions): ExportArchiveResult;
2400
+
2401
+ declare function isValidWorkstreamName(name: string): boolean;
2402
+ /** Thrown by `ensureWorkstream` and `mu workstream init` when the name
2403
+ * doesn't match the rules. */
2404
+ declare class WorkstreamNameInvalidError extends Error implements HasNextSteps {
2405
+ readonly attempted: string;
2406
+ readonly name = "WorkstreamNameInvalidError";
2407
+ constructor(attempted: string);
2408
+ errorNextSteps(): NextStep[];
2409
+ }
2410
+ /**
2411
+ * Ensure a row exists in the `workstreams` table for `name`. Idempotent;
2412
+ * INSERT OR IGNORE so concurrent callers race safely. Called by
2413
+ * `insertAgent` and `addTask` so callers don't need to remember to call
2414
+ * `mu init` before adding a task / spawning an agent (preserves the
2415
+ * spawn-without-init ergonomics now that agents.workstream and
2416
+ * tasks.workstream are real FKs into this table).
2417
+ *
2418
+ * Validates the name before inserting; throws `WorkstreamNameInvalidError`
2419
+ * for names tmux would silently mangle (containing '.' or ':') or that
2420
+ * exceed 32 chars / start with a non-letter.
2421
+ *
2422
+ * Returns true iff a row was actually inserted (vs. already present).
2423
+ */
2424
+ declare function ensureWorkstream(db: Db, name: string): boolean;
2425
+ interface WorkstreamSummary {
2426
+ /** The workstream's own name. */
2427
+ name: string;
2428
+ /** Tmux session name, defaults to `mu-<name>`. */
2429
+ tmuxSession: string;
2430
+ /** True iff `tmux has-session -t <tmuxSession>` succeeds right now. */
2431
+ tmuxAlive: boolean;
2432
+ /** Rows in `agents` for this workstream. */
2433
+ agentCount: number;
2434
+ /** Rows in `tasks` for this workstream. */
2435
+ taskCount: number;
2436
+ /** Rows in `task_notes` whose task is in this workstream. */
2437
+ noteCount: number;
2438
+ /** Rows in `task_edges` whose `from_task` is in this workstream. */
2439
+ edgeCount: number;
2440
+ /** Rows in `vcs_workspaces` for this workstream. Surfaced so the
2441
+ * destroy dry-run can warn about per-agent worktrees that need
2442
+ * cleanup before the FK cascade silently nukes their rows. */
2443
+ workspaceCount: number;
2444
+ /** True iff a row exists in the `workstreams` table itself. False
2445
+ * for tmux-only `mu-*` sessions that mu never observed via
2446
+ * `mu workstream init`. Surfaced so destroy can clean up bare
2447
+ * registry rows (workstream row exists, no agents/tasks/etc.) —
2448
+ * otherwise such rows are orphaned forever (the previous
2449
+ * `nothingToDo` heuristic short-circuited on them). */
2450
+ registered: boolean;
2451
+ }
2452
+ interface DestroyResult {
2453
+ /** True iff `tmux kill-session` actually killed something. */
2454
+ killedTmux: boolean;
2455
+ /** Number of `agents` rows deleted. */
2456
+ deletedAgents: number;
2457
+ /** Number of `tasks` rows deleted (edges/notes cascade via FK). */
2458
+ deletedTasks: number;
2459
+ /** Number of `task_notes` deleted by the cascade — informational. */
2460
+ deletedNotes: number;
2461
+ /** Number of `task_edges` deleted by the cascade — informational. */
2462
+ deletedEdges: number;
2463
+ /** Number of vcs_workspaces whose on-disk path was actually
2464
+ * removed by the backend on this destroy. Excludes
2465
+ * `alreadyGoneWorkspaces` (those were no-ops on disk). */
2466
+ freedWorkspaces: number;
2467
+ /** Number of vcs_workspaces whose registry row existed but
2468
+ * whose on-disk path was already gone (manual rm -rf or a prior
2469
+ * interrupted destroy). The DB row was cascade-deleted; the
2470
+ * backend did no filesystem work. Tracked separately so the
2471
+ * destroy report doesn't lie about how much cleanup it actually
2472
+ * performed. */
2473
+ alreadyGoneWorkspaces: number;
2474
+ /** Workspaces whose backend cleanup failed (e.g. `git worktree
2475
+ * remove` refused because of uncommitted changes). The DB row
2476
+ * was still cascade-deleted; the on-disk path remains and needs
2477
+ * manual cleanup. */
2478
+ failedWorkspaces: WorkspaceFailure[];
2479
+ }
2480
+ interface WorkspaceFailure {
2481
+ agent: string;
2482
+ backend: string;
2483
+ path: string;
2484
+ error: string;
2485
+ }
2486
+ interface WorkstreamOptions {
2487
+ workstream: string;
2488
+ /** Override the tmux session name. Defaults to `mu-<workstream>`. */
2489
+ tmuxSession?: string;
2490
+ /** Override the per-name VcsBackend resolver. Defaults to
2491
+ * `backendByName`. Lets tests inject a fake backend (e.g. one whose
2492
+ * `freeWorkspace` throws) without mutating the exported singletons —
2493
+ * same pattern as `createWorkspace`'s `opts.backend` accepting a
2494
+ * pre-built `VcsBackend` object. Production callers leave this
2495
+ * unset. */
2496
+ resolveBackend?: (name: VcsBackendName) => VcsBackend;
2497
+ }
2498
+ /**
2499
+ * Discover every workstream visible on this machine. The union of:
2500
+ * - rows in the `workstreams` table (canonical DB source; populated by
2501
+ * `mu init` and auto-created by insertAgent / addTask)
2502
+ * - tmux sessions named `mu-*` (with the prefix stripped) — catches
2503
+ * externally-created `tmux new-session -s mu-foo` that mu hasn't
2504
+ * observed yet
2505
+ *
2506
+ * Returns one `WorkstreamSummary` per workstream, sorted by name.
2507
+ * Useful as a pre-flight before `mu init` ("is this name taken?") and
2508
+ * for `mu doctor`-style diagnostics.
2509
+ */
2510
+ declare function listWorkstreams(db: Db): Promise<WorkstreamSummary[]>;
2511
+ declare function summarizeWorkstream(db: Db, opts: WorkstreamOptions): Promise<WorkstreamSummary>;
2512
+ /**
2513
+ * Tear down a workstream: kill its tmux session and delete every DB row
2514
+ * tagged with its name. Cascades on `tasks` clean up `task_edges` and
2515
+ * `task_notes` automatically (FK ON DELETE CASCADE in the schema).
2516
+ *
2517
+ * Idempotent: safe to call against a workstream that never existed; safe
2518
+ * to call repeatedly. Returns counts so the caller can print a useful
2519
+ * summary.
2520
+ */
2521
+ declare function destroyWorkstream(db: Db, opts: WorkstreamOptions): Promise<DestroyResult>;
2522
+ interface ExportWorkstreamOptions {
2523
+ workstream: string;
2524
+ /** Output directory (the bucket). Defaults to `./<workstream>/`
2525
+ * in the cwd — i.e. the bucket and its single source-ws subdir
2526
+ * share a name. */
2527
+ outDir?: string;
2528
+ }
2529
+ interface ExportResult {
2530
+ outDir: string;
2531
+ /** Per-task files rewritten this call. */
2532
+ written: number;
2533
+ /** Per-task files sha256-skipped this call. */
2534
+ unchanged: number;
2535
+ /** Tasks present in a prior manifest that are no longer in the DB.
2536
+ * Their .md stays on disk; a banner is added once. */
2537
+ preserved: number;
2538
+ manifestPath: string;
2539
+ manifest: ExportManifest;
2540
+ /** Per-source-ws manifest entry for this workstream — convenience
2541
+ * for callers who only want one source's view. */
2542
+ source: ExportSourceManifest;
2543
+ }
2544
+ /**
2545
+ * Export one live workstream to a bucket directory. Idempotent +
2546
+ * additive: re-exporting the same workstream is sha256-skipped,
2547
+ * exporting a different workstream into the same bucket appends a
2548
+ * sibling subdir.
2549
+ *
2550
+ * Throws:
2551
+ * - `LegacyExportLayoutError` if `outDir` already contains a
2552
+ * pre-0.3 (single-source) manifest.json.
2553
+ */
2554
+ declare function exportWorkstream(db: Db, opts: ExportWorkstreamOptions): ExportResult;
2555
+
2556
+ declare class ImportBucketInvalidError extends Error implements HasNextSteps {
2557
+ readonly bucketDir: string;
2558
+ readonly reason: string;
2559
+ readonly name = "ImportBucketInvalidError";
2560
+ constructor(bucketDir: string, reason: string);
2561
+ errorNextSteps(): NextStep[];
2562
+ }
2563
+ declare class ImportLegacyLayoutError extends Error implements HasNextSteps {
2564
+ readonly bucketDir: string;
2565
+ readonly name = "ImportLegacyLayoutError";
2566
+ constructor(bucketDir: string);
2567
+ errorNextSteps(): NextStep[];
2568
+ }
2569
+ declare class WorkstreamAlreadyExistsError extends Error implements HasNextSteps {
2570
+ readonly workstream: string;
2571
+ readonly name = "WorkstreamAlreadyExistsError";
2572
+ constructor(workstream: string);
2573
+ errorNextSteps(): NextStep[];
2574
+ }
2575
+ declare class ImportFrontmatterParseError extends Error implements HasNextSteps {
2576
+ readonly path: string;
2577
+ readonly line: number;
2578
+ readonly raw: string;
2579
+ readonly name = "ImportFrontmatterParseError";
2580
+ constructor(path: string, line: number, raw: string);
2581
+ errorNextSteps(): NextStep[];
2582
+ }
2583
+ declare class ImportEdgeRefMissingError extends Error implements HasNextSteps {
2584
+ readonly fromTask: string;
2585
+ readonly toTask: string;
2586
+ readonly direction: "blocked_by" | "blocks";
2587
+ readonly name = "ImportEdgeRefMissingError";
2588
+ constructor(fromTask: string, toTask: string, direction: "blocked_by" | "blocks");
2589
+ errorNextSteps(): NextStep[];
2590
+ }
2591
+ interface ImportBucketOptions {
2592
+ bucketDir: string;
2593
+ /** Rename the (single) source workstream on import. Only valid when
2594
+ * the bucket has exactly one source-ws subdir (after applying any
2595
+ * `sourceWs` filter); otherwise rejected with an
2596
+ * ImportBucketInvalidError. */
2597
+ workstreamOverride?: string;
2598
+ /** Restrict the import to a subset of source-ws subdirs (by name).
2599
+ * Each name must be a key in the bucket manifest's `sources` map;
2600
+ * otherwise ImportSourceNotInBucketError is raised. Mutually
2601
+ * exclusive with the per-source-ws-subdir invocation form (Form 1):
2602
+ * passing this flag against a Form 1 path raises
2603
+ * ImportBucketInvalidError. Empty array is treated as "no filter";
2604
+ * the CLI rejects an explicitly-empty `--source-ws ,,`. */
2605
+ sourceWs?: string[];
2606
+ /** Walk + parse but write nothing to the DB. */
2607
+ dryRun?: boolean;
2608
+ }
2609
+ interface ImportSourceResult {
2610
+ workstreamName: string;
2611
+ tasksImported: number;
2612
+ edgesImported: number;
2613
+ notesImported: number;
2614
+ tombstonesSkipped: number;
2615
+ /** Per-source-ws errors that did NOT roll back this source. Empty
2616
+ * on success. (Sibling failures live in their own entry.) */
2617
+ errors: string[];
2618
+ }
2619
+ interface ImportBucketResult {
2620
+ bucketLabel: string | null;
2621
+ bucketVersion: number;
2622
+ sources: ImportSourceResult[];
2623
+ }
2624
+ /**
2625
+ * Import a v0.3 bucket directory back into the DB. One source-ws
2626
+ * subdirectory becomes one workstream + N tasks + M edges + K notes.
2627
+ * Per source-ws transactional: a failure in source A rolls back A
2628
+ * but leaves source B's import committed.
2629
+ *
2630
+ * Throws on unrecoverable bucket-level errors (no manifest, legacy
2631
+ * layout, --workstream override against multi-source). Per-source
2632
+ * errors (frontmatter parse, edge ref, target name collision) leave
2633
+ * the failing source's `errors` array populated and that source's
2634
+ * counts at zero; siblings still attempt their own import.
2635
+ */
2636
+ declare function importBucket(db: Db, opts: ImportBucketOptions): ImportBucketResult;
2637
+
2638
+ interface WorkspaceRow {
2639
+ agentName: string;
2640
+ workstreamName: string;
2641
+ backend: VcsBackendName;
2642
+ path: string;
2643
+ parentRef: string | null;
2644
+ createdAt: string;
2645
+ /** How many commits the workspace's parent_ref is behind the project's
2646
+ * default branch HEAD, as of the last time the workspace's local refs
2647
+ * cache was updated. Undefined when not yet computed (the listWorkspaces
2648
+ * fast path leaves it unset; call decorateWithStaleness to populate).
2649
+ * Null when staleness was queried but cannot be computed (no main found,
2650
+ * none-backend, missing parent_ref, command failure). */
2651
+ commitsBehindMain?: number | null;
2652
+ }
2653
+ declare class WorkspaceExistsError extends Error implements HasNextSteps {
2654
+ readonly agent: string;
2655
+ readonly name = "WorkspaceExistsError";
2656
+ constructor(agent: string);
2657
+ errorNextSteps(): NextStep[];
2658
+ }
2659
+ declare class WorkspaceNotFoundError extends Error implements HasNextSteps {
2660
+ readonly agent: string;
2661
+ readonly name = "WorkspaceNotFoundError";
2662
+ constructor(agent: string);
2663
+ errorNextSteps(): NextStep[];
2664
+ }
2665
+ /**
2666
+ * Thrown by createWorkspace when the on-disk path it would create is
2667
+ * already occupied. Distinct from WorkspaceExistsError (which is about
2668
+ * the DB row) so the recovery is clear: the dir is orphaned (no DB
2669
+ * row points at it) and needs cleanup.
2670
+ *
2671
+ * Surfaced as a real bug from the multi-agent dogfood (mufeedback note
2672
+ * #143): users hit a bare 'vcs git: workspacePath already exists' from
2673
+ * the backend, with no nextSteps. After the cccba88 fix (close-refuses-
2674
+ * with-workspace), this case only fires when an orphan from a previous
2675
+ * mu version persists OR when the dir was manually rm-rf'd while a
2676
+ * stale registration remains (the git-worktree case).
2677
+ *
2678
+ * Maps to exit code 4 (conflict).
2679
+ */
2680
+ declare class WorkspacePathNotEmptyError extends Error implements HasNextSteps {
2681
+ readonly agent: string;
2682
+ readonly workstream: string;
2683
+ readonly workspacePath: string;
2684
+ readonly name = "WorkspacePathNotEmptyError";
2685
+ constructor(agent: string, workstream: string, workspacePath: string);
2686
+ errorNextSteps(): NextStep[];
2687
+ }
2688
+ /**
2689
+ * Thrown by createWorkspace when the resolved projectRoot is the
2690
+ * user's $HOME. Surfaced by snap_dogfood Finding 4: a `mu workspace
2691
+ * create` invoked from cwd=$HOME with no --project-root began a
2692
+ * recursive `cp -a` of $HOME (~/Music, ~/.config, ...) into the
2693
+ * workspace dir, stalled on DRM-protected files, and on ctrl-C left
2694
+ * a partial dir behind with no DB row.
2695
+ *
2696
+ * The guard's whole point is to make the user pick a real project
2697
+ * deliberately — there's no --force escape hatch on purpose. The
2698
+ * resolution is `--project-root <real-path>` (or `cd` into a real
2699
+ * project first).
2700
+ *
2701
+ * Maps to exit code 4 (conflict).
2702
+ */
2703
+ declare class HomeDirAsProjectRootError extends Error implements HasNextSteps {
2704
+ readonly agent: string;
2705
+ readonly workstream: string;
2706
+ readonly homeDir: string;
2707
+ readonly name = "HomeDirAsProjectRootError";
2708
+ constructor(agent: string, workstream: string, homeDir: string);
2709
+ errorNextSteps(): NextStep[];
2710
+ }
2711
+ /**
2712
+ * Compose the canonical on-disk path for an agent's workspace. Used by
2713
+ * createWorkspace and reachable from `mu workspace path` so the user
2714
+ * can `cd $(mu workspace path foo)` even before the directory exists.
2715
+ */
2716
+ declare function workspacePath(workstream: string, agent: string): string;
2717
+ /** Root dir for a workstream's workspaces — the parent of all
2718
+ * per-agent workspace dirs. Used by listWorkspaceOrphans to scan
2719
+ * the filesystem. */
2720
+ declare function workspacesRoot(workstream: string): string;
2721
+ interface WorkspaceOrphan {
2722
+ /** The on-disk dir name (the agent name it WOULD be for, if mu had
2723
+ * registered it). */
2724
+ agentName: string;
2725
+ /** Workstream the dir is filed under. */
2726
+ workstreamName: string;
2727
+ /** Absolute path to the orphan dir. */
2728
+ path: string;
2729
+ }
2730
+ /**
2731
+ * Like WorkspaceOrphan but additionally flags whether the parent
2732
+ * workstream itself is gone (no row in `workstreams`). Returned by
2733
+ * listAllOrphanWorkspaces; the per-workstream listWorkspaceOrphans
2734
+ * doesn't carry this since by construction it only runs against an
2735
+ * existing workstream.
2736
+ */
2737
+ interface StrandedWorkspaceOrphan extends WorkspaceOrphan {
2738
+ /** True iff the parent workstream has no DB row (the dir was left
2739
+ * behind by a `mu workstream destroy` or a manual DELETE). */
2740
+ stranded: boolean;
2741
+ }
2742
+ /**
2743
+ * Scan `<state-dir>/workspaces/<workstream>/` for directories that
2744
+ * have no row in `vcs_workspaces`. These are the result of:
2745
+ * - pre-cccba88 agents closed without --discard-workspace
2746
+ * - failed spawn rollbacks (pre-bug_agent_spawn_workspace_fk_failure fix)
2747
+ * - manual cleanup that left the dir but not the row
2748
+ * - any case where the operator manually rm-rf'd vcs_workspaces rows
2749
+ *
2750
+ * Returns `[]` when the workstream's workspaces dir doesn't exist,
2751
+ * or when every dir on disk has a corresponding DB row. Filesystem
2752
+ * read is best-effort: a missing/inaccessible dir returns `[]`
2753
+ * (caller doesn't have to check existsSync first).
2754
+ *
2755
+ * Surfaced by bug_workspace_orphan_not_in_state: orphan dirs were
2756
+ * invisible to `mu state` and `mu workspace list`, but blocked
2757
+ * subsequent `--workspace` spawns with WorkspacePathNotEmptyError.
2758
+ */
2759
+ declare function listWorkspaceOrphans(db: Db, workstream: string): WorkspaceOrphan[];
2760
+ /**
2761
+ * Cross-workstream variant of listWorkspaceOrphans. Reads
2762
+ * `<state-dir>/workspaces/`, recurses one level (per-ws subdir →
2763
+ * per-agent subdir), and surfaces every dir with no row in
2764
+ * `vcs_workspaces`.
2765
+ *
2766
+ * Each entry is additionally tagged with `stranded: boolean`: true
2767
+ * when the parent workstream has no row in `workstreams`. Stranded
2768
+ * orphans are the failure mode this verb was added for — workstreams
2769
+ * destroyed before the close-refuses-with-workspace fix landed (or
2770
+ * via `mu sql DELETE FROM workstreams ...`) would leave their entire
2771
+ * workspace subtree invisible to `mu workspace orphans -w <ws>`,
2772
+ * because the user couldn't know to ask for the right name.
2773
+ *
2774
+ * Surfaced by workspace_orphans_misses_destroyed_workstreams. Returns
2775
+ * `[]` when the workspaces root itself doesn't exist; otherwise scans
2776
+ * best-effort and skips any subdir that fails to read.
2777
+ */
2778
+ declare function listAllOrphanWorkspaces(db: Db): StrandedWorkspaceOrphan[];
2779
+ interface CreateWorkspaceOptions {
2780
+ agent: string;
2781
+ workstream: string;
2782
+ /** Project root to branch from. Defaults to the current working
2783
+ * directory (the `mu` invocation site, which is normally what the
2784
+ * user wants). */
2785
+ projectRoot?: string;
2786
+ /** Override backend detection. Default: walk `detectBackend`.
2787
+ * Accepts either a name ("jj" / "sl" / "git" / "none") OR a
2788
+ * pre-built `VcsBackend` object — the object form lets tests inject
2789
+ * a fresh fake backend without mutating the exported singletons. */
2790
+ backend?: VcsBackendName | VcsBackend;
2791
+ /** Optional ref to base the workspace on. Backend-specific. */
2792
+ parentRef?: string;
2793
+ }
2794
+ /**
2795
+ * Create a fresh workspace for an agent. Allocates the on-disk
2796
+ * directory, records the row, emits a system event. Idempotent ONLY
2797
+ * to the extent that the row check is up-front; if the row exists
2798
+ * we throw `WorkspaceExistsError` rather than silently re-using a
2799
+ * possibly-stale on-disk state. Callers should `freeWorkspace` first.
2800
+ */
2801
+ declare function createWorkspace(db: Db, opts: CreateWorkspaceOptions): Promise<WorkspaceRow>;
2802
+ declare function getWorkspaceForAgent(db: Db, agent: string, workstream: string): WorkspaceRow | undefined;
2803
+ declare function listWorkspaces(db: Db, workstream?: string): WorkspaceRow[];
2804
+ declare function decorateWithStaleness(rows: readonly WorkspaceRow[]): Promise<WorkspaceRow[]>;
2805
+ interface FreeWorkspaceOptions {
2806
+ /** If true, attempt to commit pending changes before tearing down.
2807
+ * Backend-specific; see VcsBackend.freeWorkspace. */
2808
+ commit?: boolean;
2809
+ }
2810
+ interface FreeWorkspaceResult {
2811
+ /** The committed ref, when `commit` was true and there was something
2812
+ * to commit. */
2813
+ committedRef?: string;
2814
+ /** True iff the on-disk path was actually removed. */
2815
+ removed: boolean;
2816
+ /** True iff the DB row was actually deleted. */
2817
+ rowDeleted: boolean;
2818
+ }
2819
+ /**
2820
+ * Tear down an agent's workspace. Calls the backend to remove the
2821
+ * on-disk directory (with optional auto-commit), then DELETEs the row.
2822
+ * Idempotent on a missing workspace (returns all-false).
2823
+ */
2824
+ declare function freeWorkspace(db: Db, agent: string, opts: FreeWorkspaceOptions & {
2825
+ workstream: string;
2826
+ }): Promise<FreeWorkspaceResult>;
2827
+
2828
+ type LogKind = "message" | "event" | "broadcast" | string;
2829
+ interface LogRow {
2830
+ /** Monotonic AUTOINCREMENT id. Use as the cursor for `--since`. */
2831
+ seq: number;
2832
+ /** Workstream this entry belongs to, or `null` for machine-wide. */
2833
+ workstreamName: string | null;
2834
+ /** Free TEXT: agent name, "system", "user", or anything a caller picks. */
2835
+ source: string;
2836
+ /** Free TEXT: "message" (default), "event" (auto state changes),
2837
+ * "broadcast" (explicit cross-agent), or any caller-defined value. */
2838
+ kind: LogKind;
2839
+ /** Free utf-8 string. May be JSON if the kind suggests structure. */
2840
+ payload: string;
2841
+ /** ISO 8601 timestamp set at insert time. */
2842
+ createdAt: string;
2843
+ }
2844
+ interface AppendLogOptions {
2845
+ /** Workstream this entry belongs to. `null` for machine-wide. */
2846
+ workstream: string | null;
2847
+ /** Who emitted this. Agent name, "system", "user", or arbitrary. */
2848
+ source: string;
2849
+ /** Defaults to "message". */
2850
+ kind?: LogKind;
2851
+ /** Free utf-8. Multi-line allowed. */
2852
+ payload: string;
2853
+ }
2854
+ /**
2855
+ * Append a log entry. Returns the inserted row (with assigned `seq`).
2856
+ * Constant-time. Single INSERT; safe to call from any state-changing
2857
+ * verb without a transaction wrapper.
2858
+ */
2859
+ declare function appendLog(db: Db, opts: AppendLogOptions): LogRow;
2860
+ interface ListLogsOptions {
2861
+ /** Filter by workstream. `undefined` = every workstream + machine-wide.
2862
+ * `null` = ONLY machine-wide entries. */
2863
+ workstream?: string | null;
2864
+ /** Strictly > this seq. Use to resume a tail. */
2865
+ since?: number;
2866
+ /** Cap the result. With `since`, returns the FIRST N matching (oldest
2867
+ * first). Without `since`, returns the LAST N (most recent),
2868
+ * re-sorted oldest-first. */
2869
+ limit?: number;
2870
+ source?: string;
2871
+ kind?: string;
2872
+ }
2873
+ /**
2874
+ * List log entries. Always returns oldest-first. Use `since` for
2875
+ * cursor-based reads (the canonical tail pattern); use `limit` alone
2876
+ * for "show me the most recent N" reads.
2877
+ */
2878
+ declare function listLogs(db: Db, opts?: ListLogsOptions): LogRow[];
2879
+ /**
2880
+ * Return the latest seq currently in the table (or 0 if empty). Used
2881
+ * by `mu log --tail` to start the cursor at "now" so the subscriber
2882
+ * only sees NEW entries unless they explicitly pass `--since 0`.
2883
+ */
2884
+ declare function latestSeq(db: Db): number;
2885
+ /**
2886
+ * One-line helper for state-changing SDK functions to auto-emit a
2887
+ * `kind='event'` log entry. Called AFTER the mutation succeeds, only
2888
+ * when the mutation actually produced a change (no-ops stay quiet).
2889
+ *
2890
+ * `source` defaults to 'system' since this is the auto-emission path;
2891
+ * a different source means "a specific agent caused this" and is set
2892
+ * by callers like `claimTask` (source = the claiming agent).
2893
+ */
2894
+ declare function emitEvent(db: Db, workstream: string | null, payload: string, source?: string): void;
2895
+
2896
+ interface SnapshotRow {
2897
+ /** Operator-facing snapshot id. EXCEPTION to the no-surrogate-ids rule:
2898
+ * snapshots have no human-meaningful name; the id is what the
2899
+ * operator types in `mu undo --to <id>` / `mu snapshot show <id>`. */
2900
+ id: number;
2901
+ /** NULL for whole-DB snapshots (e.g. workstream destroy). */
2902
+ workstreamName: string | null;
2903
+ /** Human-readable operation label, e.g. "task close design". */
2904
+ label: string;
2905
+ /** Absolute path to the .db file on disk. */
2906
+ dbPath: string;
2907
+ /** schema_version at the moment of capture. */
2908
+ schemaVersion: number;
2909
+ /** ISO-8601 capture timestamp. */
2910
+ createdAt: string;
2911
+ }
2912
+ interface ListSnapshotsOptions {
2913
+ /** Filter to one workstream. NULL-workstream rows are also returned
2914
+ * when this is set, since they (workstream-destroy snapshots) span
2915
+ * every workstream including this one. */
2916
+ workstream?: string;
2917
+ /** Cap the number of rows returned. Default: no cap. */
2918
+ limit?: number;
2919
+ }
2920
+ interface CaptureSnapshotResult {
2921
+ id: number;
2922
+ dbPath: string;
2923
+ }
2924
+ interface RestoreSnapshotResult {
2925
+ id: number;
2926
+ /** The path the snapshot was copied to (the live DB path). */
2927
+ restoredTo: string;
2928
+ /** schema_version of the restored snapshot (== CURRENT_SCHEMA_VERSION
2929
+ * by virtue of having passed the version check). */
2930
+ schemaVersion: number;
2931
+ }
2932
+ declare class SnapshotNotFoundError extends Error implements HasNextSteps {
2933
+ readonly snapshotId: number;
2934
+ readonly name = "SnapshotNotFoundError";
2935
+ constructor(snapshotId: number);
2936
+ errorNextSteps(): NextStep[];
2937
+ }
2938
+ /**
2939
+ * Thrown by restoreSnapshot when the snapshot's schema_version doesn't
2940
+ * match the live DB's CURRENT_SCHEMA_VERSION. Maps to exit code 4
2941
+ * (conflict). Auto-migration of snapshot files was deliberately rejected
2942
+ * in snap_design note #293 (mutates forensic data; migrations are
2943
+ * forward-only).
2944
+ */
2945
+ declare class SnapshotVersionMismatchError extends Error implements HasNextSteps {
2946
+ readonly snapshotId: number;
2947
+ readonly snapshotVersion: number;
2948
+ readonly currentVersion: number;
2949
+ readonly name = "SnapshotVersionMismatchError";
2950
+ constructor(snapshotId: number, snapshotVersion: number, currentVersion: number);
2951
+ errorNextSteps(): NextStep[];
2952
+ }
2953
+ /**
2954
+ * Thrown when the snapshot's .db file has been removed from disk (manual
2955
+ * cleanup, fs corruption) but the row still exists. Maps to exit code 3
2956
+ * (not found).
2957
+ */
2958
+ declare class SnapshotFileMissingError extends Error implements HasNextSteps {
2959
+ readonly snapshotId: number;
2960
+ readonly dbPath: string;
2961
+ readonly name = "SnapshotFileMissingError";
2962
+ constructor(snapshotId: number, dbPath: string);
2963
+ errorNextSteps(): NextStep[];
2964
+ }
2965
+ /** Read the operator-tunable count cap (`MU_SNAPSHOT_KEEP_LAST`). */
2966
+ declare function gcMaxCount(): number;
2967
+ /** Read the operator-tunable age cap (`MU_SNAPSHOT_MAX_AGE_DAYS`). */
2968
+ declare function gcMaxAgeDays(): number;
2969
+ /**
2970
+ * Resolve the snapshots directory.
2971
+ *
2972
+ * If a live `Db` handle is supplied, snapshots land under
2973
+ * `<dirname(db-path)>/snapshots/` — colocated with the DB they back.
2974
+ * This keeps snapshots discoverable for non-default DB paths
2975
+ * (`MU_DB_PATH=/some/place/foo.db` users) AND keeps tests that use
2976
+ * temp-dir DBs from polluting the user's `~/.local/state/mu/`.
2977
+ *
2978
+ * Without a Db handle, falls back to `<state-dir>/snapshots/` (the
2979
+ * canonical default per snap_design §WHERE).
2980
+ *
2981
+ * Flat (not per-workstream) by design: workstream-destroy snapshots
2982
+ * span every workstream so subdirs would lie about scope.
2983
+ */
2984
+ declare function snapshotsDir(db?: Db): string;
2985
+ /**
2986
+ * Take a whole-DB snapshot before a destructive verb mutates state.
2987
+ *
2988
+ * Steps:
2989
+ * 1. INSERT a row to claim an id.
2990
+ * 2. VACUUM INTO <state-dir>/snapshots/<id>.db. Synchronous; runs
2991
+ * page-level on the live DB without extra locks beyond SQLite's
2992
+ * existing busy_timeout.
2993
+ * 3. UPDATE the row with the canonical db_path (we couldn't know it
2994
+ * before step 1 because id is AUTOINCREMENT).
2995
+ * 4. Run opportunistic GC.
2996
+ *
2997
+ * If VACUUM INTO fails (disk full, perms, race), the row is rolled back
2998
+ * so the DB never points at a non-existent file. The original verb's
2999
+ * exception path still surfaces the underlying error.
3000
+ *
3001
+ * Idempotent on a same-instant double-call (each call gets its own id).
3002
+ */
3003
+ declare function captureSnapshot(db: Db, label: string, workstream?: string | null): CaptureSnapshotResult;
3004
+ /**
3005
+ * List snapshots, newest first. When `workstream` is set, returns rows
3006
+ * for that workstream PLUS rows with workstream = NULL (workstream-
3007
+ * destroy snapshots span every workstream so excluding them would hide
3008
+ * the most-recent restorable point during recovery).
3009
+ */
3010
+ declare function listSnapshots(db: Db, opts?: ListSnapshotsOptions): SnapshotRow[];
3011
+ /**
3012
+ * Restore a snapshot by file-swapping its .db onto the live DB path.
3013
+ *
3014
+ * Caller contract: pass the live `Db` handle so we can read the live DB
3015
+ * path, the snapshot row, and emit a pre-restore self-snapshot for the
3016
+ * "undo of undo" case (snap_design §EDGE CASES > snapshot-of-snapshot).
3017
+ *
3018
+ * The caller is expected to be a short-lived `mu undo` process: this
3019
+ * function CLOSES `db` after taking the pre-restore snapshot, then
3020
+ * fs.copyFileSync's the snapshot file onto the live DB path and unlinks
3021
+ * any -wal / -shm sidecars. Any other live mu process holding the DB
3022
+ * will see SQLITE_BUSY / disk-image-malformed on next write and exit
3023
+ * cleanly (snap_design recommends gating the verb behind --yes for
3024
+ * exactly this reason; that's snap_undo_verb's surface, not ours).
3025
+ */
3026
+ declare function restoreSnapshot(db: Db, snapshotId: number): RestoreSnapshotResult;
3027
+ /**
3028
+ * Drop snapshots that are EITHER past the count cap OR past the age
3029
+ * cap — "whichever cap is more permissive wins" (snap_design §GC).
3030
+ * Concretely: keep the N most recent AND keep everything <D days old;
3031
+ * delete the rest (and their on-disk .db files).
3032
+ *
3033
+ * The caps come from `gcMaxCount()` / `gcMaxAgeDays()` (env-tunable
3034
+ * via `MU_SNAPSHOT_KEEP_LAST` / `MU_SNAPSHOT_MAX_AGE_DAYS`).
3035
+ *
3036
+ * NOTE: prior to snapshot_gc_caps_too_lax_no_cleanup_verb the WHERE
3037
+ * was `created_at < cutoff AND id NOT IN protected`, i.e. "delete
3038
+ * only if BOTH old AND past the count cap". That made the count cap
3039
+ * effectively dead under bursty use (every row was younger than the
3040
+ * 14-day age cap, so the date filter spared everything regardless of
3041
+ * row count). The fix flips AND→OR.
3042
+ *
3043
+ * Best-effort on file unlink: if a file is already gone, the row goes
3044
+ * anyway (the user's intent — "this snapshot is gone" — is satisfied).
3045
+ */
3046
+ declare function gcSnapshots(db: Db): {
3047
+ deletedRows: number;
3048
+ deletedFiles: number;
3049
+ };
3050
+ type PruneMode = "gc" | "keep-last" | "older-than" | "stale-version" | "all";
3051
+ interface PruneOptions {
3052
+ mode: PruneMode;
3053
+ /** For mode='keep-last'. Required by the CLI. */
3054
+ keepLast?: number;
3055
+ /** For mode='older-than'. Days; required by the CLI. */
3056
+ olderThanDays?: number;
3057
+ /** When true, return the would-delete shape but don't touch the DB
3058
+ * or the on-disk .db files. */
3059
+ dryRun?: boolean;
3060
+ }
3061
+ interface PruneResult {
3062
+ /** Rows that would be / were deleted. Always populated, even on
3063
+ * dry-run (the CLI's summary uses it). */
3064
+ victims: SnapshotRow[];
3065
+ /** Total bytes that would be / were freed (sum of victim file
3066
+ * sizes; missing files contribute 0). */
3067
+ freedBytes: number;
3068
+ /** Number of `snapshots` rows actually deleted. 0 on dry-run. */
3069
+ deletedRows: number;
3070
+ /** Number of on-disk .db files actually unlinked. 0 on dry-run. */
3071
+ deletedFiles: number;
3072
+ /** Set when mode='all' and dryRun=false: id of the safety-net
3073
+ * snapshot captured BEFORE the wipe. (Survives the wipe.) */
3074
+ safetyNetSnapshotId?: number;
3075
+ }
3076
+ declare class PruneOptionsInvalidError extends Error implements HasNextSteps {
3077
+ readonly name = "PruneOptionsInvalidError";
3078
+ errorNextSteps(): NextStep[];
3079
+ }
3080
+ /** True if a snapshot row's schema_version doesn't match the live DB's
3081
+ * CURRENT_SCHEMA_VERSION. Stale snapshots are unrestorable (restore
3082
+ * raises SnapshotVersionMismatchError) — surfaced dimmed in
3083
+ * `mu snapshot list` and as the target set of `prune --stale-version`. */
3084
+ declare function isStaleVersion(row: {
3085
+ schemaVersion: number;
3086
+ }): boolean;
3087
+ /**
3088
+ * Bulk policy-driven cleanup. The CLI's `mu snapshot prune` verb is
3089
+ * a thin wrapper. Modes:
3090
+ *
3091
+ * gc — apply the auto-GC policy explicitly (same as the
3092
+ * opportunistic call inside captureSnapshot).
3093
+ * keep-last — keep only the N newest rows.
3094
+ * older-than — drop rows whose created_at is older than D days.
3095
+ * stale-version — drop rows whose schema_version != current.
3096
+ * all — drop EVERY row. dryRun=false additionally captures
3097
+ * a safety-net snapshot of the live DB FIRST, so a
3098
+ * subsequent `mu undo` can recover; the safety-net
3099
+ * row survives the wipe.
3100
+ *
3101
+ * On dryRun=true: returns the victim set + freed-bytes total without
3102
+ * touching the DB or the filesystem.
3103
+ */
3104
+ declare function pruneSnapshots(db: Db, opts: PruneOptions): PruneResult;
3105
+ interface DeleteSnapshotResult {
3106
+ /** Always true on success. (Misses raise SnapshotNotFoundError; the
3107
+ * shape mirrors `deleteTask`'s structured-result style.) */
3108
+ deleted: true;
3109
+ /** 1 if the .db file was on disk + unlinked; 0 if it was already
3110
+ * gone (orphaned row). */
3111
+ deletedFiles: 0 | 1;
3112
+ /** Bytes freed by unlinking the .db file. 0 when the file was
3113
+ * already gone. */
3114
+ freedBytes: number;
3115
+ }
3116
+ /**
3117
+ * Surgical removal of one snapshot: drop the `snapshots` row + unlink
3118
+ * the on-disk .db file. Mirrors `mu task delete`. Errors with
3119
+ * `SnapshotNotFoundError` on miss.
3120
+ *
3121
+ * No auto-snapshot before the delete: the point IS to delete one row,
3122
+ * and removing one stepping-stone can't break `mu undo` (it still has
3123
+ * every other snapshot). Auto-snapshotting here would be circular.
3124
+ */
3125
+ declare function deleteSnapshot(db: Db, snapshotId: number): DeleteSnapshotResult;
3126
+ /** Return the on-disk size of the snapshot file in bytes, or null if
3127
+ * the file is missing. Useful for `mu snapshot list --json` output. */
3128
+ declare function snapshotFileSize(snapshot: SnapshotRow): number | null;
3129
+
3130
+ export { type AddNoteOptions, type AddTaskOptions, type AddToArchiveResult, type AdoptAgentOptions, type AdoptAgentResult, AgentDiedOnSpawnError, AgentExistsError, AgentNotFoundError, AgentNotInWorkstreamError, type AgentRow, type AgentStatus, type AppendLogOptions, type Archive, ArchiveAlreadyExistsError, ArchiveLabelInvalidError, ArchiveNotFoundError, type ArchiveSearchHit, type ArchiveSourceSummary, type ArchiveSummary, type ArchivedTaskRow, type BlockEdgeResult, CURRENT_SCHEMA_VERSION, type CaptureOptions, type CaptureSnapshotResult, type ClaimResult, type ClaimTaskOptions, ClaimerNotRegisteredError, type CloseAgentOptions, type CloseAgentResult, CrossWorkstreamEdgeError, CycleError, type Db, type DeleteSnapshotResult, type DeleteTaskResult, type DestroyResult, type DetectedStatus, EXPECTED_TABLES, type EvidenceOption, type ExportArchiveOptions, type ExportArchiveResult, type ExportManifest, type ExportResult, type ExportSource, type ExportSourceManifest, type ExportTaskEntry, type ExportWorkstreamOptions, type FreeAgentResult, HomeDirAsProjectRootError, type IdFromTitleResult, ImportBucketInvalidError, type ImportBucketOptions, type ImportBucketResult, ImportEdgeRefMissingError, ImportFrontmatterParseError, ImportLegacyLayoutError, type ImportSourceResult, type InsertAgentInput, LegacyExportLayoutError, type ListArchivedTasksOptions, type ListLiveAgentsOptions, type ListLogsOptions, type ListReadyOptions, type ListSnapshotsOptions, type ListTasksOptions, type LiveAgentsView, type LogKind, type LogRow, type NewSessionOptions, type NewWindowOptions, type OpenDbOptions, PANE_ID_RE, PaneNotFoundError, type PruneMode, type PruneOptions, PruneOptionsInvalidError, type PruneResult, type ReconcileMode, type ReconcileOptions, type ReconcileReport, type RejectDeferOptions, type RejectDeferResult, type ReleaseResult, type ReleaseTaskOptions, type RemoveBlockEdgeResult, type RemoveFromArchiveResult, type RenderBucketInput, type RenderBucketResult, type ReparentTaskResult, type RestoreSnapshotResult, STATUSES_TERMINAL_OR_PARKED, STATUS_EMOJI, SchemaTooOldError, type SearchArchivesOptions, type SearchTasksOptions, type SendOptions, type SetStatusResult, type SlugifyResult, SnapshotFileMissingError, SnapshotNotFoundError, type SnapshotRow, SnapshotVersionMismatchError, type SpawnAgentOptions, type SplitWindowOptions, type StrandedWorkspaceOrphan, TASK_STATUSES, TASK_STATUS_LIST, TaskAlreadyOwnedError, type TaskEdgeWithStatus, type TaskEdges, type TaskEdgesWithStatus, TaskExistsError, TaskHasOpenDependentsError, TaskNotFoundError, TaskNotInWorkstreamError, type TaskNoteRow, type TaskRow, type TaskStatus, type TaskWaitOptions, type TaskWaitRef, type TaskWaitResult, type TaskWaitTaskState, TmuxError, type TmuxExecResult, type TmuxExecutor, type TmuxPane, type TmuxSession, type TmuxWindow, type Track, type UpdateTaskOptions, type UpdateTaskResult, type VcsBackend, type VcsBackendName, type CreateWorkspaceOptions$1 as VcsCreateWorkspaceOptions, type CreateWorkspaceResult as VcsCreateWorkspaceResult, type FreeWorkspaceOptions$1 as VcsFreeWorkspaceOptions, type FreeWorkspaceResult$1 as VcsFreeWorkspaceResult, type CreateWorkspaceOptions as WorkspaceCreateOptions, WorkspaceExistsError, type WorkspaceFailure, type FreeWorkspaceOptions as WorkspaceFreeOptions, type FreeWorkspaceResult as WorkspaceFreeResult, WorkspaceNotFoundError, type WorkspaceOrphan, WorkspacePathNotEmptyError, WorkspacePreservedError, type WorkspaceRow, WorkstreamAlreadyExistsError, WorkstreamNameInvalidError, type WorkstreamOptions, type WorkstreamSummary, addBlockEdge, addNote, addTask, addToArchive, adoptAgent, appendLog, assertValidPaneId, backendByName, capturePane, captureSnapshot, claimTask, closeAgent, closeTask, composeAgentTitle, createArchive, createWorkspace, currentAgentName, currentPaneTitle, decorateWithStaleness, defaultDbPath, defaultSendDelayMs, defaultSpawnLivenessMs, defaultStateDir, deferTask, deleteAgent, deleteArchive, deleteSnapshot, deleteTask, destroyWorkstream, detectBackend, detectPiStatus, emitEvent, ensureWorkstream, ensureWorkstreamStateDir, exportArchive, exportSourceForWorkstream, exportSourcesForArchive, exportWorkstream, extractTail, freeAgent, freeWorkspace, gcMaxAgeDays, gcMaxCount, gcSnapshots, getAgent, getAgentByPane, getArchive, getParallelTracks, getPrerequisites, getTask, getTaskEdges, getTaskEdgesWithStatus, getWaitPollCount, getWorkspaceForAgent, gitBackend, idFromTitle, idFromTitleVerbose, importBucket, insertAgent, isStaleVersion, isTaskStatus, isValidAgentName, isValidArchiveLabel, isValidPaneId, isValidTaskId, isValidWorkstreamName, jjBackend, killPane, killSession, latestSeq, listAgents, listAllOrphanWorkspaces, listArchivedTasks, listArchives, listBlocked, listGoals, listInProgress, listLiveAgents, listLogs, listNotes, listPanes, listPanesInSession, listReady, listRecentClosed, listSessions, listSnapshots, listTasks, listTasksByOwner, listWindows, listWorkspaceOrphans, listWorkspaces, listWorkstreams, newSession, newSessionWithPane, newWindow, noneBackend, openDb, openTask, paneExists, parseAgentNameFromTitle, pruneSnapshots, readAgent, reconcile, refreshAgentTitle, rejectTask, releaseTask, removeBlockEdge, removeFromArchive, renderToBucket, reparentTask, resetSleep, resetTmuxExecutor, resetWaitPollCount, resolveActorIdentity, resolveCliCommand, restoreSnapshot, searchArchives, searchTasks, selectLayout, sendToAgent, sendToPane, sessionExists, setPaneTitle, setSleepForTests, setTaskStatus, setTmuxExecutor, setWaitSleepForTests, setWaitStuckWarnForTests, slBackend, sleep, slugifyTitle, slugifyTitleVerbose, snapshotFileSize, snapshotsDir, spawnAgent, splitWindow, summarizeWorkstream, tmux$1 as tmux, updateAgentStatus, updateTask, waitForTasks, workspacePath, workspacesRoot, workstreamStateDir };