@martintrojer/mu 0.3.2 → 0.4.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.
package/dist/index.d.ts CHANGED
@@ -49,15 +49,6 @@ declare function defaultStateDir(): string;
49
49
  * MU_DB_PATH > <state-dir>/mu.db
50
50
  */
51
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
52
  /**
62
53
  * Open the mu database. Creates the parent directory and applies the schema
63
54
  * idempotently on every open. Safe to call from many short-lived processes
@@ -71,15 +62,13 @@ declare class SchemaTooOldError extends Error implements HasNextSteps {
71
62
  constructor(detectedVersion: number, requiredVersion: number);
72
63
  errorNextSteps(): NextStep[];
73
64
  }
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;
65
+ /** The schema version a fresh DB starts at. v8 adds the
66
+ * machine_identity and workstream_sync sync substrate on top of v7
67
+ * (which dropped the approvals table), v6 (which added 5 archive_*
68
+ * tables), and v5's surrogate-PK substrate. The refusal floor is
69
+ * v5 pre-v5 DBs throw `SchemaTooOldError`; v5+ DBs are
70
+ * forward-bumped in place by `applySchema`. */
71
+ declare const CURRENT_SCHEMA_VERSION = 8;
83
72
  /** Tables a healthy DB must contain. Single source of truth so
84
73
  * `mu doctor` and any other consumer don't drift. Adding a new table
85
74
  * = one new entry here AND a CREATE TABLE in CURRENT_SCHEMA. (Schema
@@ -221,7 +210,19 @@ interface NewSessionWithPaneOptions {
221
210
  * session left behind by a failed spawn.
222
211
  */
223
212
  declare function newSessionWithPane(name: string, opts: NewSessionWithPaneOptions): Promise<string>;
224
- /** Idempotent: succeeds even if the session is already gone. */
213
+ /**
214
+ * Idempotent: succeeds even if the session is already gone.
215
+ *
216
+ * Three swallowed shapes:
217
+ * - "can't find session: <name>" — session never existed.
218
+ * - "session not found" — alternate phrasing on some tmux builds.
219
+ * - "no server running on <path>" — the tmux server itself has exited
220
+ * (typical when the test suite runs against a private `tmux -L
221
+ * <socket>` server and the just-killed session was its last; tmux
222
+ * quietly shuts the server down). Without this, killSession would
223
+ * throw on the very next idempotent call — only visible under
224
+ * Layer 3 of bug_test_suite_flake_leaks_isolation.
225
+ */
225
226
  declare function killSession(name: string): Promise<void>;
226
227
  declare function listWindows(session?: string): Promise<TmuxWindow[]>;
227
228
  interface NewWindowOptions {
@@ -338,8 +339,7 @@ declare function enableMuPaneBordersForPane(paneId: string): Promise<void>;
338
339
  * The border is tmux chrome, not pane content: it doesn't scroll, it
339
340
  * survives copy-mode, and the inner CLI never sees it.
340
341
  *
341
- * Designed in roadmap-v0-2 hud_visual_cue_design (note #283); shipped
342
- * in hud_visual_cue_impl.
342
+ * Designed as the pane-border visual cue for mu-managed panes.
343
343
  */
344
344
  declare function enableMuPaneBorders(target: string): Promise<void>;
345
345
  /**
@@ -363,18 +363,6 @@ declare function getPaneTitle(paneId: string): Promise<string | undefined>;
363
363
  * protocol's zero-config identity step.
364
364
  */
365
365
  declare function currentPaneTitle(): Promise<string | undefined>;
366
- /**
367
- * Read the *current* pane's interior size (`pane_width` x `pane_height`)
368
- * via $TMUX_PANE. Returns undefined when not inside tmux or when the
369
- * tmux call fails. Used by `mu hud` to size its tables when stdout
370
- * isn't a TTY (e.g. when running under `watch -n 5 mu hud -w X` or
371
- * `tmux display-popup -E 'mu hud -w X'`, both of which strip TTY-ness
372
- * but still run inside a tmux pane whose dimensions matter).
373
- */
374
- declare function currentPaneSize(): Promise<{
375
- width: number;
376
- height: number;
377
- } | undefined>;
378
366
  /**
379
367
  * Extract the agent-name token from a (possibly composed) pane title.
380
368
  * mu's composeAgentTitle renders titles as `name · <glyph> · task_id`,
@@ -445,7 +433,6 @@ type tmux$1_TmuxWindow = TmuxWindow;
445
433
  declare const tmux$1_assertValidPaneId: typeof assertValidPaneId;
446
434
  declare const tmux$1_capturePane: typeof capturePane;
447
435
  declare const tmux$1_currentAgentName: typeof currentAgentName;
448
- declare const tmux$1_currentPaneSize: typeof currentPaneSize;
449
436
  declare const tmux$1_currentPaneTitle: typeof currentPaneTitle;
450
437
  declare const tmux$1_defaultSendDelayMs: typeof defaultSendDelayMs;
451
438
  declare const tmux$1_enableMuPaneBorders: typeof enableMuPaneBorders;
@@ -478,7 +465,7 @@ declare const tmux$1_sleep: typeof sleep;
478
465
  declare const tmux$1_splitWindow: typeof splitWindow;
479
466
  declare const tmux$1_tmux: typeof tmux;
480
467
  declare namespace tmux$1 {
481
- 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_paneTTY as paneTTY, 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
+ 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_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_paneTTY as paneTTY, 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 };
482
469
  }
483
470
 
484
471
  /**
@@ -495,9 +482,9 @@ declare namespace tmux$1 {
495
482
  * pane title — desired side-effects of a refresh) and
496
483
  * orphan surface. Does NOT prune (so a dead pane's
497
484
  * row stays visible until a real `mu agent list`) and
498
- * does NOT reap. Used by `mu state`, `mu hud`, bare
499
- * `mu`, and `mu agent attach` — the verbs an operator
500
- * polls to answer "is worker-X busy or idle right
485
+ * does NOT reap. Used by `mu state` and
486
+ * `mu agent attach` — the verbs an operator polls to
487
+ * answer "is worker-X busy or idle right
501
488
  * now?". Status detection skips placeholder agents
502
489
  * whose pane id starts with `%pending-` (mid-spawn,
503
490
  * no usable scrollback yet).
@@ -513,7 +500,7 @@ declare namespace tmux$1 {
513
500
  *
514
501
  * Surfaced live by bug_pane_title_glyph_stuck_at_needs_input: the
515
502
  * old `dryRun: boolean` flag conflated "don't prune" with "don't
516
- * detect status", so `mu state` / `mu hud` showed stale status
503
+ * detect status", so state-card pollers showed stale status
517
504
  * indefinitely. Splitting prune-suppression from status-suppression
518
505
  * is the fix.
519
506
  */
@@ -757,8 +744,20 @@ interface CommitSummary {
757
744
  subject: string;
758
745
  /** Remainder of the commit message (may be empty). */
759
746
  body: string;
747
+ /** Author display name, when the backend exposes one. */
748
+ author: string;
760
749
  /** ISO-8601 author / commit timestamp. */
761
750
  authorDate: string;
751
+ /** Compact relative author time (e.g. "3m", "2d"). */
752
+ relTime: string;
753
+ }
754
+ interface ShowCommitResult {
755
+ /** Captured VCS show output (possibly truncated). Empty string on error. */
756
+ text: string;
757
+ /** True when stdout exceeded SHOW_COMMIT_MAX_CHARS and was clipped. */
758
+ truncated: boolean;
759
+ /** Human-readable error message; omitted on success. */
760
+ error?: string;
762
761
  }
763
762
  interface CreateWorkspaceOptions$1 {
764
763
  /** The repository being branched from. Absolute path. */
@@ -784,7 +783,7 @@ interface FreeWorkspaceOptions$1 {
784
783
  * needs an explicit commit on the worktree, sl needs `sl commit`,
785
784
  * none has nothing to commit. If pending changes exist and `commit`
786
785
  * is false, the on-disk directory still gets removed and changes are
787
- * lost \u2014 the verb prints a clear warning. */
786
+ * lost the verb prints a clear warning. */
788
787
  commit: boolean;
789
788
  }
790
789
  interface FreeWorkspaceResult$1 {
@@ -885,6 +884,13 @@ interface VcsBackend {
885
884
  * on backend command failure (unknown ref, missing repo).
886
885
  */
887
886
  commitsSinceBase(workspacePath: string, baseRef: string): Promise<CommitSummary[]>;
887
+ /** Last N commits on the project root, newest-first. Used by the
888
+ * TUI Commits card / popup. Unlike commitsSinceBase, this is NOT
889
+ * a per-workspace since-fork query. */
890
+ recentCommits(projectRoot: string, limit: number): Promise<CommitSummary[]>;
891
+ /** Show one commit / change from the project root, capped for TUI
892
+ * rendering. Backend-specific equivalent of `git show <sha>`. */
893
+ showCommit(projectRoot: string, sha: string): Promise<ShowCommitResult>;
888
894
  /**
889
895
  * Return the list of dirty (uncommitted / unstaged / untracked-not-
890
896
  * ignored) paths in the workspace. Empty array = clean.
@@ -907,10 +913,15 @@ interface VcsBackend {
907
913
  */
908
914
  listDirtyFiles(workspacePath: string): Promise<string[]>;
909
915
  }
910
- declare const noneBackend: VcsBackend;
916
+
911
917
  declare const gitBackend: VcsBackend;
918
+
912
919
  declare const jjBackend: VcsBackend;
920
+
921
+ declare const noneBackend: VcsBackend;
922
+
913
923
  declare const slBackend: VcsBackend;
924
+
914
925
  /** Return the backend that should handle projectRoot. Walks BACKENDS
915
926
  * in precedence order; never returns undefined because noneBackend
916
927
  * always claims. */
@@ -1403,7 +1414,7 @@ interface ListLiveAgentsOptions {
1403
1414
  * documented mutating behaviour `mu agent list` has always had).
1404
1415
  *
1405
1416
  * Read-only callers split two ways:
1406
- * - `mu hud`, `mu state`, bare `mu`, `mu agent attach` →
1417
+ * - `mu state`, `mu agent attach` →
1407
1418
  * `"status-only"`: refresh status + title (writes to DB),
1408
1419
  * skip prune + reap. The operator's primary signal
1409
1420
  * (busy/needs_input) stays fresh without a periodic poll
@@ -1435,10 +1446,10 @@ interface LiveAgentsView {
1435
1446
  /**
1436
1447
  * Return the live, reality-reconciled view of agents in a workstream.
1437
1448
  * `mu agent list` calls this with `mode: "full"` (mutating); status
1438
- * pollers (`mu hud`, `mu state`, bare `mu`, `mu agent attach`) call
1439
- * it with `mode: "status-only"` to refresh status without pruning;
1440
- * read-only diagnostic / restore paths (`mu doctor`, `mu undo`)
1441
- * call it with `mode: "report-only"` to mutate nothing at all.
1449
+ * pollers (`mu state`, `mu agent attach`) call it with
1450
+ * `mode: "status-only"` to refresh status without pruning; read-only
1451
+ * diagnostic / restore paths (`mu doctor`, `mu undo`) call it with
1452
+ * `mode: "report-only"` to mutate nothing at all.
1442
1453
  */
1443
1454
  declare function listLiveAgents(db: Db, opts: ListLiveAgentsOptions): Promise<LiveAgentsView>;
1444
1455
 
@@ -1523,14 +1534,9 @@ interface AddToArchiveResult {
1523
1534
  skippedTasks: number;
1524
1535
  /** Number of new archived_edges rows actually inserted. */
1525
1536
  addedEdges: number;
1526
- /** Number of new archived_notes rows inserted. (Notes have no
1527
- * natural unique key, so this matches the count of notes attached
1528
- * to NEW archived_tasks rows; existing rows' notes are not
1529
- * duplicated because note copy is gated on at-least-one new task
1530
- * for the (archive, source_workstream) pair.) */
1537
+ /** Number of new archived_notes rows inserted. */
1531
1538
  addedNotes: number;
1532
- /** Number of new archived_events rows inserted (one per kind='event'
1533
- * agent_logs row in the source workstream). */
1539
+ /** Number of new archived_events rows inserted. */
1534
1540
  addedEvents: number;
1535
1541
  }
1536
1542
  interface RemoveFromArchiveResult {
@@ -1543,26 +1549,25 @@ interface RemoveFromArchiveResult {
1543
1549
  /** Number of archived_events rows directly deleted. */
1544
1550
  removedEvents: number;
1545
1551
  }
1552
+
1546
1553
  /**
1547
- * Create a new archive bucket. Throws `ArchiveAlreadyExistsError` if
1548
- * the label is already in use; throws `ArchiveLabelInvalidError` for
1549
- * malformed labels.
1554
+ * Add every task in `workstream` to the archive identified by `label`.
1550
1555
  *
1551
- * The archive starts EMPTY: created_at and last_added_at both equal
1552
- * now(). Use `addToArchive(label, workstream)` to populate it.
1553
- */
1554
- declare function createArchive(db: Db, label: string, description?: string): Archive;
1555
- /**
1556
- * List every archive on this machine, summarised with per-source-
1557
- * workstream counts. Sorted by label ascending. Pure read; safe to
1558
- * call against an empty DB (returns []).
1556
+ * Idempotency invariant: re-running with the same (label, workstream)
1557
+ * pair is a no-op for tasks already present. The
1558
+ * (archive_id, source_workstream, original_local_id) UNIQUE on
1559
+ * archived_tasks is the lever; we INSERT OR IGNORE and skip notes /
1560
+ * events for the (archive, source_workstream) pair entirely when the
1561
+ * task copy added zero new rows.
1559
1562
  */
1560
- declare function listArchives(db: Db): ArchiveSummary[];
1563
+ declare function addToArchive(db: Db, label: string, workstream: string): AddToArchiveResult;
1561
1564
  /**
1562
- * Look up a single archive by label. Throws `ArchiveNotFoundError`
1563
- * on miss.
1565
+ * Remove every row contributed by `sourceWorkstream` from the named
1566
+ * archive. Other source workstreams' contributions are untouched
1567
+ * (additive accumulation invariant).
1564
1568
  */
1565
- declare function getArchive(db: Db, label: string): ArchiveSummary;
1569
+ declare function removeFromArchive(db: Db, label: string, sourceWorkstream: string): RemoveFromArchiveResult;
1570
+
1566
1571
  /**
1567
1572
  * Delete an archive and every row that references it. The FK
1568
1573
  * CASCADE chain (archives → archived_tasks → archived_edges /
@@ -1571,57 +1576,35 @@ declare function getArchive(db: Db, label: string): ArchiveSummary;
1571
1576
  *
1572
1577
  * Idempotent: throws `ArchiveNotFoundError` rather than silently
1573
1578
  * succeeding on a missing label (operator confusion safeguard).
1574
- *
1575
- * Mirror of `destroyWorkstream`'s safety story but cheaper: archives
1576
- * have no on-disk artifacts (no tmux session, no workspaces). The
1577
- * pre-delete snapshot is the operator's recovery path if they run
1578
- * this verb by mistake (handled in the CLI wrapper, Phase 2).
1579
1579
  */
1580
1580
  declare function deleteArchive(db: Db, label: string): void;
1581
+
1581
1582
  /**
1582
- * Add every task in `workstream` to the archive identified by `label`.
1583
- *
1584
- * Idempotency invariant: re-running with the same (label, workstream)
1585
- * pair is a no-op for tasks already present. The
1586
- * (archive_id, source_workstream, original_local_id) UNIQUE on
1587
- * archived_tasks is the lever; we INSERT OR IGNORE and skip notes /
1588
- * events for the (archive, source_workstream) pair entirely when the
1589
- * task copy added zero new rows. This makes addToArchive
1590
- * coarse-grained idempotent: the only way to get duplicate notes is
1591
- * to add a NEW task to the source workstream and re-run, which
1592
- * legitimately copies the new task's notes.
1593
- *
1594
- * Throws:
1595
- * - `ArchiveNotFoundError` if the label doesn't exist (call
1596
- * `createArchive` first).
1597
- * - `WorkstreamNotFoundError` if the source workstream is gone
1598
- * (you must archive BEFORE destroy).
1583
+ * Create a new archive bucket. Throws `ArchiveAlreadyExistsError` if
1584
+ * the label is already in use; throws `ArchiveLabelInvalidError` for
1585
+ * malformed labels.
1599
1586
  *
1600
- * The whole operation runs in a transaction so a partial failure
1601
- * leaves the archive untouched.
1587
+ * The archive starts EMPTY: created_at and last_added_at both equal
1588
+ * now(). Use `addToArchive(label, workstream)` to populate it.
1602
1589
  */
1603
- declare function addToArchive(db: Db, label: string, workstream: string): AddToArchiveResult;
1590
+ declare function createArchive(db: Db, label: string, description?: string): Archive;
1604
1591
  /**
1605
- * Remove every row contributed by `sourceWorkstream` from the named
1606
- * archive. Other source workstreams' contributions are untouched
1607
- * (additive accumulation invariant). Throws `ArchiveNotFoundError`
1608
- * if the label doesn't exist; returns all-zero counts (no error)
1609
- * when the source workstream never contributed to this archive.
1592
+ * List every archive on this machine, summarised with per-source-
1593
+ * workstream counts. Sorted by label ascending. Pure read; safe to
1594
+ * call against an empty DB (returns []).
1610
1595
  */
1611
- declare function removeFromArchive(db: Db, label: string, sourceWorkstream: string): RemoveFromArchiveResult;
1596
+ declare function listArchives(db: Db): ArchiveSummary[];
1597
+ /**
1598
+ * Look up a single archive by label. Throws `ArchiveNotFoundError`
1599
+ * on miss.
1600
+ */
1601
+ declare function getArchive(db: Db, label: string): ArchiveSummary;
1612
1602
  interface ListArchivedTasksOptions {
1613
1603
  /** Filter by source workstream. Omit to return every source's
1614
1604
  * contribution, sorted by (source_workstream, original_local_id). */
1615
1605
  sourceWorkstream?: string;
1616
1606
  }
1617
- /**
1618
- * List archived task rows in a single archive. Throws
1619
- * `ArchiveNotFoundError` on missing label.
1620
- *
1621
- * Default order: source_workstream ASC, then original_local_id ASC,
1622
- * so the output is deterministic and groups each workstream's
1623
- * contribution together.
1624
- */
1607
+ declare function listArchivedTasks(db: Db, label: string, opts?: ListArchivedTasksOptions): ArchivedTaskRow[];
1625
1608
  interface ArchiveSearchHit {
1626
1609
  /** Operator-facing label of the parent archive. */
1627
1610
  archiveLabel: string;
@@ -1641,44 +1624,37 @@ interface ArchiveSearchHit {
1641
1624
  matchSnippet: string;
1642
1625
  }
1643
1626
  interface SearchArchivesOptions {
1644
- /** LIKE-style needle. Wrapped in `%…%` automatically; `_` and `%`
1645
- * inside the pattern are still SQL LIKE wildcards (matches the
1646
- * `searchTasks` convention in src/tasks.ts). Empty / whitespace-
1647
- * only patterns throw — the CLI is the canonical caller and
1648
- * enforces it via UsageError before we get here, but the SDK
1649
- * guards it too so direct programmatic callers don't accidentally
1650
- * match every row. */
1627
+ /** LIKE-style needle. Wrapped in `%…%` automatically. */
1651
1628
  pattern: string;
1652
1629
  /** Restrict to one archive label; undefined = search every
1653
1630
  * archive. Throws ArchiveNotFoundError on miss. */
1654
1631
  label?: string;
1655
1632
  /** Cap on hits returned. Default 50; values below 1 fall back to
1656
- * the default. There is no `--all` escape hatch — for unbounded
1657
- * exports use `mu sql`. */
1633
+ * the default. */
1658
1634
  limit?: number;
1659
1635
  }
1660
- /**
1661
- * LIKE-search archived task titles AND archived note content. The
1662
- * pattern is bound as a SQL parameter (never concatenated): an
1663
- * archive label like `'); DROP TABLE archives; --` round-trips
1664
- * through `?` without touching the DDL surface.
1665
- *
1666
- * Behaviour:
1667
- * - One row per (archive, source_workstream, original_local_id)
1668
- * pair. When a task matches via BOTH title and note, the title
1669
- * row wins (matchKind='title'); only note matches stand on
1670
- * their own as matchKind='note'.
1671
- * - With `opts.label`, restricts to that archive. Resolves the
1672
- * label up-front via the helper; throws ArchiveNotFoundError
1673
- * on miss.
1674
- * - Results sorted by (archive label, source workstream,
1675
- * original_local_id) — the same order `mu archive show` uses,
1676
- * so a search hit lines up with the show output.
1677
- * - `limit` defaults to 50 and caps the result set. There is no
1678
- * unbounded mode (use `mu sql` for raw extracts).
1679
- */
1636
+ /** LIKE-search archived task titles AND archived note content. */
1680
1637
  declare function searchArchives(db: Db, opts: SearchArchivesOptions): ArchiveSearchHit[];
1681
- declare function listArchivedTasks(db: Db, label: string, opts?: ListArchivedTasksOptions): ArchivedTaskRow[];
1638
+
1639
+ declare class ArchiveSourceAmbiguousError extends Error implements HasNextSteps {
1640
+ readonly label: string;
1641
+ readonly sources: readonly string[];
1642
+ readonly name = "ArchiveSourceAmbiguousError";
1643
+ constructor(label: string, sources: readonly string[]);
1644
+ errorNextSteps(): NextStep[];
1645
+ }
1646
+ interface RestoreArchiveOptions {
1647
+ sourceWorkstream?: string;
1648
+ }
1649
+ interface RestoreArchiveResult {
1650
+ archiveLabel: string;
1651
+ sourceWorkstream: string;
1652
+ workstreamName: string;
1653
+ restoredTasks: number;
1654
+ restoredEdges: number;
1655
+ restoredNotes: number;
1656
+ }
1657
+ declare function restoreArchive(db: Db, label: string, asWorkstream: string, opts?: RestoreArchiveOptions): RestoreArchiveResult;
1682
1658
 
1683
1659
  type TaskStatus = "OPEN" | "IN_PROGRESS" | "CLOSED" | "REJECTED" | "DEFERRED";
1684
1660
  /** Every legal task status, in canonical order (matches the schema
@@ -1702,1561 +1678,1679 @@ declare function isTaskStatus(s: string): s is TaskStatus;
1702
1678
  * doesn't leave stale lists rotting in the CLI surface. */
1703
1679
  declare const TASK_STATUS_LIST: string;
1704
1680
 
1705
- declare class TaskNotFoundError extends Error implements HasNextSteps {
1706
- readonly taskId: string;
1707
- readonly name = "TaskNotFoundError";
1708
- constructor(taskId: string);
1709
- errorNextSteps(): NextStep[];
1681
+ interface TaskRow {
1682
+ /** Per-workstream-unique TEXT name. The operator-facing identifier. */
1683
+ name: string;
1684
+ /** Foreign-name reference to the owning workstream. */
1685
+ workstreamName: string;
1686
+ title: string;
1687
+ status: TaskStatus;
1688
+ impact: number;
1689
+ effortDays: number;
1690
+ /** Foreign-name reference to the owning agent (NULL when unowned). */
1691
+ ownerName: string | null;
1692
+ createdAt: string;
1693
+ updatedAt: string;
1710
1694
  }
1711
- declare class TaskExistsError extends Error implements HasNextSteps {
1712
- readonly taskId: string;
1713
- readonly name = "TaskExistsError";
1714
- constructor(taskId: string);
1715
- errorNextSteps(): NextStep[];
1695
+ interface TaskNoteRow {
1696
+ author: string | null;
1697
+ content: string;
1698
+ createdAt: string;
1699
+ }
1700
+
1701
+ interface TaskEdges {
1702
+ /** Tasks that must close before this one can start (blockers). */
1703
+ blockers: string[];
1704
+ /** Tasks that this one blocks (dependents). */
1705
+ dependents: string[];
1706
+ }
1707
+ /** One end of an edge with the neighbour's current status attached.
1708
+ * Used by `mu task show` to group blockers/dependents into
1709
+ * "still gating" vs "satisfied" buckets without making the renderer
1710
+ * do a second round-trip to the DB per neighbour. */
1711
+ interface TaskEdgeWithStatus {
1712
+ name: string;
1713
+ status: TaskStatus;
1714
+ }
1715
+ interface TaskEdgesWithStatus {
1716
+ /** Tasks that must close before this one can start (blockers),
1717
+ * carrying each blocker's current status. */
1718
+ blockers: TaskEdgeWithStatus[];
1719
+ /** Tasks that this one blocks (dependents), carrying each
1720
+ * dependent's current status. */
1721
+ dependents: TaskEdgeWithStatus[];
1716
1722
  }
1717
1723
  /**
1718
- * Thrown when a verb is invoked with `-w/--workstream <name>` but the
1719
- * named task lives in a different workstream. Distinguishes "the user
1720
- * typo'd the workstream" from "the task doesn't exist anywhere"
1721
- * (which surfaces as `TaskNotFoundError`). Maps to exit code 4
1722
- * (conflict / wrong scope).
1724
+ * Direct (one-hop) edges for a task. For transitive prerequisites, use
1725
+ * `getPrerequisites()`; this helper is the immediate-neighbour view used
1726
+ * by `mu task show`.
1723
1727
  */
1724
- declare class TaskNotInWorkstreamError extends Error implements HasNextSteps {
1725
- readonly taskId: string;
1726
- readonly expectedWorkstream: string;
1727
- readonly actualWorkstream: string;
1728
- readonly name = "TaskNotInWorkstreamError";
1729
- constructor(taskId: string, expectedWorkstream: string, actualWorkstream: string);
1730
- errorNextSteps(): NextStep[];
1728
+ declare function getTaskEdges(db: Db, taskLocalId: string, workstream: string): TaskEdges;
1729
+ /**
1730
+ * Same one-hop edge view as `getTaskEdges`, but each neighbour is
1731
+ * returned as `{ name, status }` so callers can group / colour by
1732
+ * status without an N+1 round-trip. Used by `mu task show` to split
1733
+ * "blocked by" (still-gating) from "satisfied" (already-CLOSED)
1734
+ * blockers, and the symmetric split on the dependents side
1735
+ * (task_show_blocked_by_renders_closed). The status is the neighbour's
1736
+ * full TaskStatus, not just OPEN/CLOSED — REJECTED/DEFERRED still
1737
+ * gate downstream work, so the renderer keeps them in the
1738
+ * still-gating bucket.
1739
+ */
1740
+ declare function getTaskEdgesWithStatus(db: Db, taskLocalId: string, workstream: string): TaskEdgesWithStatus;
1741
+ /**
1742
+ * All tasks transitively reachable from `taskId` via reverse-edge
1743
+ * traversal (i.e. the set of tasks that block this one), including the
1744
+ * task itself.
1745
+ */
1746
+ declare function getPrerequisites(db: Db, taskLocalId: string, workstream: string): Set<string>;
1747
+ interface BlockEdgeResult {
1748
+ /** True iff a row was actually inserted (vs. already present). */
1749
+ added: boolean;
1731
1750
  }
1732
- declare class TaskAlreadyOwnedError extends Error implements HasNextSteps {
1733
- readonly taskId: string;
1734
- readonly currentOwner: string;
1735
- readonly name = "TaskAlreadyOwnedError";
1736
- constructor(taskId: string, currentOwner: string);
1737
- errorNextSteps(): NextStep[];
1751
+ /**
1752
+ * Add the edge `blocker → blocked` ('blocker blocks blocked').
1753
+ * Idempotent (existing edge → `added: false`). Validates:
1754
+ *
1755
+ * - both tasks exist
1756
+ * - same workstream (cross-workstream edges forbidden)
1757
+ * - no cycle (the new edge wouldn't form a path blocked → ... → blocker)
1758
+ * - blocker ≠ blocked (no self-reference)
1759
+ */
1760
+ declare function addBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): BlockEdgeResult;
1761
+ interface RemoveBlockEdgeResult {
1762
+ /** True iff a row was actually deleted (vs. no such edge). */
1763
+ removed: boolean;
1738
1764
  }
1739
1765
  /**
1740
- * Thrown by `rejectTask` / `deferTask` when the target task has
1741
- * dependents that are still OPEN or IN_PROGRESS. Rejecting or
1742
- * deferring such a task would silently strand the dependents (they'd
1743
- * remain blocked by a prereq that's never going to satisfy the edge),
1744
- * so we refuse and force an explicit decision: pass `--cascade` to
1745
- * apply the same status to every transitive dependent, drop the
1746
- * blocking edge first with `mu task unblock`, or address the
1747
- * dependents individually. Maps to exit code 4.
1766
+ * Remove the edge `blocker blocked`. Idempotent (no edge
1767
+ * `removed: false`). Does NOT validate task existence if the
1768
+ * edge is gone there's nothing to do, regardless of whether the
1769
+ * tasks are gone too.
1748
1770
  */
1749
- declare class TaskHasOpenDependentsError extends Error implements HasNextSteps {
1750
- readonly taskId: string;
1751
- readonly verb: "reject" | "defer";
1752
- readonly dependents: readonly string[];
1753
- readonly name = "TaskHasOpenDependentsError";
1754
- constructor(taskId: string, verb: "reject" | "defer", dependents: readonly string[]);
1755
- errorNextSteps(): NextStep[];
1771
+ declare function removeBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): RemoveBlockEdgeResult;
1772
+ interface ReparentTaskResult {
1773
+ /** Edges removed (i.e. all incoming `to_task = taskId` edges). */
1774
+ removedEdges: number;
1775
+ /** Edges added (after duplicate blockers are canonicalised). */
1776
+ addedEdges: number;
1756
1777
  }
1757
1778
  /**
1758
- * Thrown when `mu task claim` resolves a claimer agent name (from the
1759
- * pane title or --for) that has no matching row in the agents table.
1779
+ * Atomically replace every incoming edge of `taskId` with new ones
1780
+ * `blocker[i] taskId`. Pass an empty `blockers` array to clear all
1781
+ * incoming edges (the task becomes ready iff its status allows).
1760
1782
  *
1761
- * The FK on `tasks.owner` references `agents.name`; without this guard
1762
- * the claim attempt would fail with the unhelpful 'FOREIGN KEY constraint
1763
- * failed' from SQLite. This typed error gives the user actionable next
1764
- * steps (run `mu agent adopt <pane-id>` to register, or use --for to pick a
1765
- * different agent).
1783
+ * Validates ALL new blockers up-front (existence + same workstream +
1784
+ * cycle check); if any fails, no DELETE happens — the call is fully
1785
+ * atomic via a single transaction.
1766
1786
  *
1767
- * Maps to exit code 4 (conflict) via the cli.ts handler.
1787
+ * Cycle reasoning: removing the existing incoming edges to `taskId`
1788
+ * doesn't change `taskId`'s OUTGOING reachability, so
1789
+ * `wouldCreateCycle(db, blocker, taskId)` evaluated against the
1790
+ * pre-state gives the right answer for each new edge.
1768
1791
  */
1769
- declare class ClaimerNotRegisteredError extends Error implements HasNextSteps {
1770
- readonly agentName: string;
1771
- readonly paneId: string | null;
1772
- readonly name = "ClaimerNotRegisteredError";
1773
- constructor(agentName: string, paneId: string | null);
1792
+ declare function reparentTask(db: Db, taskLocalId: string, blockers: readonly string[], scope: {
1793
+ workstream: string;
1794
+ }): ReparentTaskResult;
1795
+
1796
+ interface AddTaskOptions {
1797
+ localId: string;
1798
+ workstream: string;
1799
+ title: string;
1800
+ /** 1..100; enforced by schema CHECK. */
1801
+ impact: number;
1802
+ /** > 0; enforced by schema CHECK. */
1803
+ effortDays: number;
1774
1804
  /**
1775
- * Three actionable resolutions in expected-frequency order:
1776
- * 1. --self : orchestrator pattern (working directly)
1777
- * 2. --for : dispatcher pattern (assigning to a worker)
1778
- * 3. mu agent adopt: registration pattern (promote pane to worker)
1805
+ * Tasks that block this one. Edges inserted as `blocker -> newTask`.
1806
+ * Each blocker must already exist AND share this task's workstream
1807
+ * (cross-workstream edges are forbidden); cycle check guards each
1808
+ * edge. The CLI surfaces this as `--blocked-by`; the SDK key matches.
1779
1809
  */
1780
- errorNextSteps(): NextStep[];
1781
- }
1782
- declare class CycleError extends Error implements HasNextSteps {
1783
- readonly from: string;
1784
- readonly to: string;
1785
- readonly name = "CycleError";
1786
- constructor(from: string, to: string);
1787
- errorNextSteps(): NextStep[];
1788
- }
1789
- declare class CrossWorkstreamEdgeError extends Error implements HasNextSteps {
1790
- readonly blocker: string;
1791
- readonly blockerWorkstream: string;
1792
- readonly dependent: string;
1793
- readonly dependentWorkstream: string;
1794
- readonly name = "CrossWorkstreamEdgeError";
1795
- constructor(blocker: string, blockerWorkstream: string, dependent: string, dependentWorkstream: string);
1796
- errorNextSteps(): NextStep[];
1797
- }
1798
-
1799
- declare function setWaitSleepForTests(impl: ((ms: number) => Promise<void>) | undefined): (ms: number) => Promise<void>;
1800
- /** Test seam: swap the stderr writer used by the stuck-task warning so
1801
- * unit tests can capture warnings without spying on process.stderr. */
1802
- declare function setWaitStuckWarnForTests(impl: ((msg: string) => void) | undefined): (msg: string) => void;
1803
- /** Total number of polls performed across all `waitForTasks` calls in this
1804
- * process. Tests typically reset before exercising and read after. */
1805
- declare function getWaitPollCount(): number;
1806
- declare function resetWaitPollCount(): void;
1807
- /** A single task ref the wait verb is watching. Cross-workstream
1808
- * waits arrive as a heterogeneous list of (workstream, name) pairs;
1809
- * the legacy single-workstream call passes the same workstream on
1810
- * every ref. task_wait_cross_workstream. */
1811
- interface TaskWaitRef {
1812
- /** The workstream the task lives in. Each ref carries its own so
1813
- * the SDK doesn't need a single "the workstream" — cross-ws waits
1814
- * pass refs from multiple workstreams in one call. */
1815
- workstreamName: string;
1816
- /** The task's per-workstream-unique local id. */
1817
- name: string;
1810
+ blockedBy?: string[];
1818
1811
  }
1819
- interface TaskWaitOptions {
1820
- /** Target status. Default 'CLOSED'. */
1821
- status?: TaskStatus;
1822
- /** When true, succeed as soon as ONE listed task reaches the target.
1823
- * Default false: every listed task must reach the target. */
1824
- any?: boolean;
1825
- /** Maximum time to wait, in milliseconds. Default 600_000 (10 min).
1826
- * Pass 0 to wait forever. */
1827
- timeoutMs?: number;
1828
- /** Polling interval. Default 1000ms; overridable for tests. */
1829
- pollMs?: number;
1830
- /** Workstream context applied to bare-string ids. Required when the
1831
- * caller passes `string[]`; ignored when the caller passes
1832
- * `TaskWaitRef[]` (each ref carries its own ws). The legacy
1833
- * single-ws SDK call site keeps its today's shape; the cross-ws
1834
- * callers (CLI verb) pass `TaskWaitRef[]` and omit `workstream`.
1835
- * task_wait_cross_workstream. */
1836
- workstream?: string;
1837
- /** Emit a yellow STUCK warning to stderr (once per task per wait call)
1838
- * when an IN_PROGRESS task's owner has been in `needs_input` for at
1839
- * least this many milliseconds since the agent row's last update.
1840
- * Default 300_000 (5 min). Pass 0 to disable.
1841
- *
1842
- * Surfaced by agent_close_discipline_gap in mufeedback: workers
1843
- * occasionally finish + commit + go idle without running
1844
- * `mu task close <id>`, leaving wait blocked indefinitely. The
1845
- * warning is observation-only — wait keeps polling so the operator
1846
- * (or a wrapping policy) decides whether to force-close, re-prompt,
1847
- * or escalate. */
1848
- stuckAfterMs?: number;
1849
- /** What to do when the `--stuck-after` predicate fires on a watched
1850
- * task. `'warn'` (default) = today's behaviour: yellow STUCK line
1851
- * to stderr (deduped per task per wait call) + corroborating
1852
- * `kind='event'` agent_logs row; wait keeps polling. `'exit'` =
1853
- * same emit + persist, but THEN throw `StallDetectedDuringWaitError`
1854
- * so the CLI wrapper exits 7 (STALL_DETECTED). The exit-action is
1855
- * the unattended-orchestrator escape: a wrapping policy can branch
1856
- * on 7 (idle, ambiguous — operator decides poke vs release) vs 6
1857
- * (dead pane, unambiguous — re-dispatch).
1858
- *
1859
- * Carve-out (lives at the call site, not here): the CLI passes
1860
- * `'exit'` only when the wait target is CLOSED — mirrors exit-6's
1861
- * reaper-flip suppression. With `--status OPEN` the worker reaching
1862
- * needs_input might BE the success path. See
1863
- * task_wait_stall_action_flag. */
1864
- onStall?: "warn" | "exit";
1865
- /** Optional async hook run BEFORE every snapshot (initial + each
1866
- * poll iteration). The CLI uses this to reconcile the workstream
1867
- * each tick (reaper flips IN_PROGRESS → OPEN for dead-pane
1868
- * workers) and to throw a typed error when a reaper-flip on a
1869
- * watched task should abandon the wait — see
1870
- * task_wait_reconcile_dead_panes. Throwing from `beforePoll`
1871
- * propagates out of `waitForTasks` unchanged.
1872
- *
1873
- * Kept as a generic seam (not a `--reconcile`-shaped option) so
1874
- * the SDK module stays free of tmux/reconcile imports — that
1875
- * layering belongs above the SDK in the CLI wrapper. */
1876
- beforePoll?: () => Promise<void>;
1812
+ /**
1813
+ * Atomically create a task and (optionally) its incoming blocked-by
1814
+ * edges.
1815
+ *
1816
+ * The task insert + every edge insert + cycle check happen inside one
1817
+ * SQLite transaction. If any blocker is missing or any edge would
1818
+ * create a cycle, the entire add rolls back.
1819
+ *
1820
+ * Cycle check for `addTask` is structurally trivial (a fresh task has
1821
+ * no outgoing edges, so `to -> ... -> from` is impossible). It's still
1822
+ * called here so the same primitive is exercised by tests.
1823
+ */
1824
+ declare function addTask(db: Db, opts: AddTaskOptions): TaskRow;
1825
+ interface AddNoteOptions {
1826
+ /** Free-form author label. Convention: agent name, "user", or "orchestrator". */
1827
+ author?: string;
1828
+ /** Workstream context (operator-facing name). v5: tasks.local_id is
1829
+ * per-workstream unique, so this is required to disambiguate. */
1830
+ workstream: string;
1877
1831
  }
1878
- interface TaskWaitTaskState {
1879
- /** The workstream this task lives in. Cross-workstream waits
1880
- * return a mixed list; the workstream is part of identity.
1881
- * task_wait_cross_workstream. */
1882
- workstreamName: string;
1883
- /** The task's per-workstream-unique name. */
1884
- name: string;
1885
- /** Current status (at the moment we exit). */
1886
- status: TaskStatus;
1887
- /** Owner at exit time (NULL when unowned, after release, or after
1888
- * the reaper flipped IN_PROGRESS OPEN due to a dead pane). */
1889
- owner: string | null;
1890
- /** True when this task's status equals the target. */
1891
- reachedTarget: boolean;
1892
- /** True when the task is IN_PROGRESS, owned by a registered agent
1893
- * whose detected status is `needs_input` for >= `stuckAfterMs`.
1894
- * Surfaces the agent_close_discipline_gap pattern: worker finished +
1895
- * committed but skipped `mu task close <id>`. Backwards-compatible
1896
- * signal callers ignoring it see no behaviour change. */
1897
- stuck: boolean;
1832
+ declare function addNote(db: Db, taskLocalId: string, content: string, opts: AddNoteOptions): {
1833
+ author: string | null;
1834
+ content: string;
1835
+ createdAt: string;
1836
+ };
1837
+ interface DeleteTaskResult {
1838
+ /** True iff the row existed and was deleted. False on a dry-run
1839
+ * (preview) AND on the idempotent missing-row case. */
1840
+ deleted: boolean;
1841
+ /** Number of `task_edges` rows cascaded out (informational). On a
1842
+ * dry-run, this is the would-be count. */
1843
+ deletedEdges: number;
1844
+ /** Number of `task_notes` rows cascaded out (informational). On a
1845
+ * dry-run, this is the would-be count. */
1846
+ deletedNotes: number;
1847
+ /** True iff this was a dry-run (`opts.dryRun: true`). On a
1848
+ * dry-run `deleted` is false and the counts are the would-be
1849
+ * counts; the DB is unchanged. Always false on a commit / on a
1850
+ * missing-row idempotent no-op. */
1851
+ dryRun: boolean;
1852
+ /** True iff a matching task row was found at the time of the
1853
+ * call. Discriminator for the CLI: a dry-run that found nothing
1854
+ * (`present: false`) renders differently from a dry-run that
1855
+ * found an existing task with zero edges and zero notes
1856
+ * (`present: true, deletedEdges: 0, deletedNotes: 0`). */
1857
+ present: boolean;
1898
1858
  }
1899
- interface TaskWaitResult {
1900
- /** Per-task state at exit time. Same length and order as the input
1901
- * list. The caller derives all-reached / any-reached / elapsed
1902
- * from this list (count `r.reachedTarget`) and from its own
1903
- * startedAt clock keeping the SDK return minimal. */
1904
- refs: TaskWaitTaskState[];
1905
- /** True when we exited because of the timeout, not because the wait
1906
- * condition was met. Refs that did reach the target are still
1907
- * reflected in `refs[i].reachedTarget` on partial-progress timeout. */
1908
- timedOut: boolean;
1859
+ interface DeleteTaskOptions {
1860
+ /** When true, return the cascade preview (would-be edge / note
1861
+ * counts) without mutating and without snapshotting. The CLI uses
1862
+ * this to power the bare `mu task delete <id>` two-phase pattern
1863
+ * (mirrors `mu workstream destroy` / `mu archive delete` /
1864
+ * `mu snapshot prune`). Surfaced by feedback ws task
1865
+ * fb_task_delete_no_yes (impact=30): a dogfood report typed
1866
+ * `mu task delete X --yes` (mirroring workstream destroy) and got
1867
+ * 'unknown option --yes' the verb took no confirmation flag at
1868
+ * all. Two failed deletes left long-named tasks lingering. */
1869
+ dryRun?: boolean;
1909
1870
  }
1910
1871
  /**
1911
- * Block until a set of tasks reaches `opts.status` (default CLOSED).
1912
- * Returns a result describing the final state the caller decides
1913
- * whether to treat partial-progress timeouts as success or failure
1914
- * (the CLI maps a clean exit to 0, a timeout to 5).
1872
+ * Delete a task. FK CASCADE on `task_edges` (from + to) and
1873
+ * `task_notes` cleans the joined rows automatically. Idempotent on
1874
+ * a missing task (returns `deleted: false`).
1915
1875
  *
1916
- * Pre-flight: every task in `localIds` MUST exist; missing ones throw
1917
- * TaskNotFoundError before any waiting begins. This is loud-fail by
1918
- * design — a typo'd id silently waiting forever is the worst-case UX.
1876
+ * Pre-counts the cascade victims for reporting because SQLite's
1877
+ * `changes()` only reports rows directly affected by the DELETE.
1878
+ *
1879
+ * With `opts.dryRun: true`, returns the would-be counts without
1880
+ * touching the DB and without taking a snapshot (no mutation = no
1881
+ * snapshot — same reasoning that gates the closeTask snap on the
1882
+ * idempotent no-op path). The CLI bare `mu task delete <id>` form
1883
+ * uses this; `--yes` calls through with `dryRun: false`.
1919
1884
  */
1920
- declare function waitForTasks(db: Db, input: readonly TaskWaitRef[] | readonly string[], opts: TaskWaitOptions): Promise<TaskWaitResult>;
1921
-
1922
- interface SetStatusResult {
1923
- /** Status before the call. */
1924
- previousStatus: TaskStatus;
1925
- /** Status after the call (== requested status). */
1926
- status: TaskStatus;
1927
- /** True iff the row actually changed. False on idempotent no-op. */
1928
- changed: boolean;
1885
+ declare function deleteTask(db: Db, localId: string, workstream: string, opts?: DeleteTaskOptions): DeleteTaskResult;
1886
+ interface UpdateTaskOptions {
1887
+ title?: string;
1888
+ /** 1..100; enforced by schema CHECK. */
1889
+ impact?: number;
1890
+ /** > 0; enforced by schema CHECK. */
1891
+ effortDays?: number;
1929
1892
  }
1930
- /**
1931
- * Optional evidence string carried on lifecycle verbs (close / open /
1932
- * claim / release). Lands in the auto-emitted `kind='event'` payload
1933
- * verbatim, prefixed with `evidence=`. The first inch of distinguishing
1934
- * "observed" from "claimed" state per an internal critique: the
1935
- * verb still trusts the caller (it's not a verifier), but the audit
1936
- * trail records what the caller said it relied on.
1937
- */
1938
- interface EvidenceOption {
1939
- evidence?: string;
1893
+ interface UpdateTaskResult {
1894
+ /** True iff at least one field actually changed. */
1895
+ updated: boolean;
1896
+ /** The fields whose values differ post-update (in `UpdateTaskOptions`'s
1897
+ * camelCase shape). Empty when `updated: false`. */
1898
+ changedFields: string[];
1940
1899
  }
1941
1900
  /**
1942
- * Flip a task's status to any of OPEN / IN_PROGRESS / CLOSED.
1943
- * Idempotent: setting a task to its current status is a no-op (returns
1944
- * `changed: false`) rather than throwing. Owner is unchanged.
1901
+ * Update scalar fields on a task. Each option is independently optional;
1902
+ * passing none is a typed no-op (returns `updated: false, changedFields: []`).
1903
+ * Fields whose new value equals the current value are skipped (no row change).
1904
+ *
1905
+ * NOT for status (use `closeTask` / `openTask` / `setTaskStatus`), owner
1906
+ * (use `claimTask` / `releaseTask`), local_id (rename is deferred), or
1907
+ * workstream (cross-workstream moves are deferred).
1945
1908
  */
1946
- declare function setTaskStatus(db: Db, localId: string, status: TaskStatus, opts: EvidenceOption & {
1909
+ interface UpdateTaskScopeOption {
1947
1910
  workstream: string;
1948
- }): SetStatusResult;
1949
- /** Result of `closeTask` when called with `ifReady: true` and the
1950
- * task is NOT yet ready to close (still has at least one OPEN /
1951
- * IN_PROGRESS blocker). Distinguished from a regular `SetStatusResult`
1952
- * by the literal `skipped` field; the CLI keys on it to switch
1953
- * between the "closed" and "waiting" rendering paths.
1954
- *
1955
- * Surfaced in `fb_umbrella_no_auto_close` (impact=60): a wave umbrella
1956
- * with N blockers stayed OPEN after every blocker reached a terminal
1957
- * status. `--if-ready` is the cheap fix: bare `mu task close` is
1958
- * unchanged (closes regardless), `--if-ready` is a no-op unless every
1959
- * blocker is in a terminal status (CLOSED / REJECTED / DEFERRED).
1960
- * Reject and defer satisfy the predicate too because `--if-ready`'s
1961
- * job is to fire when the umbrella has nothing left to wait for, and
1962
- * a rejected/deferred blocker is no longer being waited on. */
1963
- interface CloseSkippedResult {
1964
- /** Always 'not_ready' when set; future cause-codes can extend this
1965
- * without reshaping the JSON payload (the literal-union narrows
1966
- * safely in the CLI rendering path). */
1967
- skipped: "not_ready";
1968
- /** Status before the call (always the current status, no change). */
1969
- previousStatus: TaskStatus;
1970
- /** Status after the call (== previousStatus, since we no-op). */
1971
- status: TaskStatus;
1972
- /** Always false on a skip (no row mutated). */
1973
- changed: false;
1974
- /** Local ids of every blocker still in OPEN or IN_PROGRESS, sorted
1975
- * alphabetically for deterministic rendering. Empty list is
1976
- * impossible on this branch — the no-op only fires when ≥1
1977
- * blocker is non-terminal. */
1978
- blockingIds: string[];
1979
- }
1980
- interface CloseTaskOptions extends EvidenceOption {
1981
- workstream: string;
1982
- /** When true, no-op the close unless every blocker is in a terminal
1983
- * status (CLOSED / REJECTED / DEFERRED). Returns a
1984
- * `CloseSkippedResult` carrying the still-blocking ids; the CLI
1985
- * renders the skip with a Next: hint pointing at `mu task wait`.
1986
- * When false / omitted, behaves as bare `closeTask` (closes
1987
- * regardless of blocker status). */
1988
- ifReady?: boolean;
1989
- /** Optional actor identity attributed to the synthetic `CLOSE: …`
1990
- * note auto-inserted when `evidence` is non-empty (see closeTask
1991
- * body). The CLI resolves this via `resolveActorIdentity()` so the
1992
- * note carries the closing worker's name; SDK callers (tests,
1993
- * internal use) may omit it (the note then carries no author, same
1994
- * as a bare `addNote` without `--author`). Surfaced in mufeedback
1995
- * task_close_evidence_does_not_append_the. */
1996
- author?: string;
1997
1911
  }
1998
- /** Convenience: setTaskStatus(db, id, "CLOSED"). Accepts evidence.
1999
- * Pre-snapshots the DB (snap_design §CAPTURE STRATEGY > WHEN). Skipped
2000
- * for the idempotent no-op (already CLOSED) so we don't accumulate
2001
- * empty-delta snapshots on retry loops.
1912
+ declare function updateTask(db: Db, localId: string, opts: UpdateTaskOptions, scope: UpdateTaskScopeOption): UpdateTaskResult;
1913
+
1914
+ declare function isValidTaskId(id: string): boolean;
1915
+ /**
1916
+ * Lowercase title; collapse non-alnum runs into single `_`; trim
1917
+ * leading/trailing `_`; prefix `t_` if the result starts with a digit
1918
+ * (schema requires first char letter); apply the soft cap with
1919
+ * word-boundary trim (cut at the last `_` at-or-before SLUG_SOFT_CAP
1920
+ * when one exists, else hard-truncate). Mirrors `tg`'s `id_from_title`
1921
+ * but adds the soft cap.
2002
1922
  *
2003
- * With `ifReady: true`, returns a `CloseSkippedResult` (no mutation,
2004
- * no snapshot) when any blocker is still OPEN / IN_PROGRESS. Used by
2005
- * `mu task close --if-ready` so an orchestrator can fire-and-forget
2006
- * the umbrella close after every blocker resolves without first
2007
- * re-querying the graph. */
2008
- declare function closeTask(db: Db, localId: string, opts: CloseTaskOptions): SetStatusResult | CloseSkippedResult;
2009
- /** Convenience: setTaskStatus(db, id, "OPEN"). Owner intentionally NOT
2010
- * cleared use `releaseTask` for that. Accepts evidence. */
2011
- declare function openTask(db: Db, localId: string, opts: EvidenceOption & {
2012
- workstream: string;
2013
- }): SetStatusResult;
2014
- interface RejectDeferOptions extends EvidenceOption {
2015
- /** Workstream context for the root task. All internal task lookups
2016
- * (including the dependent walk) scope to this workstream. */
2017
- workstream: string;
2018
- /** If true, walk the transitive dependent closure and (with `yes`)
2019
- * apply the same status to every dependent, atomically. Without
2020
- * `yes`, runs as a dry-run: returns the list of tasks that WOULD
2021
- * be swept (changedIds) with `dryRun: true` and changes nothing.
2022
- * Logs one event per task (via setTaskStatus) on commit. */
2023
- cascade?: boolean;
2024
- /** Required to actually commit a `cascade` operation. Without it,
2025
- * cascade is dry-run only prints the affected dependents so the
2026
- * caller can verify before sweeping. Mirrors `mu workstream destroy
2027
- * --yes`. Surfaced in mufeedback bug_cascade_reject_too_aggressive
2028
- * when an accidentally-cascaded reject swept hud_dogfood (which had
2029
- * independent merit and needed reopening). */
2030
- yes?: boolean;
1923
+ * Throws if `title` yields an empty slug after stripping.
1924
+ */
1925
+ declare function slugifyTitle(title: string): string;
1926
+ /**
1927
+ * Result of `slugifyTitleVerbose`: the slug plus enough metadata for
1928
+ * the CLI to decide whether to warn the user that meaning was lost.
1929
+ *
1930
+ * slug the same string `slugifyTitle` returns.
1931
+ * strippedLength length of the post-strip pre-cap slug. When this
1932
+ * exceeds the SLUG_SOFT_CAP the verbose form had to
1933
+ * cut at a word boundary (or hard-truncate); the
1934
+ * cut clauses are gone with no in-band signal.
1935
+ * originalSlug — what the slug WOULD have been without the
1936
+ * SLUG_SOFT_CAP cut: full stripped slug with the
1937
+ * same `t_` digit-prefix correction and the same
1938
+ * SLUG_HARD_CAP ceiling, but no word-boundary
1939
+ * truncation. Equal to `slug` when nothing was
1940
+ * cut. The CLI surfaces this in `mu task add
1941
+ * --json` so scripted callers can detect the
1942
+ * truncation without grepping stderr.
1943
+ * truncated — true iff `slug.length < strippedLength` AFTER the
1944
+ * `t_` digit-prefix correction, i.e. real bytes were
1945
+ * dropped. False for any title that fits under the
1946
+ * soft cap or whose only diff vs the stripped slug
1947
+ * is the `t_` prefix.
1948
+ *
1949
+ * The CLI's `mu task add` uses `truncated` to print a one-line stderr
1950
+ * hint pointing at the `<id>` positional override and (under --json)
1951
+ * to surface `originalSlug` alongside `truncated:true`
1952
+ * (slugifytitle_silently_drops_clauses; task_add_slugify_silently_truncates_ids).
1953
+ */
1954
+ interface SlugifyResult {
1955
+ slug: string;
1956
+ strippedLength: number;
1957
+ originalSlug: string;
1958
+ truncated: boolean;
2031
1959
  }
2032
- interface RejectDeferResult {
2033
- /** Tasks that actually changed status, in cascade order (root first). */
2034
- changedIds: string[];
2035
- /** The status now stamped on every changedId. */
2036
- status: TaskStatus;
2037
- /** True iff anything changed. False on a clean idempotent no-op
2038
- * (root task already in target status, no dependents). */
2039
- changed: boolean;
2040
- /** True iff this was a `cascade` dry-run (cascade requested without
2041
- * `yes`). In that case `changedIds` lists tasks that WOULD be
2042
- * swept; the DB is unchanged. */
2043
- dryRun: boolean;
2044
- /** Tasks that would be touched by a cascade. Same as `changedIds`
2045
- * on a dry-run; populated even on a commit so the caller can
2046
- * report what was swept. */
2047
- affectedIds: string[];
1960
+ /**
1961
+ * Verbose sibling of `slugifyTitle`: returns the slug AND a
1962
+ * `truncated` flag so the CLI can hint to the user when the soft cap
1963
+ * dropped clauses (the meaning-shift hazard documented in
1964
+ * slugifytitle_silently_drops_clauses).
1965
+ *
1966
+ * Algorithm is byte-for-byte identical to `slugifyTitle`; this just
1967
+ * surfaces the metadata that the plain form throws away.
1968
+ */
1969
+ declare function slugifyTitleVerbose(title: string): SlugifyResult;
1970
+ /**
1971
+ * Generate a unique task id from a title. v5: tasks.local_id is
1972
+ * per-workstream unique, so the collision check scopes to one
1973
+ * workstream. On collision, appends `_2`, `_3`, until unique.
1974
+ */
1975
+ declare function idFromTitle(db: Db, workstream: string, title: string): string;
1976
+ /**
1977
+ * Result of `idFromTitleVerbose`: the unique-in-workstream id plus the
1978
+ * truncated flag from the underlying slugify pass. Used by `mu task
1979
+ * add` to decide whether to surface the stderr hint about lost clauses
1980
+ * (slugifytitle_silently_drops_clauses) and to surface the un-truncated
1981
+ * slug in `--json` (task_add_slugify_silently_truncates_ids).
1982
+ *
1983
+ * id — the unique-in-workstream task id.
1984
+ * truncated — true iff the underlying slugify pass cut real
1985
+ * characters (collision-suffixing does NOT flip
1986
+ * this).
1987
+ * originalSlug — what the slug would have been without the
1988
+ * SLUG_SOFT_CAP cut. Equal to `id` when nothing was
1989
+ * cut AND no collision suffix was appended; for
1990
+ * the truncation-detection use case the only thing
1991
+ * the CLI cares about is the lossy-vs-not
1992
+ * comparison surfaced via `truncated`.
1993
+ */
1994
+ interface IdFromTitleResult {
1995
+ id: string;
1996
+ truncated: boolean;
1997
+ originalSlug: string;
2048
1998
  }
2049
- /** Reject a task: terminal 'won't do' (out of scope, duplicate, wontfix).
2050
- * Refuses if dependents are open unless `--cascade`.
2051
- * Pre-snapshots once at the verb level so a cascade onto N children
2052
- * produces a single snapshot, not N. Skipped for the idempotent no-op. */
2053
- declare function rejectTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
2054
- /** Defer a task: parked, may revisit. Same dependent-stranding semantics
2055
- * as reject (DEFERRED also doesn't satisfy a `--blocked-by` edge).
2056
- * Pre-snapshots once at the verb level. Skipped for the idempotent no-op. */
2057
- declare function deferTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
1999
+ /**
2000
+ * Verbose sibling of `idFromTitle`: returns the unique id, the
2001
+ * `truncated` flag from the slugify pass, and the un-truncated
2002
+ * `originalSlug` for `--json` consumers. Collision-suffixing (`_2`,
2003
+ * `_3`, …) does not flip `truncated` — the underlying slug's lossiness
2004
+ * is what the CLI hint cares about.
2005
+ */
2006
+ declare function idFromTitleVerbose(db: Db, workstream: string, title: string): IdFromTitleResult;
2058
2007
 
2059
- interface ReleaseResult {
2060
- /** The previous owner (null if the task was already unowned). */
2061
- previousOwnerName: string | null;
2062
- /** Status before the release. */
2063
- previousStatus: TaskStatus;
2064
- /** Status after the release. */
2065
- status: TaskStatus;
2066
- /** True iff owner OR status actually changed. */
2067
- changed: boolean;
2008
+ declare function getTask(db: Db, localId: string, workstream: string): TaskRow | undefined;
2009
+ /**
2010
+ * List tasks. With no `workstream` arg returns every row — used by `mu sql`
2011
+ * and by tests; CLI surfaces always pass a workstream so users only see
2012
+ * their own.
2013
+ */
2014
+ interface ListTasksOptions {
2015
+ /** Filter to one or more lifecycle statuses. Omitted = all statuses. */
2016
+ status?: TaskStatus | readonly TaskStatus[];
2068
2017
  }
2069
- interface ReleaseTaskOptions extends EvidenceOption {
2070
- /** Workstream context for the task (v5: tasks.local_id is
2071
- * per-workstream unique). */
2072
- workstream: string;
2073
- /** Force `status = OPEN` regardless of the current status. Without
2074
- * this flag, `IN_PROGRESS` is also flipped to `OPEN` automatically
2075
- * (so a released task isn't left structurally stranded with
2076
- * `owner=NULL, status=IN_PROGRESS`); CLOSED / REJECTED / DEFERRED
2077
- * are preserved. `--reopen` is the override for the rarer "un-
2078
- * close and hand back to the pool" workflow. */
2079
- reopen?: boolean;
2018
+ declare function listTasks(db: Db, workstream?: string, opts?: ListTasksOptions): TaskRow[];
2019
+ /** Options for listReady. The optional `statuses` filter composes
2020
+ * on top of the `ready` view (which itself constrains to
2021
+ * `status='OPEN'`); passing only OPEN is identical to today's no-
2022
+ * filter shape, passing only non-OPEN values returns []. Exists so
2023
+ * `mu task next --status` can mirror the multi-status flag shape
2024
+ * shipped on `mu task list` (task_list_multi_status_union). */
2025
+ interface ListReadyOptions {
2026
+ status?: TaskStatus | readonly TaskStatus[];
2080
2027
  }
2081
- /**
2082
- * Release a task: clear `tasks.owner`.
2028
+ declare function listReady(db: Db, workstream: string, opts?: ListReadyOptions): TaskRow[];
2029
+ declare function listBlocked(db: Db, workstream: string): TaskRow[];
2030
+ declare function listGoals(db: Db, workstream: string): TaskRow[];
2031
+ /** All IN_PROGRESS tasks in a workstream, most-recently-touched first.
2032
+ * Used by `mu state` to populate its in-progress slice; exposed as a
2033
+ * named SDK helper so CLI renderers don't re-derive the row-shape
2034
+ * conversion (review_code_raw_task_state_duplicate). */
2035
+ declare function listInProgress(db: Db, workstream: string): TaskRow[];
2036
+ /** Most-recently-closed tasks in a workstream, newest first, capped at
2037
+ * `limit` (default 5). Used by `mu state` for its 'recent closed'
2038
+ * slice; exposed as a named SDK helper so the CLI no longer needs the
2039
+ * raw-row type that was duplicating RawTaskRow
2040
+ * (review_code_raw_task_state_duplicate). */
2041
+ declare function listRecentClosed(db: Db, workstream: string, limit?: number): TaskRow[];
2042
+ /** Optional filter knobs for `listNotes`. Default-everything-undefined
2043
+ * preserves the historical "return every note, oldest-first" shape so
2044
+ * every existing caller (cmdTaskShow's notes block, exporting.ts's
2045
+ * bucket renderer, agents.test.ts) keeps working unchanged.
2083
2046
  *
2084
- * Status side-effects (review_release_open_in_progress_inconsistency):
2085
- * - IN_PROGRESS OPEN automatically (without it, the task is
2086
- * stranded: no owner to drive it forward, but `mu task next`
2087
- * skips it because it's not OPEN).
2088
- * - OPEN / CLOSED / REJECTED / DEFERRED preserved.
2089
- * - `--reopen` forces OPEN regardless of current status the
2090
- * escape hatch for un-closing a CLOSED owned task in one verb.
2047
+ * Filters compose multiplicatively when both apply (`since` AND
2048
+ * `tail`): the timestamp filter is applied first, then `tail` slices
2049
+ * the last N of what survived. The CLI surface (`mu task notes
2050
+ * --tail / --since / --since-claim`) lives in src/cli/tasks/edit.ts;
2051
+ * the mutex between `--since` and `--since-claim` is a CLI concern,
2052
+ * not enforced here if both arrive at the SDK, `since` wins (it's
2053
+ * the explicit one) and `sinceClaim` is ignored. The auto-resolve
2054
+ * for `sinceClaim` (look up the most recent `task claim` event in
2055
+ * agent_logs) happens here so the SDK is self-contained for scripted
2056
+ * callers. */
2057
+ interface ListNotesOptions {
2058
+ /** Print only the last N notes (after any timestamp filter). Must
2059
+ * be a positive integer; a value of 0 returns no rows but is not
2060
+ * an error here — CLI-side validation rejects `--tail 0`. */
2061
+ tail?: number;
2062
+ /** ISO-8601 cutoff: only notes with `created_at > since` survive.
2063
+ * Comparison is lexicographic on the ISO string (matches the way
2064
+ * the rest of the codebase compares ISO timestamps). */
2065
+ since?: string;
2066
+ /** When true and `since` is unset, look up the `created_at` of the
2067
+ * most recent `task claim` event for this task and use it as the
2068
+ * cutoff. Falls back to no filter when no claim event exists
2069
+ * (equivalent to `--since-beginning`). */
2070
+ sinceClaim?: boolean;
2071
+ }
2072
+ /** List notes for a task. Operator-facing local_id; resolves to the
2073
+ * surrogate task id via taskIdFor (with optional workstream scope).
2091
2074
  *
2092
- * Idempotent: releasing an already-unowned task with no `--reopen` and
2093
- * no IN_PROGRESS status is a no-op (returns `changed: false`).
2094
- * Throws TaskNotFoundError on missing.
2075
+ * Optional filters: see {@link ListNotesOptions}. Default behaviour
2076
+ * (no opts) is unchanged every note, oldest-first. */
2077
+ declare function listNotes(db: Db, taskLocalId: string, workstream: string, opts?: ListNotesOptions): TaskNoteRow[];
2078
+ /**
2079
+ * All tasks currently owned by `agent` in a given workstream
2080
+ * (v5: agents.name is per-workstream unique). Sorted by local_id.
2081
+ *
2082
+ * Defaults to **excluding CLOSED** since the verb's purpose is "what
2083
+ * is X currently working on?" and a closed task is no longer being
2084
+ * worked on. closeTask intentionally preserves `owner` as a
2085
+ * historical record (so audit/notes can attribute decisions); pass
2086
+ * `{ includeClosed: true }` to surface that history.
2095
2087
  */
2096
- declare function releaseTask(db: Db, localId: string, opts: ReleaseTaskOptions): ReleaseResult;
2097
- interface ClaimTaskOptions extends EvidenceOption {
2098
- /** Workstream context for both the task and the claiming agent.
2099
- * v5: agents.name and tasks.local_id are per-workstream unique;
2100
- * the task lookup AND the agent FK lookup scope to this
2101
- * workstream so a same-named task or worker elsewhere can't be
2102
- * silently picked. The CLI always passes this from the resolved
2103
- * -w / $MU_SESSION. */
2104
- workstream: string;
2105
- /**
2106
- * Override the agent name. If omitted, derived from the current pane's
2107
- * title via `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`.
2108
- *
2109
- * Mutually exclusive with `self: true`.
2110
- */
2111
- agentName?: string;
2112
- /**
2113
- * Workstream that the claimer agent lives in. When omitted, defaults
2114
- * to `opts.workstream` (today's same-workstream behaviour). Set by
2115
- * the CLI when `mu task claim X -w A --for B/worker-1` qualifies the
2116
- * `--for` ref with a different workstream prefix
2117
- * (`task_claim_for_cross_workstream`).
2118
- *
2119
- * Cross-workstream ownership is structurally allowed by the schema:
2120
- * `tasks.owner_id` is an INTEGER FK to `agents.id` with no
2121
- * workstream qualifier on the agent side. The per-workstream UNIQUE
2122
- * on `agents(workstream_id, name)` is what previously made the
2123
- * SDK's name → id lookup scope to one workstream; this option
2124
- * widens that lookup to a different workstream when the operator
2125
- * dispatches across a workstream boundary. The agent's own
2126
- * workstream remains unchanged — only the task's `owner_id` points
2127
- * out-of-workstream.
2128
- */
2129
- agentWorkstream?: string;
2130
- /**
2131
- * Anonymous claim: write `owner = NULL` instead of resolving an agent
2132
- * name and checking the FK. Use when the actor is the orchestrator
2133
- * (or a script, or a human) doing direct work in a workstream they
2134
- * aren't a registered worker in.
2135
- *
2136
- * The actor name is still recorded — it ends up in `agent_logs.source`
2137
- * for the auto-emitted `task claim` event — so provenance is preserved.
2138
- * Just not in the FK column.
2139
- *
2140
- * Resolution order for the actor name (used as the log source):
2141
- * 1. `actor` if explicitly passed.
2142
- * 2. Current pane title (when `$TMUX_PANE` is set).
2143
- * 3. `$USER`.
2144
- * 4. The literal string 'unknown'.
2145
- *
2146
- * Mutually exclusive with `agentName` (the two are alternative
2147
- * answers to "who's the actor for this claim?"). Passing both is a
2148
- * usage error.
2149
- */
2150
- self?: boolean;
2151
- /**
2152
- * Override the actor name used for the log source when `self: true`.
2153
- * Ignored when `self: false`. Useful when the orchestrator wants to
2154
- * attribute the work to a meaningful name rather than the pane
2155
- * title (e.g. "deploy-bot" rather than "pi-mu").
2156
- */
2157
- actor?: string;
2088
+ declare function listTasksByOwner(db: Db, workstream: string, owner: string, opts?: {
2089
+ includeClosed?: boolean;
2090
+ }): TaskRow[];
2091
+ interface SearchTasksOptions {
2092
+ /** Restrict to one workstream; undefined = search across all. */
2093
+ workstream?: string;
2094
+ /** Also search `task_notes.content` (default false: titles + ids only). */
2095
+ includeNotes?: boolean;
2158
2096
  }
2159
- interface ClaimResult {
2160
- /** The agent now owning the task, or null when the claim was anonymous (--self). */
2161
- ownerName: string | null;
2162
- /** The actor recorded in the agent_logs event the agent name for a
2163
- * registered-worker claim, or the resolved actor for --self. */
2164
- actorName: string;
2165
- /** The previous owner (null if it was unowned). */
2166
- previousOwnerName: string | null;
2167
- /** The status BEFORE the claim; post-claim is IN_PROGRESS unless was CLOSED. */
2168
- previousStatus: TaskStatus;
2169
- /** The status AFTER the claim. */
2170
- status: TaskStatus;
2097
+ /**
2098
+ * Substring search on task `title` and `local_id`, case-insensitive.
2099
+ * With `includeNotes: true` also searches `task_notes.content`. The
2100
+ * pattern is wrapped in `%...%` automatically so callers don't need
2101
+ * SQL LIKE knowledge for explicit globs (or regex), use `mu sql`.
2102
+ */
2103
+ declare function searchTasks(db: Db, pattern: string, opts?: SearchTasksOptions): TaskRow[];
2104
+
2105
+ declare const WORKSPACE_STALE_THRESHOLD = 10;
2106
+ declare function isWorkspaceStale(behind: number | null | undefined): boolean;
2107
+
2108
+ interface WorkspaceRow {
2109
+ agentName: string;
2110
+ workstreamName: string;
2111
+ backend: VcsBackendName;
2112
+ path: string;
2113
+ parentRef: string | null;
2114
+ createdAt: string;
2115
+ /** How many commits the workspace's parent_ref is behind the project's
2116
+ * default branch HEAD, as of the last time the workspace's local refs
2117
+ * cache was updated. Undefined when not yet computed (the listWorkspaces
2118
+ * fast path leaves it unset; call decorateWithStaleness to populate).
2119
+ * Null when staleness was queried but cannot be computed (no main found,
2120
+ * none-backend, missing parent_ref, command failure). */
2121
+ commitsBehindMain?: number | null;
2122
+ /** True when the workspace has uncommitted / unstaged / untracked-not-
2123
+ * ignored files, as observed by the backend's `listDirtyFiles`.
2124
+ * Undefined when not yet computed (the listWorkspaces fast path leaves
2125
+ * it unset; call decorateWithDirty to populate). Null when the dirty
2126
+ * check could not be performed (backend command failure). For jj /
2127
+ * none backends — which have no operator-visible "dirty" concept —
2128
+ * this is always false (their listDirtyFiles returns []). */
2129
+ dirty?: boolean | null;
2130
+ }
2131
+ declare class WorkspaceExistsError extends Error implements HasNextSteps {
2132
+ readonly agent: string;
2133
+ readonly name = "WorkspaceExistsError";
2134
+ constructor(agent: string);
2135
+ errorNextSteps(): NextStep[];
2136
+ }
2137
+ declare class WorkspaceNotFoundError extends Error implements HasNextSteps {
2138
+ readonly agent: string;
2139
+ readonly name = "WorkspaceNotFoundError";
2140
+ constructor(agent: string);
2141
+ errorNextSteps(): NextStep[];
2171
2142
  }
2172
2143
  /**
2173
- * Claim a task. Two modes:
2174
- *
2175
- * Worker claim (default):
2176
- * Resolve an agent name from `opts.agentName` or from $TMUX_PANE's
2177
- * pane title. The name MUST exist in the agents table (FK on
2178
- * tasks.owner). Sets `owner = <name>`. This is what mu-spawned
2179
- * workers do, and what `mu task claim --for <worker>` does for
2180
- * orchestrator dispatch.
2181
- *
2182
- * Anonymous claim (--self):
2183
- * Skip the name -> agents FK lookup entirely. Sets `owner = NULL`.
2184
- * Records the actor in `agent_logs.source` instead. This is the
2185
- * orchestrator-doing-direct-work path — the actor is logged but
2186
- * not registered as a worker pane.
2187
- *
2188
- * Status side-effect: OPEN -> IN_PROGRESS; IN_PROGRESS / CLOSED unchanged.
2144
+ * Thrown by createWorkspace when the on-disk path it would create is
2145
+ * already occupied. Distinct from WorkspaceExistsError (which is about
2146
+ * the DB row) so the recovery is clear: the dir is orphaned (no DB
2147
+ * row points at it) and needs cleanup.
2189
2148
  *
2190
- * Concurrency: the worker-claim path uses a single-statement CAS UPDATE
2191
- * with `WHERE owner IS NULL OR owner = ?` so two workers racing to
2192
- * claim the same task can't both win. The anonymous path uses
2193
- * `WHERE owner IS NULL` (anonymous claims don't 'own' the task in any
2194
- * exclusive sense; if it's already owned by anyone, the anonymous claim
2195
- * is a TaskAlreadyOwnedError just like a worker claim would be).
2149
+ * Maps to exit code 4 (conflict).
2196
2150
  */
2197
- declare function claimTask(db: Db, localId: string, opts: ClaimTaskOptions): Promise<ClaimResult>;
2151
+ declare class WorkspacePathNotEmptyError extends Error implements HasNextSteps {
2152
+ readonly agent: string;
2153
+ readonly workstream: string;
2154
+ readonly workspacePath: string;
2155
+ readonly name = "WorkspacePathNotEmptyError";
2156
+ constructor(agent: string, workstream: string, workspacePath: string);
2157
+ errorNextSteps(): NextStep[];
2158
+ }
2198
2159
  /**
2199
- * Resolve the current actor's identity for attribution in task notes,
2200
- * --self claims, and any other write that wants 'who did this?'.
2201
- *
2202
- * Resolution order:
2203
- * 1. $MU_AGENT_NAME env var (set by mu spawnAgent on every managed
2204
- * pane; surfaced from the f3d4bdd commit). Authoritative when
2205
- * present — you're inside a mu-spawned worker, no ambiguity.
2206
- * 2. tmux pane title (the pane-title identity step). Works
2207
- * when running inside any pane mu manages OR adopted.
2208
- * 3. $USER (when running outside tmux entirely).
2209
- * 4. The literal 'orchestrator' as a last-resort default.
2160
+ * Thrown by createWorkspace when the resolved projectRoot is the
2161
+ * user's $HOME.
2210
2162
  *
2211
- * Why prefer env over pane title: pane titles are a tmux-server-wide
2212
- * resource that anything can rewrite. The env var is set per-pane at
2213
- * spawn time and is unforgeable from outside without explicit
2214
- * `--actor` override. Pane title is the only identity available for
2215
- * adopted panes that didn't go through mu's spawn path.
2163
+ * Maps to exit code 4 (conflict).
2216
2164
  */
2217
- declare function resolveActorIdentity(): Promise<string>;
2218
-
2219
- interface TaskRow {
2220
- /** Per-workstream-unique TEXT name. The operator-facing identifier. */
2221
- name: string;
2222
- /** Foreign-name reference to the owning workstream. */
2165
+ declare class HomeDirAsProjectRootError extends Error implements HasNextSteps {
2166
+ readonly agent: string;
2167
+ readonly workstream: string;
2168
+ readonly homeDir: string;
2169
+ readonly name = "HomeDirAsProjectRootError";
2170
+ constructor(agent: string, workstream: string, homeDir: string);
2171
+ errorNextSteps(): NextStep[];
2172
+ }
2173
+ /**
2174
+ * Compose the canonical on-disk path for an agent's workspace. Used by
2175
+ * createWorkspace and reachable from `mu workspace path` so the user
2176
+ * can `cd $(mu workspace path foo)` even before the directory exists.
2177
+ */
2178
+ declare function workspacePath(workstream: string, agent: string): string;
2179
+ /** Root dir for a workstream's workspaces — the parent of all
2180
+ * per-agent workspace dirs. Used by listWorkspaceOrphans to scan
2181
+ * the filesystem. */
2182
+ declare function workspacesRoot(workstream: string): string;
2183
+ interface WorkspaceStaleness {
2184
+ agentName: string;
2223
2185
  workstreamName: string;
2224
- title: string;
2225
- status: TaskStatus;
2226
- impact: number;
2227
- effortDays: number;
2228
- /** Foreign-name reference to the owning agent (NULL when unowned). */
2229
- ownerName: string | null;
2230
- createdAt: string;
2231
- updatedAt: string;
2186
+ commitsBehindMain: number | null;
2187
+ isStale: boolean;
2232
2188
  }
2233
- interface TaskNoteRow {
2234
- author: string | null;
2235
- content: string;
2236
- createdAt: string;
2189
+
2190
+ interface CreateWorkspaceOptions {
2191
+ agent: string;
2192
+ workstream: string;
2193
+ /** Project root to branch from. Defaults to the current working
2194
+ * directory (the `mu` invocation site, which is normally what the
2195
+ * user wants). */
2196
+ projectRoot?: string;
2197
+ /** Override backend detection. Default: walk `detectBackend`.
2198
+ * Accepts either a name ("jj" / "sl" / "git" / "none") OR a
2199
+ * pre-built `VcsBackend` object — the object form lets tests inject
2200
+ * a fresh fake backend without mutating the exported singletons. */
2201
+ backend?: VcsBackendName | VcsBackend;
2202
+ /** Optional ref to base the workspace on. Backend-specific. */
2203
+ parentRef?: string;
2204
+ /** INTERNAL. When false, suppress the `workspace create` system
2205
+ * event. Used by `recreateWorkspace` so the audit trail records
2206
+ * ONE atomic `workspace recreate` line instead of separate
2207
+ * free + create entries. Defaults to true. */
2208
+ _suppressEvent?: boolean;
2237
2209
  }
2238
- declare function isValidTaskId(id: string): boolean;
2239
2210
  /**
2240
- * Lowercase title; collapse non-alnum runs into single `_`; trim
2241
- * leading/trailing `_`; prefix `t_` if the result starts with a digit
2242
- * (schema requires first char letter); apply the soft cap with
2243
- * word-boundary trim (cut at the last `_` at-or-before SLUG_SOFT_CAP
2244
- * when one exists, else hard-truncate). Mirrors `tg`'s `id_from_title`
2245
- * but adds the soft cap.
2246
- *
2247
- * Throws if `title` yields an empty slug after stripping.
2211
+ * Create a fresh workspace for an agent. Allocates the on-disk
2212
+ * directory, records the row, emits a system event. Idempotent ONLY
2213
+ * to the extent that the row check is up-front; if the row exists
2214
+ * we throw `WorkspaceExistsError` rather than silently re-using a
2215
+ * possibly-stale on-disk state. Callers should `freeWorkspace` first.
2248
2216
  */
2249
- declare function slugifyTitle(title: string): string;
2217
+ declare function createWorkspace(db: Db, opts: CreateWorkspaceOptions): Promise<WorkspaceRow>;
2218
+ declare function getWorkspaceForAgent(db: Db, agent: string, workstream: string): WorkspaceRow | undefined;
2219
+ declare function listWorkspaces(db: Db, workstream?: string): WorkspaceRow[];
2220
+ interface FreeWorkspaceOptions {
2221
+ /** If true, attempt to commit pending changes before tearing down.
2222
+ * Backend-specific; see VcsBackend.freeWorkspace. */
2223
+ commit?: boolean;
2224
+ /** INTERNAL. When false, suppress the `workspace free` system
2225
+ * event AND skip the pre-mutation snapshot capture. Used by
2226
+ * `recreateWorkspace` so the audit trail records ONE atomic
2227
+ * `workspace recreate` line and one snapshot for the whole
2228
+ * free+create cycle. Defaults to true. */
2229
+ _suppressEvent?: boolean;
2230
+ }
2231
+ interface FreeWorkspaceResult {
2232
+ /** The committed ref, when `commit` was true and there was something
2233
+ * to commit. */
2234
+ committedRef?: string;
2235
+ /** True iff the on-disk path was actually removed. */
2236
+ removed: boolean;
2237
+ /** True iff the DB row was actually deleted. */
2238
+ rowDeleted: boolean;
2239
+ }
2250
2240
  /**
2251
- * Result of `slugifyTitleVerbose`: the slug plus enough metadata for
2252
- * the CLI to decide whether to warn the user that meaning was lost.
2241
+ * Tear down an agent's workspace. Calls the backend to remove the
2242
+ * on-disk directory (with optional auto-commit), then DELETEs the row.
2243
+ * Idempotent on a missing workspace (returns all-false).
2244
+ */
2245
+ declare function freeWorkspace(db: Db, agent: string, opts: FreeWorkspaceOptions & {
2246
+ workstream: string;
2247
+ }): Promise<FreeWorkspaceResult>;
2248
+
2249
+ declare function getWorkspaceStaleness(db: Db, agentName: string, workstreamName: string): Promise<WorkspaceStaleness | null>;
2250
+ /**
2251
+ * Decorate each row with `commitsBehindMain` by asking the row's backend
2252
+ * how far the parent_ref is behind the project's default branch HEAD.
2253
+ * Cheap, pure observation: NO automatic `git fetch` / `jj git fetch` /
2254
+ * `sl pull`. The number is as fresh as the workspace's local refs cache.
2253
2255
  *
2254
- * slug — the same string `slugifyTitle` returns.
2255
- * strippedLength length of the post-strip pre-cap slug. When this
2256
- * exceeds the SLUG_SOFT_CAP the verbose form had to
2257
- * cut at a word boundary (or hard-truncate); the
2258
- * cut clauses are gone with no in-band signal.
2259
- * originalSlug — what the slug WOULD have been without the
2260
- * SLUG_SOFT_CAP cut: full stripped slug with the
2261
- * same `t_` digit-prefix correction and the same
2262
- * SLUG_HARD_CAP ceiling, but no word-boundary
2263
- * truncation. Equal to `slug` when nothing was
2264
- * cut. The CLI surfaces this in `mu task add
2265
- * --json` so scripted callers can detect the
2266
- * truncation without grepping stderr.
2267
- * truncated — true iff `slug.length < strippedLength` AFTER the
2268
- * `t_` digit-prefix correction, i.e. real bytes were
2269
- * dropped. False for any title that fits under the
2270
- * soft cap or whose only diff vs the stripped slug
2271
- * is the `t_` prefix.
2256
+ * Returns a NEW array; does not mutate the input. Rows whose parent_ref
2257
+ * is missing, or whose backend's commitsBehind throws / returns null,
2258
+ * get `commitsBehindMain: null`.
2259
+ */
2260
+ declare function decorateWithStaleness(rows: readonly WorkspaceRow[]): Promise<WorkspaceRow[]>;
2261
+ /**
2262
+ * Decorate every row with a `dirty` marker — true when the backend's
2263
+ * `listDirtyFiles` reports any uncommitted / unstaged / untracked-not-
2264
+ * ignored files; false when clean; null on backend-command failure.
2272
2265
  *
2273
- * The CLI's `mu task add` uses `truncated` to print a one-line stderr
2274
- * hint pointing at the `<id>` positional override and (under --json)
2275
- * to surface `originalSlug` alongside `truncated:true`
2276
- * (slugifytitle_silently_drops_clauses; task_add_slugify_silently_truncates_ids).
2266
+ * Returns a NEW array; does not mutate the input.
2277
2267
  */
2278
- interface SlugifyResult {
2279
- slug: string;
2280
- strippedLength: number;
2281
- originalSlug: string;
2282
- truncated: boolean;
2268
+ declare function decorateWithDirty(rows: readonly WorkspaceRow[]): Promise<WorkspaceRow[]>;
2269
+
2270
+ interface WorkspaceOrphan {
2271
+ /** The on-disk dir name (the agent name it WOULD be for, if mu had
2272
+ * registered it). */
2273
+ agentName: string;
2274
+ /** Workstream the dir is filed under. */
2275
+ workstreamName: string;
2276
+ /** Absolute path to the orphan dir. */
2277
+ path: string;
2283
2278
  }
2284
2279
  /**
2285
- * Verbose sibling of `slugifyTitle`: returns the slug AND a
2286
- * `truncated` flag so the CLI can hint to the user when the soft cap
2287
- * dropped clauses (the meaning-shift hazard documented in
2288
- * slugifytitle_silently_drops_clauses).
2280
+ * Like WorkspaceOrphan but additionally flags whether the parent
2281
+ * workstream itself is gone (no row in `workstreams`). Returned by
2282
+ * listAllOrphanWorkspaces; the per-workstream listWorkspaceOrphans
2283
+ * doesn't carry this since by construction it only runs against an
2284
+ * existing workstream.
2285
+ */
2286
+ interface StrandedWorkspaceOrphan extends WorkspaceOrphan {
2287
+ /** True iff the parent workstream has no DB row (the dir was left
2288
+ * behind by a `mu workstream destroy` or a manual DELETE). */
2289
+ stranded: boolean;
2290
+ }
2291
+ /**
2292
+ * Scan `<state-dir>/workspaces/<workstream>/` for directories that
2293
+ * have no row in `vcs_workspaces`.
2289
2294
  *
2290
- * Algorithm is byte-for-byte identical to `slugifyTitle`; this just
2291
- * surfaces the metadata that the plain form throws away.
2295
+ * Returns `[]` when the workstream's workspaces dir doesn't exist,
2296
+ * or when every dir on disk has a corresponding DB row. Filesystem
2297
+ * read is best-effort: a missing/inaccessible dir returns `[]`.
2292
2298
  */
2293
- declare function slugifyTitleVerbose(title: string): SlugifyResult;
2299
+ declare function listWorkspaceOrphans(db: Db, workstream: string): WorkspaceOrphan[];
2294
2300
  /**
2295
- * Generate a unique task id from a title. v5: tasks.local_id is
2296
- * per-workstream unique, so the collision check scopes to one
2297
- * workstream. On collision, appends `_2`, `_3`, until unique.
2301
+ * Cross-workstream variant of listWorkspaceOrphans. Reads
2302
+ * `<state-dir>/workspaces/`, recurses one level (per-ws subdir
2303
+ * per-agent subdir), and surfaces every dir with no row in
2304
+ * `vcs_workspaces`.
2298
2305
  */
2299
- declare function idFromTitle(db: Db, workstream: string, title: string): string;
2306
+ declare function listAllOrphanWorkspaces(db: Db): StrandedWorkspaceOrphan[];
2307
+
2308
+ interface RecreateWorkspaceOptions {
2309
+ /** Same as createWorkspace; defaults to cwd. */
2310
+ projectRoot?: string;
2311
+ /** Same as createWorkspace; if undefined the previous backend is
2312
+ * reused (auto-detection re-runs only when --backend was passed). */
2313
+ backend?: VcsBackendName | VcsBackend;
2314
+ /** Same as createWorkspace; if undefined the new workspace bases on
2315
+ * the backend's current head (for git/jj/sl: the project's main),
2316
+ * which is the whole point of the verb. */
2317
+ parentRef?: string;
2318
+ /** When true, skip the dirty-check refusal and discard any
2319
+ * uncommitted changes in the existing workspace. The lossy escape
2320
+ * hatch — mirrors the implicit semantics of `mu workspace free`
2321
+ * without --commit. */
2322
+ force?: boolean;
2323
+ }
2324
+ interface RecreateWorkspaceResult {
2325
+ /** The freshly-created workspace row (the previous row is already
2326
+ * gone by the time we return). */
2327
+ workspace: WorkspaceRow;
2328
+ /** parent_ref of the WORKSPACE BEFORE recreate, so callers (and the
2329
+ * CLI's success message) can show "bumped from <old> -> <new>". */
2330
+ previousParentRef: string | null;
2331
+ }
2332
+ /**
2333
+ * Free + create in one atomic-ish verb. Between waves the operator
2334
+ * wants the SAME agent name with a fresh workspace pinned to current
2335
+ * main; doing `free` then `create` manually was the dogfood-painful
2336
+ * pattern.
2337
+ */
2338
+ declare function recreateWorkspace(db: Db, agent: string, opts: RecreateWorkspaceOptions & {
2339
+ workstream: string;
2340
+ }): Promise<RecreateWorkspaceResult>;
2341
+
2342
+ declare class TaskNotFoundError extends Error implements HasNextSteps {
2343
+ readonly taskId: string;
2344
+ readonly name = "TaskNotFoundError";
2345
+ constructor(taskId: string);
2346
+ errorNextSteps(): NextStep[];
2347
+ }
2348
+ declare class TaskExistsError extends Error implements HasNextSteps {
2349
+ readonly taskId: string;
2350
+ readonly name = "TaskExistsError";
2351
+ constructor(taskId: string);
2352
+ errorNextSteps(): NextStep[];
2353
+ }
2300
2354
  /**
2301
- * Result of `idFromTitleVerbose`: the unique-in-workstream id plus the
2302
- * truncated flag from the underlying slugify pass. Used by `mu task
2303
- * add` to decide whether to surface the stderr hint about lost clauses
2304
- * (slugifytitle_silently_drops_clauses) and to surface the un-truncated
2305
- * slug in `--json` (task_add_slugify_silently_truncates_ids).
2306
- *
2307
- * id — the unique-in-workstream task id.
2308
- * truncated — true iff the underlying slugify pass cut real
2309
- * characters (collision-suffixing does NOT flip
2310
- * this).
2311
- * originalSlug — what the slug would have been without the
2312
- * SLUG_SOFT_CAP cut. Equal to `id` when nothing was
2313
- * cut AND no collision suffix was appended; for
2314
- * the truncation-detection use case the only thing
2315
- * the CLI cares about is the lossy-vs-not
2316
- * comparison surfaced via `truncated`.
2355
+ * Thrown when a verb is invoked with `-w/--workstream <name>` but the
2356
+ * named task lives in a different workstream. Distinguishes "the user
2357
+ * typo'd the workstream" from "the task doesn't exist anywhere"
2358
+ * (which surfaces as `TaskNotFoundError`). Maps to exit code 4
2359
+ * (conflict / wrong scope).
2317
2360
  */
2318
- interface IdFromTitleResult {
2319
- id: string;
2320
- truncated: boolean;
2321
- originalSlug: string;
2361
+ declare class TaskNotInWorkstreamError extends Error implements HasNextSteps {
2362
+ readonly taskId: string;
2363
+ readonly expectedWorkstream: string;
2364
+ readonly actualWorkstream: string;
2365
+ readonly name = "TaskNotInWorkstreamError";
2366
+ constructor(taskId: string, expectedWorkstream: string, actualWorkstream: string);
2367
+ errorNextSteps(): NextStep[];
2368
+ }
2369
+ declare class TaskAlreadyOwnedError extends Error implements HasNextSteps {
2370
+ readonly taskId: string;
2371
+ readonly currentOwner: string;
2372
+ readonly name = "TaskAlreadyOwnedError";
2373
+ constructor(taskId: string, currentOwner: string);
2374
+ errorNextSteps(): NextStep[];
2322
2375
  }
2323
2376
  /**
2324
- * Verbose sibling of `idFromTitle`: returns the unique id, the
2325
- * `truncated` flag from the slugify pass, and the un-truncated
2326
- * `originalSlug` for `--json` consumers. Collision-suffixing (`_2`,
2327
- * `_3`, …) does not flip `truncated` the underlying slug's lossiness
2328
- * is what the CLI hint cares about.
2377
+ * Thrown by `rejectTask` / `deferTask` when the target task has
2378
+ * dependents that are still OPEN or IN_PROGRESS. Rejecting or
2379
+ * deferring such a task would silently strand the dependents (they'd
2380
+ * remain blocked by a prereq that's never going to satisfy the edge),
2381
+ * so we refuse and force an explicit decision: pass `--cascade` to
2382
+ * apply the same status to every transitive dependent, drop the
2383
+ * blocking edge first with `mu task unblock`, or address the
2384
+ * dependents individually. Maps to exit code 4.
2329
2385
  */
2330
- declare function idFromTitleVerbose(db: Db, workstream: string, title: string): IdFromTitleResult;
2331
- declare function getTask(db: Db, localId: string, workstream: string): TaskRow | undefined;
2386
+ declare class TaskHasOpenDependentsError extends Error implements HasNextSteps {
2387
+ readonly taskId: string;
2388
+ readonly verb: "reject" | "defer";
2389
+ readonly dependents: readonly string[];
2390
+ readonly name = "TaskHasOpenDependentsError";
2391
+ constructor(taskId: string, verb: "reject" | "defer", dependents: readonly string[]);
2392
+ errorNextSteps(): NextStep[];
2393
+ }
2332
2394
  /**
2333
- * List tasks. With no `workstream` arg returns every row used by `mu sql`
2334
- * and by tests; CLI surfaces always pass a workstream so users only see
2335
- * their own.
2395
+ * Thrown when `mu task claim` resolves a claimer agent name (from the
2396
+ * pane title or --for) that has no matching row in the agents table.
2397
+ *
2398
+ * The FK on `tasks.owner` references `agents.name`; without this guard
2399
+ * the claim attempt would fail with the unhelpful 'FOREIGN KEY constraint
2400
+ * failed' from SQLite. This typed error gives the user actionable next
2401
+ * steps (run `mu agent adopt <pane-id>` to register, or use --for to pick a
2402
+ * different agent).
2403
+ *
2404
+ * Maps to exit code 4 (conflict) via the cli.ts handler.
2336
2405
  */
2337
- interface ListTasksOptions {
2338
- /** Filter to one or more lifecycle statuses. Omitted = all statuses. */
2339
- status?: TaskStatus | readonly TaskStatus[];
2406
+ declare class ClaimerNotRegisteredError extends Error implements HasNextSteps {
2407
+ readonly agentName: string;
2408
+ readonly paneId: string | null;
2409
+ readonly name = "ClaimerNotRegisteredError";
2410
+ constructor(agentName: string, paneId: string | null);
2411
+ /**
2412
+ * Three actionable resolutions in expected-frequency order:
2413
+ * 1. --self : orchestrator pattern (working directly)
2414
+ * 2. --for : dispatcher pattern (assigning to a worker)
2415
+ * 3. mu agent adopt: registration pattern (promote pane to worker)
2416
+ */
2417
+ errorNextSteps(): NextStep[];
2340
2418
  }
2341
- declare function listTasks(db: Db, workstream?: string, opts?: ListTasksOptions): TaskRow[];
2342
- /** Options for listReady. The optional `statuses` filter composes
2343
- * on top of the `ready` view (which itself constrains to
2344
- * `status='OPEN'`); passing only OPEN is identical to today's no-
2345
- * filter shape, passing only non-OPEN values returns []. Exists so
2346
- * `mu task next --status` can mirror the multi-status flag shape
2347
- * shipped on `mu task list` (task_list_multi_status_union). */
2348
- interface ListReadyOptions {
2349
- status?: TaskStatus | readonly TaskStatus[];
2419
+ declare class CycleError extends Error implements HasNextSteps {
2420
+ readonly from: string;
2421
+ readonly to: string;
2422
+ readonly name = "CycleError";
2423
+ constructor(from: string, to: string);
2424
+ errorNextSteps(): NextStep[];
2350
2425
  }
2351
- declare function listReady(db: Db, workstream: string, opts?: ListReadyOptions): TaskRow[];
2352
- declare function listBlocked(db: Db, workstream: string): TaskRow[];
2353
- declare function listGoals(db: Db, workstream: string): TaskRow[];
2354
- /** All IN_PROGRESS tasks in a workstream, most-recently-touched first.
2355
- * Used by `mu state` and `mu hud` to populate their in-progress slice;
2356
- * exposed as a named SDK helper so those CLI verbs don't re-derive
2357
- * the row-shape conversion (review_code_raw_task_state_duplicate). */
2358
- declare function listInProgress(db: Db, workstream: string): TaskRow[];
2359
- /** Most-recently-closed tasks in a workstream, newest first, capped at
2360
- * `limit` (default 5). Used by `mu state` for its 'recent closed'
2361
- * slice; exposed as a named SDK helper so the CLI no longer needs the
2362
- * raw-row type that was duplicating RawTaskRow
2363
- * (review_code_raw_task_state_duplicate). */
2364
- declare function listRecentClosed(db: Db, workstream: string, limit?: number): TaskRow[];
2365
- /** Optional filter knobs for `listNotes`. Default-everything-undefined
2366
- * preserves the historical "return every note, oldest-first" shape so
2367
- * every existing caller (cmdTaskShow's notes block, exporting.ts's
2368
- * bucket renderer, agents.test.ts) keeps working unchanged.
2369
- *
2370
- * Filters compose multiplicatively when both apply (`since` AND
2371
- * `tail`): the timestamp filter is applied first, then `tail` slices
2372
- * the last N of what survived. The CLI surface (`mu task notes
2373
- * --tail / --since / --since-claim`) lives in src/cli/tasks/edit.ts;
2374
- * the mutex between `--since` and `--since-claim` is a CLI concern,
2375
- * not enforced here — if both arrive at the SDK, `since` wins (it's
2376
- * the explicit one) and `sinceClaim` is ignored. The auto-resolve
2377
- * for `sinceClaim` (look up the most recent `task claim` event in
2378
- * agent_logs) happens here so the SDK is self-contained for scripted
2379
- * callers. */
2380
- interface ListNotesOptions {
2381
- /** Print only the last N notes (after any timestamp filter). Must
2382
- * be a positive integer; a value of 0 returns no rows but is not
2383
- * an error here — CLI-side validation rejects `--tail 0`. */
2384
- tail?: number;
2385
- /** ISO-8601 cutoff: only notes with `created_at > since` survive.
2386
- * Comparison is lexicographic on the ISO string (matches the way
2387
- * the rest of the codebase compares ISO timestamps). */
2388
- since?: string;
2389
- /** When true and `since` is unset, look up the `created_at` of the
2390
- * most recent `task claim` event for this task and use it as the
2391
- * cutoff. Falls back to no filter when no claim event exists
2392
- * (equivalent to `--since-beginning`). */
2393
- sinceClaim?: boolean;
2426
+ declare class CrossWorkstreamEdgeError extends Error implements HasNextSteps {
2427
+ readonly blocker: string;
2428
+ readonly blockerWorkstream: string;
2429
+ readonly dependent: string;
2430
+ readonly dependentWorkstream: string;
2431
+ readonly name = "CrossWorkstreamEdgeError";
2432
+ constructor(blocker: string, blockerWorkstream: string, dependent: string, dependentWorkstream: string);
2433
+ errorNextSteps(): NextStep[];
2434
+ }
2435
+
2436
+ interface SetStatusResult {
2437
+ /** Status before the call. */
2438
+ previousStatus: TaskStatus;
2439
+ /** Status after the call (== requested status). */
2440
+ status: TaskStatus;
2441
+ /** True iff the row actually changed. False on idempotent no-op. */
2442
+ changed: boolean;
2394
2443
  }
2395
- /** List notes for a task. Operator-facing local_id; resolves to the
2396
- * surrogate task id via taskIdFor (with optional workstream scope).
2397
- *
2398
- * Optional filters: see {@link ListNotesOptions}. Default behaviour
2399
- * (no opts) is unchanged — every note, oldest-first. */
2400
- declare function listNotes(db: Db, taskLocalId: string, workstream: string, opts?: ListNotesOptions): TaskNoteRow[];
2401
2444
  /**
2402
- * All tasks currently owned by `agent` in a given workstream
2403
- * (v5: agents.name is per-workstream unique). Sorted by local_id.
2404
- *
2405
- * Defaults to **excluding CLOSED** since the verb's purpose is "what
2406
- * is X currently working on?" and a closed task is no longer being
2407
- * worked on. closeTask intentionally preserves `owner` as a
2408
- * historical record (so audit/notes can attribute decisions); pass
2409
- * `{ includeClosed: true }` to surface that history.
2445
+ * Optional evidence string carried on lifecycle verbs (close / open /
2446
+ * claim / release). Lands in the auto-emitted `kind='event'` payload
2447
+ * verbatim, prefixed with `evidence=`. The first inch of distinguishing
2448
+ * "observed" from "claimed" state per an internal critique: the
2449
+ * verb still trusts the caller (it's not a verifier), but the audit
2450
+ * trail records what the caller said it relied on.
2410
2451
  */
2411
- declare function listTasksByOwner(db: Db, workstream: string, owner: string, opts?: {
2412
- includeClosed?: boolean;
2413
- }): TaskRow[];
2414
- interface SearchTasksOptions {
2415
- /** Restrict to one workstream; undefined = search across all. */
2416
- workstream?: string;
2417
- /** Also search `task_notes.content` (default false: titles + ids only). */
2418
- includeNotes?: boolean;
2452
+ interface EvidenceOption {
2453
+ evidence?: string;
2419
2454
  }
2420
2455
  /**
2421
- * Substring search on task `title` and `local_id`, case-insensitive.
2422
- * With `includeNotes: true` also searches `task_notes.content`. The
2423
- * pattern is wrapped in `%...%` automatically so callers don't need
2424
- * SQL LIKE knowledge — for explicit globs (or regex), use `mu sql`.
2456
+ * Flip a task's status to any of OPEN / IN_PROGRESS / CLOSED.
2457
+ * Idempotent: setting a task to its current status is a no-op (returns
2458
+ * `changed: false`) rather than throwing. Owner is unchanged.
2425
2459
  */
2426
- declare function searchTasks(db: Db, pattern: string, opts?: SearchTasksOptions): TaskRow[];
2427
- interface TaskEdges {
2428
- /** Tasks that must close before this one can start (blockers). */
2429
- blockers: string[];
2430
- /** Tasks that this one blocks (dependents). */
2431
- dependents: string[];
2460
+ declare function setTaskStatus(db: Db, localId: string, status: TaskStatus, opts: EvidenceOption & {
2461
+ workstream: string;
2462
+ }): SetStatusResult;
2463
+ /** Result of `closeTask` when called with `ifReady: true` and the
2464
+ * task is NOT yet ready to close (still has at least one OPEN /
2465
+ * IN_PROGRESS blocker). Distinguished from a regular `SetStatusResult`
2466
+ * by the literal `skipped` field; the CLI keys on it to switch
2467
+ * between the "closed" and "waiting" rendering paths.
2468
+ *
2469
+ * Surfaced in `fb_umbrella_no_auto_close` (impact=60): a wave umbrella
2470
+ * with N blockers stayed OPEN after every blocker reached a terminal
2471
+ * status. `--if-ready` is the cheap fix: bare `mu task close` is
2472
+ * unchanged (closes regardless), `--if-ready` is a no-op unless every
2473
+ * blocker is in a terminal status (CLOSED / REJECTED / DEFERRED).
2474
+ * Reject and defer satisfy the predicate too because `--if-ready`'s
2475
+ * job is to fire when the umbrella has nothing left to wait for, and
2476
+ * a rejected/deferred blocker is no longer being waited on. */
2477
+ interface CloseSkippedResult {
2478
+ /** Always 'not_ready' when set; future cause-codes can extend this
2479
+ * without reshaping the JSON payload (the literal-union narrows
2480
+ * safely in the CLI rendering path). */
2481
+ skipped: "not_ready";
2482
+ /** Status before the call (always the current status, no change). */
2483
+ previousStatus: TaskStatus;
2484
+ /** Status after the call (== previousStatus, since we no-op). */
2485
+ status: TaskStatus;
2486
+ /** Always false on a skip (no row mutated). */
2487
+ changed: false;
2488
+ /** Local ids of every blocker still in OPEN or IN_PROGRESS, sorted
2489
+ * alphabetically for deterministic rendering. Empty list is
2490
+ * impossible on this branch — the no-op only fires when ≥1
2491
+ * blocker is non-terminal. */
2492
+ blockingIds: string[];
2493
+ }
2494
+ interface CloseTaskOptions extends EvidenceOption {
2495
+ workstream: string;
2496
+ /** When true, no-op the close unless every blocker is in a terminal
2497
+ * status (CLOSED / REJECTED / DEFERRED). Returns a
2498
+ * `CloseSkippedResult` carrying the still-blocking ids; the CLI
2499
+ * renders the skip with a Next: hint pointing at `mu task wait`.
2500
+ * When false / omitted, behaves as bare `closeTask` (closes
2501
+ * regardless of blocker status). */
2502
+ ifReady?: boolean;
2503
+ /** Optional actor identity attributed to the synthetic `CLOSE: …`
2504
+ * note auto-inserted when `evidence` is non-empty (see closeTask
2505
+ * body). The CLI resolves this via `resolveActorIdentity()` so the
2506
+ * note carries the closing worker's name; SDK callers (tests,
2507
+ * internal use) may omit it (the note then carries no author, same
2508
+ * as a bare `addNote` without `--author`). Surfaced in mufeedback
2509
+ * task_close_evidence_does_not_append_the. */
2510
+ author?: string;
2432
2511
  }
2433
- /** One end of an edge with the neighbour's current status attached.
2434
- * Used by `mu task show` to group blockers/dependents into
2435
- * "still gating" vs "satisfied" buckets without making the renderer
2436
- * do a second round-trip to the DB per neighbour. */
2437
- interface TaskEdgeWithStatus {
2438
- name: string;
2512
+ /** Convenience: setTaskStatus(db, id, "CLOSED"). Accepts evidence.
2513
+ * Pre-snapshots the DB (snap_design §CAPTURE STRATEGY > WHEN). Skipped
2514
+ * for the idempotent no-op (already CLOSED) so we don't accumulate
2515
+ * empty-delta snapshots on retry loops.
2516
+ *
2517
+ * With `ifReady: true`, returns a `CloseSkippedResult` (no mutation,
2518
+ * no snapshot) when any blocker is still OPEN / IN_PROGRESS. Used by
2519
+ * `mu task close --if-ready` so an orchestrator can fire-and-forget
2520
+ * the umbrella close after every blocker resolves without first
2521
+ * re-querying the graph. */
2522
+ declare function closeTask(db: Db, localId: string, opts: CloseTaskOptions): SetStatusResult | CloseSkippedResult;
2523
+ /** Convenience: setTaskStatus(db, id, "OPEN"). Owner intentionally NOT
2524
+ * cleared — use `releaseTask` for that. Accepts evidence. */
2525
+ declare function openTask(db: Db, localId: string, opts: EvidenceOption & {
2526
+ workstream: string;
2527
+ }): SetStatusResult;
2528
+ interface RejectDeferOptions extends EvidenceOption {
2529
+ /** Workstream context for the root task. All internal task lookups
2530
+ * (including the dependent walk) scope to this workstream. */
2531
+ workstream: string;
2532
+ /** If true, walk the transitive dependent closure and (with `yes`)
2533
+ * apply the same status to every dependent, atomically. Without
2534
+ * `yes`, runs as a dry-run: returns the list of tasks that WOULD
2535
+ * be swept (changedIds) with `dryRun: true` and changes nothing.
2536
+ * Logs one event per task (via setTaskStatus) on commit. */
2537
+ cascade?: boolean;
2538
+ /** Required to actually commit a `cascade` operation. Without it,
2539
+ * cascade is dry-run only — prints the affected dependents so the
2540
+ * caller can verify before sweeping. Mirrors `mu workstream destroy
2541
+ * --yes`. Surfaced in mufeedback bug_cascade_reject_too_aggressive
2542
+ * when an accidentally-cascaded reject swept hud_dogfood (which had
2543
+ * independent merit and needed reopening). */
2544
+ yes?: boolean;
2545
+ }
2546
+ interface RejectDeferResult {
2547
+ /** Tasks that actually changed status, in cascade order (root first). */
2548
+ changedIds: string[];
2549
+ /** The status now stamped on every changedId. */
2439
2550
  status: TaskStatus;
2551
+ /** True iff anything changed. False on a clean idempotent no-op
2552
+ * (root task already in target status, no dependents). */
2553
+ changed: boolean;
2554
+ /** True iff this was a `cascade` dry-run (cascade requested without
2555
+ * `yes`). In that case `changedIds` lists tasks that WOULD be
2556
+ * swept; the DB is unchanged. */
2557
+ dryRun: boolean;
2558
+ /** Tasks that would be touched by a cascade. Same as `changedIds`
2559
+ * on a dry-run; populated even on a commit so the caller can
2560
+ * report what was swept. */
2561
+ affectedIds: string[];
2440
2562
  }
2441
- interface TaskEdgesWithStatus {
2442
- /** Tasks that must close before this one can start (blockers),
2443
- * carrying each blocker's current status. */
2444
- blockers: TaskEdgeWithStatus[];
2445
- /** Tasks that this one blocks (dependents), carrying each
2446
- * dependent's current status. */
2447
- dependents: TaskEdgeWithStatus[];
2563
+ /** Reject a task: terminal 'won't do' (out of scope, duplicate, wontfix).
2564
+ * Refuses if dependents are open unless `--cascade`.
2565
+ * Pre-snapshots once at the verb level so a cascade onto N children
2566
+ * produces a single snapshot, not N. Skipped for the idempotent no-op. */
2567
+ declare function rejectTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
2568
+ /** Defer a task: parked, may revisit. Same dependent-stranding semantics
2569
+ * as reject (DEFERRED also doesn't satisfy a `--blocked-by` edge).
2570
+ * Pre-snapshots once at the verb level. Skipped for the idempotent no-op. */
2571
+ declare function deferTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
2572
+
2573
+ interface ReleaseResult {
2574
+ /** The previous owner (null if the task was already unowned). */
2575
+ previousOwnerName: string | null;
2576
+ /** Status before the release. */
2577
+ previousStatus: TaskStatus;
2578
+ /** Status after the release. */
2579
+ status: TaskStatus;
2580
+ /** True iff owner OR status actually changed. */
2581
+ changed: boolean;
2448
2582
  }
2449
- /**
2450
- * Direct (one-hop) edges for a task. For transitive prerequisites, use
2451
- * `getPrerequisites()`; this helper is the immediate-neighbour view used
2452
- * by `mu task show`.
2453
- */
2454
- declare function getTaskEdges(db: Db, taskLocalId: string, workstream: string): TaskEdges;
2455
- /**
2456
- * Same one-hop edge view as `getTaskEdges`, but each neighbour is
2457
- * returned as `{ name, status }` so callers can group / colour by
2458
- * status without an N+1 round-trip. Used by `mu task show` to split
2459
- * "blocked by" (still-gating) from "satisfied" (already-CLOSED)
2460
- * blockers, and the symmetric split on the dependents side
2461
- * (task_show_blocked_by_renders_closed). The status is the neighbour's
2462
- * full TaskStatus, not just OPEN/CLOSED — REJECTED/DEFERRED still
2463
- * gate downstream work, so the renderer keeps them in the
2464
- * still-gating bucket.
2465
- */
2466
- declare function getTaskEdgesWithStatus(db: Db, taskLocalId: string, workstream: string): TaskEdgesWithStatus;
2467
- /**
2468
- * All tasks transitively reachable from `taskId` via reverse-edge
2469
- * traversal (i.e. the set of tasks that block this one), including the
2470
- * task itself.
2471
- */
2472
- declare function getPrerequisites(db: Db, taskLocalId: string, workstream: string): Set<string>;
2473
- interface AddTaskOptions {
2474
- localId: string;
2583
+ interface ReleaseTaskOptions extends EvidenceOption {
2584
+ /** Workstream context for the task (v5: tasks.local_id is
2585
+ * per-workstream unique). */
2475
2586
  workstream: string;
2476
- title: string;
2477
- /** 1..100; enforced by schema CHECK. */
2478
- impact: number;
2479
- /** > 0; enforced by schema CHECK. */
2480
- effortDays: number;
2481
- /**
2482
- * Tasks that block this one. Edges inserted as `blocker -> newTask`.
2483
- * Each blocker must already exist AND share this task's workstream
2484
- * (cross-workstream edges are forbidden); cycle check guards each
2485
- * edge. The CLI surfaces this as `--blocked-by`; the SDK key matches.
2486
- */
2487
- blockedBy?: string[];
2587
+ /** Force `status = OPEN` regardless of the current status. Without
2588
+ * this flag, `IN_PROGRESS` is also flipped to `OPEN` automatically
2589
+ * (so a released task isn't left structurally stranded with
2590
+ * `owner=NULL, status=IN_PROGRESS`); CLOSED / REJECTED / DEFERRED
2591
+ * are preserved. `--reopen` is the override for the rarer "un-
2592
+ * close and hand back to the pool" workflow. */
2593
+ reopen?: boolean;
2488
2594
  }
2489
2595
  /**
2490
- * Atomically create a task and (optionally) its incoming blocked-by
2491
- * edges.
2596
+ * Release a task: clear `tasks.owner`.
2492
2597
  *
2493
- * The task insert + every edge insert + cycle check happen inside one
2494
- * SQLite transaction. If any blocker is missing or any edge would
2495
- * create a cycle, the entire add rolls back.
2598
+ * Status side-effects (review_release_open_in_progress_inconsistency):
2599
+ * - IN_PROGRESS OPEN automatically (without it, the task is
2600
+ * stranded: no owner to drive it forward, but `mu task next`
2601
+ * skips it because it's not OPEN).
2602
+ * - OPEN / CLOSED / REJECTED / DEFERRED preserved.
2603
+ * - `--reopen` forces OPEN regardless of current status — the
2604
+ * escape hatch for un-closing a CLOSED owned task in one verb.
2496
2605
  *
2497
- * Cycle check for `addTask` is structurally trivial (a fresh task has
2498
- * no outgoing edges, so `to -> ... -> from` is impossible). It's still
2499
- * called here so the same primitive is exercised by tests.
2606
+ * Idempotent: releasing an already-unowned task with no `--reopen` and
2607
+ * no IN_PROGRESS status is a no-op (returns `changed: false`).
2608
+ * Throws TaskNotFoundError on missing.
2500
2609
  */
2501
- declare function addTask(db: Db, opts: AddTaskOptions): TaskRow;
2502
- interface AddNoteOptions {
2503
- /** Free-form author label. Convention: agent name, "user", or "orchestrator". */
2504
- author?: string;
2505
- /** Workstream context (operator-facing name). v5: tasks.local_id is
2506
- * per-workstream unique, so this is required to disambiguate. */
2610
+ declare function releaseTask(db: Db, localId: string, opts: ReleaseTaskOptions): ReleaseResult;
2611
+ interface ClaimTaskOptions extends EvidenceOption {
2612
+ /** Workstream context for both the task and the claiming agent.
2613
+ * v5: agents.name and tasks.local_id are per-workstream unique;
2614
+ * the task lookup AND the agent FK lookup scope to this
2615
+ * workstream so a same-named task or worker elsewhere can't be
2616
+ * silently picked. The CLI always passes this from the resolved
2617
+ * -w / $MU_SESSION. */
2507
2618
  workstream: string;
2619
+ /**
2620
+ * Override the agent name. If omitted, derived from the current pane's
2621
+ * title via `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`.
2622
+ *
2623
+ * Mutually exclusive with `self: true`.
2624
+ */
2625
+ agentName?: string;
2626
+ /**
2627
+ * Workstream that the claimer agent lives in. When omitted, defaults
2628
+ * to `opts.workstream` (today's same-workstream behaviour). Set by
2629
+ * the CLI when `mu task claim X -w A --for B/worker-1` qualifies the
2630
+ * `--for` ref with a different workstream prefix
2631
+ * (`task_claim_for_cross_workstream`).
2632
+ *
2633
+ * Cross-workstream ownership is structurally allowed by the schema:
2634
+ * `tasks.owner_id` is an INTEGER FK to `agents.id` with no
2635
+ * workstream qualifier on the agent side. The per-workstream UNIQUE
2636
+ * on `agents(workstream_id, name)` is what previously made the
2637
+ * SDK's name → id lookup scope to one workstream; this option
2638
+ * widens that lookup to a different workstream when the operator
2639
+ * dispatches across a workstream boundary. The agent's own
2640
+ * workstream remains unchanged — only the task's `owner_id` points
2641
+ * out-of-workstream.
2642
+ */
2643
+ agentWorkstream?: string;
2644
+ /**
2645
+ * Anonymous claim: write `owner = NULL` instead of resolving an agent
2646
+ * name and checking the FK. Use when the actor is the orchestrator
2647
+ * (or a script, or a human) doing direct work in a workstream they
2648
+ * aren't a registered worker in.
2649
+ *
2650
+ * The actor name is still recorded — it ends up in `agent_logs.source`
2651
+ * for the auto-emitted `task claim` event — so provenance is preserved.
2652
+ * Just not in the FK column.
2653
+ *
2654
+ * Resolution order for the actor name (used as the log source):
2655
+ * 1. `actor` if explicitly passed.
2656
+ * 2. Current pane title (when `$TMUX_PANE` is set).
2657
+ * 3. `$USER`.
2658
+ * 4. The literal string 'unknown'.
2659
+ *
2660
+ * Mutually exclusive with `agentName` (the two are alternative
2661
+ * answers to "who's the actor for this claim?"). Passing both is a
2662
+ * usage error.
2663
+ */
2664
+ self?: boolean;
2665
+ /**
2666
+ * Override the actor name used for the log source when `self: true`.
2667
+ * Ignored when `self: false`. Useful when the orchestrator wants to
2668
+ * attribute the work to a meaningful name rather than the pane
2669
+ * title (e.g. "deploy-bot" rather than "pi-mu").
2670
+ */
2671
+ actor?: string;
2508
2672
  }
2509
- declare function addNote(db: Db, taskLocalId: string, content: string, opts: AddNoteOptions): TaskNoteRow;
2510
- interface BlockEdgeResult {
2511
- /** True iff a row was actually inserted (vs. already present). */
2512
- added: boolean;
2513
- }
2514
- /**
2515
- * Add the edge `blocker blocked` ('blocker blocks blocked').
2516
- * Idempotent (existing edge → `added: false`). Validates:
2517
- *
2518
- * - both tasks exist
2519
- * - same workstream (cross-workstream edges forbidden)
2520
- * - no cycle (the new edge wouldn't form a path blocked → ... → blocker)
2521
- * - blocker ≠ blocked (no self-reference)
2522
- */
2523
- declare function addBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): BlockEdgeResult;
2524
- interface RemoveBlockEdgeResult {
2525
- /** True iff a row was actually deleted (vs. no such edge). */
2526
- removed: boolean;
2527
- }
2528
- /**
2529
- * Remove the edge `blocker → blocked`. Idempotent (no edge →
2530
- * `removed: false`). Does NOT validate task existence — if the
2531
- * edge is gone there's nothing to do, regardless of whether the
2532
- * tasks are gone too.
2533
- */
2534
- declare function removeBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): RemoveBlockEdgeResult;
2535
- interface DeleteTaskResult {
2536
- /** True iff the row existed and was deleted. False on a dry-run
2537
- * (preview) AND on the idempotent missing-row case. */
2538
- deleted: boolean;
2539
- /** Number of `task_edges` rows cascaded out (informational). On a
2540
- * dry-run, this is the would-be count. */
2541
- deletedEdges: number;
2542
- /** Number of `task_notes` rows cascaded out (informational). On a
2543
- * dry-run, this is the would-be count. */
2544
- deletedNotes: number;
2545
- /** True iff this was a dry-run (`opts.dryRun: true`). On a
2546
- * dry-run `deleted` is false and the counts are the would-be
2547
- * counts; the DB is unchanged. Always false on a commit / on a
2548
- * missing-row idempotent no-op. */
2549
- dryRun: boolean;
2550
- /** True iff a matching task row was found at the time of the
2551
- * call. Discriminator for the CLI: a dry-run that found nothing
2552
- * (`present: false`) renders differently from a dry-run that
2553
- * found an existing task with zero edges and zero notes
2554
- * (`present: true, deletedEdges: 0, deletedNotes: 0`). */
2555
- present: boolean;
2556
- }
2557
- interface DeleteTaskOptions {
2558
- /** When true, return the cascade preview (would-be edge / note
2559
- * counts) without mutating and without snapshotting. The CLI uses
2560
- * this to power the bare `mu task delete <id>` two-phase pattern
2561
- * (mirrors `mu workstream destroy` / `mu archive delete` /
2562
- * `mu snapshot prune`). Surfaced by feedback ws task
2563
- * fb_task_delete_no_yes (impact=30): a dogfood report typed
2564
- * `mu task delete X --yes` (mirroring workstream destroy) and got
2565
- * 'unknown option --yes' — the verb took no confirmation flag at
2566
- * all. Two failed deletes left long-named tasks lingering. */
2567
- dryRun?: boolean;
2673
+ interface ClaimResult {
2674
+ /** The agent now owning the task, or null when the claim was anonymous (--self). */
2675
+ ownerName: string | null;
2676
+ /** The actor recorded in the agent_logs event — the agent name for a
2677
+ * registered-worker claim, or the resolved actor for --self. */
2678
+ actorName: string;
2679
+ /** The previous owner (null if it was unowned). */
2680
+ previousOwnerName: string | null;
2681
+ /** The status BEFORE the claim; post-claim is IN_PROGRESS unless was CLOSED. */
2682
+ previousStatus: TaskStatus;
2683
+ /** The status AFTER the claim. */
2684
+ status: TaskStatus;
2568
2685
  }
2569
2686
  /**
2570
- * Delete a task. FK CASCADE on `task_edges` (from + to) and
2571
- * `task_notes` cleans the joined rows automatically. Idempotent on
2572
- * a missing task (returns `deleted: false`).
2573
- *
2574
- * Pre-counts the cascade victims for reporting because SQLite's
2575
- * `changes()` only reports rows directly affected by the DELETE.
2687
+ * Claim a task. Two modes:
2576
2688
  *
2577
- * With `opts.dryRun: true`, returns the would-be counts without
2578
- * touching the DB and without taking a snapshot (no mutation = no
2579
- * snapshot same reasoning that gates the closeTask snap on the
2580
- * idempotent no-op path). The CLI bare `mu task delete <id>` form
2581
- * uses this; `--yes` calls through with `dryRun: false`.
2582
- */
2583
- declare function deleteTask(db: Db, localId: string, workstream: string, opts?: DeleteTaskOptions): DeleteTaskResult;
2584
- interface UpdateTaskOptions {
2585
- title?: string;
2586
- /** 1..100; enforced by schema CHECK. */
2587
- impact?: number;
2588
- /** > 0; enforced by schema CHECK. */
2589
- effortDays?: number;
2590
- }
2591
- interface UpdateTaskResult {
2592
- /** True iff at least one field actually changed. */
2593
- updated: boolean;
2594
- /** The fields whose values differ post-update (in `UpdateTaskOptions`'s
2595
- * camelCase shape). Empty when `updated: false`. */
2596
- changedFields: string[];
2597
- }
2598
- /**
2599
- * Update scalar fields on a task. Each option is independently optional;
2600
- * passing none is a typed no-op (returns `updated: false, changedFields: []`).
2601
- * Fields whose new value equals the current value are skipped (no row change).
2689
+ * Worker claim (default):
2690
+ * Resolve an agent name from `opts.agentName` or from $TMUX_PANE's
2691
+ * pane title. The name MUST exist in the agents table (FK on
2692
+ * tasks.owner). Sets `owner = <name>`. This is what mu-spawned
2693
+ * workers do, and what `mu task claim --for <worker>` does for
2694
+ * orchestrator dispatch.
2602
2695
  *
2603
- * NOT for status (use `closeTask` / `openTask` / `setTaskStatus`), owner
2604
- * (use `claimTask` / `releaseTask`), local_id (rename is deferred), or
2605
- * workstream (cross-workstream moves are deferred).
2606
- */
2607
- interface UpdateTaskScopeOption {
2608
- workstream: string;
2609
- }
2610
- declare function updateTask(db: Db, localId: string, opts: UpdateTaskOptions, scope: UpdateTaskScopeOption): UpdateTaskResult;
2611
- interface ReparentTaskResult {
2612
- /** Edges removed (i.e. all incoming `to_task = taskId` edges). */
2613
- removedEdges: number;
2614
- /** Edges added (== blockers.length on success). */
2615
- addedEdges: number;
2616
- }
2617
- /**
2618
- * Atomically replace every incoming edge of `taskId` with new ones
2619
- * `blocker[i] → taskId`. Pass an empty `blockers` array to clear all
2620
- * incoming edges (the task becomes ready iff its status allows).
2696
+ * Anonymous claim (--self):
2697
+ * Skip the name -> agents FK lookup entirely. Sets `owner = NULL`.
2698
+ * Records the actor in `agent_logs.source` instead. This is the
2699
+ * orchestrator-doing-direct-work path — the actor is logged but
2700
+ * not registered as a worker pane.
2621
2701
  *
2622
- * Validates ALL new blockers up-front (existence + same workstream +
2623
- * cycle check); if any fails, no DELETE happens — the call is fully
2624
- * atomic via a single transaction.
2702
+ * Status side-effect: OPEN -> IN_PROGRESS; IN_PROGRESS / CLOSED unchanged.
2625
2703
  *
2626
- * Cycle reasoning: removing the existing incoming edges to `taskId`
2627
- * doesn't change `taskId`'s OUTGOING reachability, so
2628
- * `wouldCreateCycle(db, blocker, taskId)` evaluated against the
2629
- * pre-state gives the right answer for each new edge.
2704
+ * Concurrency: the worker-claim path uses a single-statement CAS UPDATE
2705
+ * with `WHERE owner IS NULL OR owner = ?` so two workers racing to
2706
+ * claim the same task can't both win. The anonymous path uses
2707
+ * `WHERE owner IS NULL` (anonymous claims don't 'own' the task in any
2708
+ * exclusive sense; if it's already owned by anyone, the anonymous claim
2709
+ * is a TaskAlreadyOwnedError just like a worker claim would be).
2630
2710
  */
2631
- declare function reparentTask(db: Db, taskLocalId: string, blockers: readonly string[], scope: {
2632
- workstream: string;
2633
- }): ReparentTaskResult;
2634
-
2635
- interface Track {
2636
- /** Goal tasks (no outgoing edges) belonging to this track. */
2637
- roots: TaskRow[];
2638
- /** Every task id reachable as a prerequisite of any root in this track. */
2639
- taskIds: ReadonlySet<string>;
2640
- /** Number of READY tasks (per the SQL view) within this track's subgraph. */
2641
- readyCount: number;
2642
- }
2711
+ declare function claimTask(db: Db, localId: string, opts: ClaimTaskOptions): Promise<ClaimResult>;
2643
2712
  /**
2644
- * Identify independent task subtrees suitable for parallel assignment
2645
- * within a workstream. Open goals only; CLOSED goals are excluded as
2646
- * they no longer represent work to schedule.
2713
+ * Resolve the current actor's identity for attribution in task notes,
2714
+ * --self claims, and any other write that wants 'who did this?'.
2647
2715
  *
2648
- * Scoping: only goals belonging to `workstream` are considered.
2649
- * Cross-workstream edges are forbidden by addTask, so a goal's
2650
- * prerequisite subgraph is naturally workstream-internal.
2716
+ * Resolution order:
2717
+ * 1. $MU_AGENT_NAME env var (set by mu spawnAgent on every managed
2718
+ * pane; surfaced from the f3d4bdd commit). Authoritative when
2719
+ * present — you're inside a mu-spawned worker, no ambiguity.
2720
+ * 2. tmux pane title (the pane-title identity step). Works
2721
+ * when running inside any pane mu manages OR adopted.
2722
+ * 3. $USER (when running outside tmux entirely).
2723
+ * 4. The literal 'orchestrator' as a last-resort default.
2724
+ *
2725
+ * Why prefer env over pane title: pane titles are a tmux-server-wide
2726
+ * resource that anything can rewrite. The env var is set per-pane at
2727
+ * spawn time and is unforgeable from outside without explicit
2728
+ * `--actor` override. Pane title is the only identity available for
2729
+ * adopted panes that didn't go through mu's spawn path.
2651
2730
  */
2652
- declare function getParallelTracks(db: Db, workstream: string): Track[];
2731
+ declare function resolveActorIdentity(): Promise<string>;
2653
2732
 
2654
- /** One per-task summary inside a per-source-ws section of the manifest. */
2655
- interface ExportTaskEntry {
2656
- /** Task local_id == filename stem (`<id>.md`). */
2657
- id: string;
2658
- /** Path relative to the bucket root (e.g. `auth/tasks/design.md`). */
2659
- path: string;
2660
- /** sha256 of the markdown body bytes; idempotency key. */
2661
- sha256: string;
2662
- /** ISO timestamp of the first observed export at which the task
2663
- * was missing from the source. Absent for tasks still present. */
2664
- deletedAt?: string;
2665
- }
2666
- /** Per-source-ws entry under `manifest.sources`. */
2667
- interface ExportSourceManifest {
2668
- /** ISO timestamp the source was first added to the bucket. */
2669
- addedAt: string;
2670
- /** ISO timestamp of the most recent re-export of this source. */
2671
- lastReExportedAt: string;
2672
- /** `latestSeq(db)` at the most recent re-export; for live workstreams
2673
- * this is the live `agent_logs.seq` cursor. For archive sources
2674
- * there is no equivalent live counter — we record the seq at
2675
- * archive-add time when available, else 0. */
2676
- eventsSeqAtExport: number;
2677
- /** Per-task entries; sorted by id for stable diffs. */
2678
- tasks: ExportTaskEntry[];
2679
- }
2680
- /** Top-level bucket manifest. `bucketVersion: 2` — the v0.3 shape.
2681
- * Manifests without `bucketVersion: 2` fall through to the
2682
- * `corrupt` lane in `readManifest`. */
2683
- interface ExportManifest {
2684
- /** Schema discriminator. Always 2 in this codebase. */
2685
- bucketVersion: 2;
2686
- /** Operator-chosen bucket label (an archive label, or null for a
2687
- * one-shot `mu workstream export`). Surfaced in README only. */
2688
- bucketLabel: string | null;
2689
- bucketCreatedAt: string;
2690
- bucketLastUpdatedAt: string;
2691
- muVersion: string;
2692
- /** Per-source-ws map; key is the source workstream's TEXT name. */
2693
- sources: Record<string, ExportSourceManifest>;
2694
- }
2695
- /** One source's worth of input: the per-task data the renderer needs.
2696
- * Both entry points (workstream / archive) collapse to this shape. */
2697
- interface ExportSource {
2698
- /** Source workstream name. Becomes the subdirectory name. */
2733
+ declare function setWaitSleepForTests(impl: ((ms: number) => Promise<void>) | undefined): (ms: number) => Promise<void>;
2734
+ /** Test seam: swap the stderr writer used by the stuck-task warning so
2735
+ * unit tests can capture warnings without spying on process.stderr. */
2736
+ declare function setWaitStuckWarnForTests(impl: ((msg: string) => void) | undefined): (msg: string) => void;
2737
+ /** Total number of polls performed across all `waitForTasks` calls in this
2738
+ * process. Tests typically reset before exercising and read after. */
2739
+ declare function getWaitPollCount(): number;
2740
+ declare function resetWaitPollCount(): void;
2741
+ /** A single task ref the wait verb is watching. Cross-workstream
2742
+ * waits arrive as a heterogeneous list of (workstream, name) pairs;
2743
+ * the legacy single-workstream call passes the same workstream on
2744
+ * every ref. task_wait_cross_workstream. */
2745
+ interface TaskWaitRef {
2746
+ /** The workstream the task lives in. Each ref carries its own so
2747
+ * the SDK doesn't need a single "the workstream" cross-ws waits
2748
+ * pass refs from multiple workstreams in one call. */
2749
+ workstreamName: string;
2750
+ /** The task's per-workstream-unique local id. */
2699
2751
  name: string;
2700
- tasks: TaskRow[];
2701
- /** Per-task edges keyed on task name. Missing keys → no edges. */
2702
- edges: Map<string, {
2703
- blockers: string[];
2704
- dependents: string[];
2705
- }>;
2706
- /** Per-task notes keyed on task name. Missing keys → no notes. */
2707
- notes: Map<string, TaskNoteRow[]>;
2708
- /** `agent_logs.seq` cursor at this source's snapshot moment. 0 for
2709
- * archive sources (no live cursor). */
2710
- eventsSeqAtExport: number;
2711
2752
  }
2712
- interface RenderBucketInput {
2713
- sources: ExportSource[];
2714
- /** Operator-chosen archive label, or null for a workstream export. */
2715
- bucketLabel: string | null;
2716
- outDir: string;
2753
+ interface TaskWaitOptions {
2754
+ /** Target status. Default 'CLOSED'. */
2755
+ status?: TaskStatus;
2756
+ /** When true, succeed as soon as ONE listed task reaches the target.
2757
+ * Default false: every listed task must reach the target. */
2758
+ any?: boolean;
2759
+ /** Maximum time to wait, in milliseconds. Default 600_000 (10 min).
2760
+ * Pass 0 to wait forever. */
2761
+ timeoutMs?: number;
2762
+ /** Polling interval. Default 1000ms; overridable for tests. */
2763
+ pollMs?: number;
2764
+ /** Workstream context applied to bare-string ids. Required when the
2765
+ * caller passes `string[]`; ignored when the caller passes
2766
+ * `TaskWaitRef[]` (each ref carries its own ws). The legacy
2767
+ * single-ws SDK call site keeps its today's shape; the cross-ws
2768
+ * callers (CLI verb) pass `TaskWaitRef[]` and omit `workstream`.
2769
+ * task_wait_cross_workstream. */
2770
+ workstream?: string;
2771
+ /** Emit a yellow STUCK warning to stderr (once per task per wait call)
2772
+ * when an IN_PROGRESS task's owner has been in `needs_input` for at
2773
+ * least this many milliseconds since the agent row's last update.
2774
+ * Default 300_000 (5 min). Pass 0 to disable.
2775
+ *
2776
+ * Surfaced by agent_close_discipline_gap in mufeedback: workers
2777
+ * occasionally finish + commit + go idle without running
2778
+ * `mu task close <id>`, leaving wait blocked indefinitely. The
2779
+ * warning is observation-only — wait keeps polling so the operator
2780
+ * (or a wrapping policy) decides whether to force-close, re-prompt,
2781
+ * or escalate. */
2782
+ stuckAfterMs?: number;
2783
+ /** What to do when the `--stuck-after` predicate fires on a watched
2784
+ * task. `'warn'` (default) = today's behaviour: yellow STUCK line
2785
+ * to stderr (deduped per task per wait call) + corroborating
2786
+ * `kind='event'` agent_logs row; wait keeps polling. `'exit'` =
2787
+ * same emit + persist, but THEN throw `StallDetectedDuringWaitError`
2788
+ * so the CLI wrapper exits 7 (STALL_DETECTED). The exit-action is
2789
+ * the unattended-orchestrator escape: a wrapping policy can branch
2790
+ * on 7 (idle, ambiguous — operator decides poke vs release) vs 6
2791
+ * (dead pane, unambiguous — re-dispatch).
2792
+ *
2793
+ * Carve-out (lives at the call site, not here): the CLI passes
2794
+ * `'exit'` only when the wait target is CLOSED — mirrors exit-6's
2795
+ * reaper-flip suppression. With `--status OPEN` the worker reaching
2796
+ * needs_input might BE the success path. See
2797
+ * task_wait_stall_action_flag. */
2798
+ onStall?: "warn" | "exit";
2799
+ /** Optional async hook run BEFORE every snapshot (initial + each
2800
+ * poll iteration). The CLI uses this to reconcile the workstream
2801
+ * each tick (reaper flips IN_PROGRESS → OPEN for dead-pane
2802
+ * workers) and to throw a typed error when a reaper-flip on a
2803
+ * watched task should abandon the wait — see
2804
+ * task_wait_reconcile_dead_panes. Throwing from `beforePoll`
2805
+ * propagates out of `waitForTasks` unchanged.
2806
+ *
2807
+ * Kept as a generic seam (not a `--reconcile`-shaped option) so
2808
+ * the SDK module stays free of tmux/reconcile imports — that
2809
+ * layering belongs above the SDK in the CLI wrapper. */
2810
+ beforePoll?: () => Promise<void>;
2811
+ }
2812
+ interface TaskWaitTaskState {
2813
+ /** The workstream this task lives in. Cross-workstream waits
2814
+ * return a mixed list; the workstream is part of identity.
2815
+ * task_wait_cross_workstream. */
2816
+ workstreamName: string;
2817
+ /** The task's per-workstream-unique name. */
2818
+ name: string;
2819
+ /** Current status (at the moment we exit). */
2820
+ status: TaskStatus;
2821
+ /** Owner at exit time (NULL when unowned, after release, or after
2822
+ * the reaper flipped IN_PROGRESS → OPEN due to a dead pane). */
2823
+ owner: string | null;
2824
+ /** True when this task's status equals the target. */
2825
+ reachedTarget: boolean;
2826
+ /** True when the task is IN_PROGRESS, owned by a registered agent
2827
+ * whose detected status is `needs_input` for >= `stuckAfterMs`.
2828
+ * Surfaces the agent_close_discipline_gap pattern: worker finished +
2829
+ * committed but skipped `mu task close <id>`. Backwards-compatible
2830
+ * signal — callers ignoring it see no behaviour change. */
2831
+ stuck: boolean;
2717
2832
  }
2718
- interface RenderBucketResult {
2719
- outDir: string;
2720
- /** Per-source-ws stat: how many task files were rewritten across
2721
- * every source in this call. */
2722
- written: number;
2723
- /** Per-source-ws stat: how many task files were sha256-skipped. */
2724
- unchanged: number;
2725
- /** Per-source-ws stat: how many task files exist for a task that
2726
- * has since vanished from the source. Banner is added once. */
2727
- preserved: number;
2728
- manifestPath: string;
2729
- manifest: ExportManifest;
2833
+ interface TaskWaitResult {
2834
+ /** Per-task state at exit time. Same length and order as the input
2835
+ * list. The caller derives all-reached / any-reached / elapsed
2836
+ * from this list (count `r.reachedTarget`) and from its own
2837
+ * startedAt clock — keeping the SDK return minimal. */
2838
+ refs: TaskWaitTaskState[];
2839
+ /** True when we exited because of the timeout, not because the wait
2840
+ * condition was met. Refs that did reach the target are still
2841
+ * reflected in `refs[i].reachedTarget` on partial-progress timeout. */
2842
+ timedOut: boolean;
2730
2843
  }
2731
2844
  /**
2732
- * Render `input.sources` to disk under `input.outDir` in the v0.3
2733
- * bucket layout. Idempotent + additive:
2734
- * - If the bucket doesn't exist, scaffold it.
2735
- * - If it does exist with bucketVersion 2, MERGE: each source in
2736
- * `input.sources` either appends (new) or refreshes (existing)
2737
- * its subdirectory; sources NOT in `input.sources` are left
2738
- * untouched.
2845
+ * Block until a set of tasks reaches `opts.status` (default CLOSED).
2846
+ * Returns a result describing the final state — the caller decides
2847
+ * whether to treat partial-progress timeouts as success or failure
2848
+ * (the CLI maps a clean exit to 0, a timeout to 5).
2739
2849
  *
2740
- * Per-task idempotency is sha256-keyed: a re-export of the same
2741
- * source against an unchanged DB rewrites zero task files. Tasks
2742
- * that disappear from a source between re-exports are preserved on
2743
- * disk with a one-time `> **Deleted from DB on <ts>**` banner.
2850
+ * Pre-flight: every task in `localIds` MUST exist; missing ones throw
2851
+ * TaskNotFoundError before any waiting begins. This is loud-fail by
2852
+ * design a typo'd id silently waiting forever is the worst-case UX.
2744
2853
  */
2745
- declare function renderToBucket(input: RenderBucketInput): RenderBucketResult;
2746
- /** Construct an ExportSource for one live workstream by reading the
2747
- * current DB. Pure data assembly; renderer does the I/O. */
2748
- declare function exportSourceForWorkstream(db: Db, workstream: string): ExportSource;
2749
- /** Construct ExportSources for every source workstream that
2750
- * contributed to an archive label. One ExportSource per
2751
- * (archive_id, source_workstream) partition. The TaskRow shapes are
2752
- * reconstructed from archived_* rows; `workstreamName` is set to
2753
- * the source workstream so the rendered frontmatter reflects the
2754
- * task's original home. */
2755
- declare function exportSourcesForArchive(db: Db, label: string): ExportSource[];
2756
- interface ExportArchiveOptions {
2757
- label: string;
2758
- /** Output directory (the bucket). Created if missing. */
2759
- outDir: string;
2854
+ declare function waitForTasks(db: Db, input: readonly TaskWaitRef[] | readonly string[], opts: TaskWaitOptions): Promise<TaskWaitResult>;
2855
+
2856
+ interface FullDag {
2857
+ /** Root tasks: no incoming `blocks` edge (no blockers). */
2858
+ roots: TaskRow[];
2859
+ /** Edges map parent task name child task names (what parent blocks). */
2860
+ edges: Map<string, string[]>;
2861
+ /** All tasks in the workstream, keyed by operator-facing name. */
2862
+ tasks: Map<string, TaskRow>;
2760
2863
  }
2761
- interface ExportArchiveResult extends RenderBucketResult {
2762
- archiveLabel: string;
2763
- /** Number of source workstreams the renderer wrote / refreshed. */
2764
- sourceCount: number;
2864
+ type TaskStatusLabelFn = (task: TaskRow) => string;
2865
+ interface RenderTreeOptions {
2866
+ /** Include the task title after the name + status label. Default: true. */
2867
+ includeTitle?: boolean;
2765
2868
  }
2766
- /** Render every source-ws in an archive to a bucket directory.
2767
- * Throws `ArchiveNotFoundError` (via listArchivedTasks) when the
2768
- * label doesn't exist. */
2769
- declare function exportArchive(db: Db, opts: ExportArchiveOptions): ExportArchiveResult;
2770
-
2771
- declare function isValidWorkstreamName(name: string): boolean;
2772
- /** Thrown by `ensureWorkstream` and `mu workstream init` when the name
2773
- * doesn't match the rules. */
2774
- declare class WorkstreamNameInvalidError extends Error implements HasNextSteps {
2775
- readonly attempted: string;
2776
- readonly name = "WorkstreamNameInvalidError";
2777
- constructor(attempted: string);
2778
- errorNextSteps(): NextStep[];
2869
+ interface LoadFullDagOptions {
2870
+ /** Optional visible-status filter. Omitted = every task status. */
2871
+ statuses?: ReadonlySet<TaskStatus>;
2779
2872
  }
2873
+ declare function loadFullDag(db: Db, workstream: string, opts?: LoadFullDagOptions): FullDag;
2780
2874
  /**
2781
- * Ensure a row exists in the `workstreams` table for `name`. Idempotent;
2782
- * INSERT OR IGNORE so concurrent callers race safely. Called by
2783
- * `insertAgent` and `addTask` so callers don't need to remember to call
2784
- * `mu init` before adding a task / spawning an agent (preserves the
2785
- * spawn-without-init ergonomics now that agents.workstream and
2786
- * tasks.workstream are real FKs into this table).
2787
- *
2788
- * Validates the name before inserting; throws `WorkstreamNameInvalidError`
2789
- * for names tmux would silently mangle (containing '.' or ':') or that
2790
- * exceed 32 chars / start with a non-letter.
2791
- *
2792
- * Returns true iff a row was actually inserted (vs. already present).
2875
+ * Render a DAG forest in the same ASCII shape as `mu task tree --down`:
2876
+ * each root is printed as a header node, dependents are below it, and
2877
+ * DAG diamonds collapse after the first full subtree render with a
2878
+ * one-line recurrence marker.
2793
2879
  */
2794
- declare function ensureWorkstream(db: Db, name: string): boolean;
2795
- interface WorkstreamSummary {
2796
- /** The workstream's own name. */
2797
- name: string;
2798
- /** Tmux session name, defaults to `mu-<name>`. */
2799
- tmuxSession: string;
2800
- /** True iff `tmux has-session -t <tmuxSession>` succeeds right now. */
2801
- tmuxAlive: boolean;
2802
- /** Rows in `agents` for this workstream. */
2803
- agentCount: number;
2804
- /** Rows in `tasks` for this workstream. */
2805
- taskCount: number;
2806
- /** Rows in `task_notes` whose task is in this workstream. */
2807
- noteCount: number;
2808
- /** Rows in `task_edges` whose `from_task` is in this workstream. */
2809
- edgeCount: number;
2810
- /** Rows in `vcs_workspaces` for this workstream. Surfaced so the
2811
- * destroy dry-run can warn about per-agent worktrees that need
2812
- * cleanup before the FK cascade silently nukes their rows. */
2813
- workspaceCount: number;
2814
- /** True iff a row exists in the `workstreams` table itself. False
2815
- * for tmux-only `mu-*` sessions that mu never observed via
2816
- * `mu workstream init`. Surfaced so destroy can clean up bare
2817
- * registry rows (workstream row exists, no agents/tasks/etc.) —
2818
- * otherwise such rows are orphaned forever (the previous
2819
- * `nothingToDo` heuristic short-circuited on them). */
2820
- registered: boolean;
2880
+ declare function renderForest(roots: readonly TaskRow[], edges: ReadonlyMap<string, readonly string[]>, statusFn: TaskStatusLabelFn, tasksByName?: ReadonlyMap<string, TaskRow>, opts?: RenderTreeOptions): string;
2881
+ declare function renderTaskTree(db: Db, workstream: string, root: TaskRow, direction: "blockers" | "dependents", statusFn: TaskStatusLabelFn, opts?: RenderTreeOptions): string;
2882
+
2883
+ declare class DbReplayWorkstreamMissingError extends Error implements HasNextSteps {
2884
+ readonly workstream: string;
2885
+ readonly name = "DbReplayWorkstreamMissingError";
2886
+ constructor(workstream: string);
2887
+ errorNextSteps(): NextStep[];
2821
2888
  }
2822
- interface DestroyResult {
2823
- /** True iff `tmux kill-session` actually killed something. */
2824
- killedTmux: boolean;
2825
- /** Number of `agents` rows deleted. */
2826
- deletedAgents: number;
2827
- /** Number of `tasks` rows deleted (edges/notes cascade via FK). */
2828
- deletedTasks: number;
2829
- /** Number of `task_notes` deleted by the cascade — informational. */
2830
- deletedNotes: number;
2831
- /** Number of `task_edges` deleted by the cascade — informational. */
2832
- deletedEdges: number;
2833
- /** Number of vcs_workspaces whose on-disk path was actually
2834
- * removed by the backend on this destroy. Excludes
2835
- * `alreadyGoneWorkspaces` (those were no-ops on disk). */
2836
- freedWorkspaces: number;
2837
- /** Number of vcs_workspaces whose registry row existed but
2838
- * whose on-disk path was already gone (manual rm -rf or a prior
2839
- * interrupted destroy). The DB row was cascade-deleted; the
2840
- * backend did no filesystem work. Tracked separately so the
2841
- * destroy report doesn't lie about how much cleanup it actually
2842
- * performed. */
2843
- alreadyGoneWorkspaces: number;
2844
- /** Workspaces whose backend cleanup failed (e.g. `git worktree
2845
- * remove` refused because of uncommitted changes). The DB row
2846
- * was still cascade-deleted; the on-disk path remains and needs
2847
- * manual cleanup. */
2848
- failedWorkspaces: WorkspaceFailure[];
2889
+ interface DbReplayTaskConflict {
2890
+ localId: string;
2891
+ local: {
2892
+ title: string;
2893
+ status: string;
2894
+ };
2895
+ sidecar: {
2896
+ title: string;
2897
+ status: string;
2898
+ };
2899
+ }
2900
+ declare class DbReplayLocalIdConflictError extends Error implements HasNextSteps {
2901
+ readonly workstream: string;
2902
+ readonly conflicts: readonly DbReplayTaskConflict[];
2903
+ readonly name = "DbReplayLocalIdConflictError";
2904
+ constructor(workstream: string, conflicts: readonly DbReplayTaskConflict[]);
2905
+ errorNextSteps(): NextStep[];
2849
2906
  }
2850
- interface WorkspaceFailure {
2851
- agent: string;
2852
- backend: string;
2853
- path: string;
2854
- error: string;
2907
+ interface ReplayDbOptions {
2908
+ apply?: boolean;
2909
+ tasks?: readonly string[];
2910
+ notes?: readonly string[];
2911
+ all?: boolean;
2855
2912
  }
2856
- interface WorkstreamOptions {
2857
- workstream: string;
2858
- /** Override the tmux session name. Defaults to `mu-<workstream>`. */
2859
- tmuxSession?: string;
2860
- /** Override the per-name VcsBackend resolver. Defaults to
2861
- * `backendByName`. Lets tests inject a fake backend (e.g. one whose
2862
- * `freeWorkspace` throws) without mutating the exported singletons —
2863
- * same pattern as `createWorkspace`'s `opts.backend` accepting a
2864
- * pre-built `VcsBackend` object. Production callers leave this
2865
- * unset. */
2866
- resolveBackend?: (name: VcsBackendName) => VcsBackend;
2913
+ interface DbReplayTaskItem {
2914
+ localId: string;
2915
+ title: string;
2916
+ status: string;
2917
+ impact: number;
2918
+ effortDays: number;
2919
+ createdAt: string;
2920
+ updatedAt: string;
2867
2921
  }
2868
- /**
2869
- * Discover every workstream visible on this machine. The union of:
2870
- * - rows in the `workstreams` table (canonical DB source; populated by
2871
- * `mu init` and auto-created by insertAgent / addTask)
2872
- * - tmux sessions named `mu-*` (with the prefix stripped) — catches
2873
- * externally-created `tmux new-session -s mu-foo` that mu hasn't
2874
- * observed yet
2875
- *
2876
- * Returns one `WorkstreamSummary` per workstream, sorted by name.
2877
- * Useful as a pre-flight before `mu init` ("is this name taken?") and
2878
- * for `mu doctor`-style diagnostics.
2879
- */
2880
- declare function listWorkstreams(db: Db): Promise<WorkstreamSummary[]>;
2881
- declare function summarizeWorkstream(db: Db, opts: WorkstreamOptions): Promise<WorkstreamSummary>;
2882
- /**
2883
- * Tear down a workstream: kill its tmux session and delete every DB row
2884
- * tagged with its name. Cascades on `tasks` clean up `task_edges` and
2885
- * `task_notes` automatically (FK ON DELETE CASCADE in the schema).
2886
- *
2887
- * Idempotent: safe to call against a workstream that never existed; safe
2888
- * to call repeatedly. Returns counts so the caller can print a useful
2889
- * summary.
2890
- */
2891
- declare function destroyWorkstream(db: Db, opts: WorkstreamOptions): Promise<DestroyResult>;
2892
- interface ExportWorkstreamOptions {
2893
- workstream: string;
2894
- /** Output directory (the bucket). Defaults to `./<workstream>/`
2895
- * in the cwd — i.e. the bucket and its single source-ws subdir
2896
- * share a name. */
2897
- outDir?: string;
2922
+ interface DbReplayNoteItem {
2923
+ taskLocalId: string;
2924
+ author: string | null;
2925
+ content: string;
2926
+ createdAt: string;
2927
+ hash: string;
2898
2928
  }
2899
- interface ExportResult {
2900
- outDir: string;
2901
- /** Per-task files rewritten this call. */
2902
- written: number;
2903
- /** Per-task files sha256-skipped this call. */
2904
- unchanged: number;
2905
- /** Tasks present in a prior manifest that are no longer in the DB.
2906
- * Their .md stays on disk; a banner is added once. */
2907
- preserved: number;
2908
- manifestPath: string;
2909
- manifest: ExportManifest;
2910
- /** Per-source-ws manifest entry for this workstream — convenience
2911
- * for callers who only want one source's view. */
2912
- source: ExportSourceManifest;
2929
+ interface DbReplayEdgeItem {
2930
+ fromLocalId: string;
2931
+ toLocalId: string;
2932
+ createdAt: string;
2913
2933
  }
2914
- /**
2915
- * Export one live workstream to a bucket directory. Idempotent +
2916
- * additive: re-exporting the same workstream is sha256-skipped,
2917
- * exporting a different workstream into the same bucket appends a
2918
- * sibling subdir.
2919
- *
2920
- */
2921
- declare function exportWorkstream(db: Db, opts: ExportWorkstreamOptions): ExportResult;
2934
+ interface DbReplayPlan {
2935
+ sourceFile: string;
2936
+ workstream: string;
2937
+ tasks: DbReplayTaskItem[];
2938
+ notes: DbReplayNoteItem[];
2939
+ edges: DbReplayEdgeItem[];
2940
+ conflicts: DbReplayTaskConflict[];
2941
+ }
2942
+ interface DbReplayResult extends DbReplayPlan {
2943
+ dryRun: boolean;
2944
+ applied: boolean;
2945
+ snapshotId?: number;
2946
+ added: {
2947
+ tasks: number;
2948
+ notes: number;
2949
+ edges: number;
2950
+ };
2951
+ warnings: string[];
2952
+ }
2953
+ declare function replayDb(db: Db, file: string, opts?: ReplayDbOptions): DbReplayResult;
2954
+ declare function buildReplayPlan(localDb: Db, sidecarDb: Db, sourceFile: string): DbReplayPlan;
2922
2955
 
2923
- declare class ImportBucketInvalidError extends Error implements HasNextSteps {
2924
- readonly bucketDir: string;
2925
- readonly reason: string;
2926
- readonly name = "ImportBucketInvalidError";
2927
- constructor(bucketDir: string, reason: string);
2956
+ interface DbExportManifestWorkstream {
2957
+ name: string;
2958
+ tasks: number;
2959
+ edges: number;
2960
+ notes: number;
2961
+ latestSeq: number;
2962
+ }
2963
+ interface DbExportManifest {
2964
+ muVersion: string;
2965
+ schemaVersion: number;
2966
+ machineId: string;
2967
+ hostname: string | null;
2968
+ exportedAt: string;
2969
+ workstreams: DbExportManifestWorkstream[];
2970
+ }
2971
+ interface ExportDbOptions {
2972
+ force?: boolean;
2973
+ }
2974
+ interface ExportDbResult {
2975
+ file: string;
2976
+ manifestPath: string;
2977
+ manifest: DbExportManifest;
2978
+ overwritten: boolean;
2979
+ }
2980
+ declare class DbExportTargetExistsError extends Error implements HasNextSteps {
2981
+ readonly file: string;
2982
+ readonly name = "DbExportTargetExistsError";
2983
+ constructor(file: string);
2928
2984
  errorNextSteps(): NextStep[];
2929
2985
  }
2930
- declare class WorkstreamAlreadyExistsError extends Error implements HasNextSteps {
2931
- readonly workstream: string;
2932
- readonly name = "WorkstreamAlreadyExistsError";
2933
- constructor(workstream: string);
2986
+ declare class DbImportManifestMissingError extends Error implements HasNextSteps {
2987
+ readonly manifestPath: string;
2988
+ readonly name = "DbImportManifestMissingError";
2989
+ constructor(manifestPath: string);
2934
2990
  errorNextSteps(): NextStep[];
2935
2991
  }
2936
- declare class ImportFrontmatterParseError extends Error implements HasNextSteps {
2937
- readonly path: string;
2938
- readonly line: number;
2939
- readonly raw: string;
2940
- readonly name = "ImportFrontmatterParseError";
2941
- constructor(path: string, line: number, raw: string);
2992
+ declare class DbImportSchemaTooOldError extends Error implements HasNextSteps {
2993
+ readonly sourceVersion: number;
2994
+ readonly name = "DbImportSchemaTooOldError";
2995
+ constructor(sourceVersion: number);
2942
2996
  errorNextSteps(): NextStep[];
2943
2997
  }
2944
- declare class ImportEdgeRefMissingError extends Error implements HasNextSteps {
2945
- readonly fromTask: string;
2946
- readonly toTask: string;
2947
- readonly direction: "blocked_by" | "blocks";
2948
- readonly name = "ImportEdgeRefMissingError";
2949
- constructor(fromTask: string, toTask: string, direction: "blocked_by" | "blocks");
2998
+ declare class DbImportSchemaTooNewError extends Error implements HasNextSteps {
2999
+ readonly sourceVersion: number;
3000
+ readonly name = "DbImportSchemaTooNewError";
3001
+ constructor(sourceVersion: number);
2950
3002
  errorNextSteps(): NextStep[];
2951
3003
  }
2952
- interface ImportBucketOptions {
2953
- bucketDir: string;
2954
- /** Rename the (single) source workstream on import. Only valid when
2955
- * the bucket has exactly one source-ws subdir (after applying any
2956
- * `sourceWs` filter); otherwise rejected with an
2957
- * ImportBucketInvalidError. */
2958
- workstreamOverride?: string;
2959
- /** Restrict the import to a subset of source-ws subdirs (by name).
2960
- * Each name must be a key in the bucket manifest's `sources` map;
2961
- * otherwise ImportSourceNotInBucketError is raised. Mutually
2962
- * exclusive with the per-source-ws-subdir invocation form (Form 1):
2963
- * passing this flag against a Form 1 path raises
2964
- * ImportBucketInvalidError. Empty array is treated as "no filter";
2965
- * the CLI rejects an explicitly-empty `--source-ws ,,`. */
2966
- sourceWs?: string[];
2967
- /** Walk + parse but write nothing to the DB. */
2968
- dryRun?: boolean;
3004
+ declare class DbImportSourceStaleError extends Error implements HasNextSteps {
3005
+ readonly workstreams: readonly string[];
3006
+ readonly name = "DbImportSourceStaleError";
3007
+ constructor(workstreams: readonly string[]);
3008
+ errorNextSteps(): NextStep[];
2969
3009
  }
2970
- interface ImportSourceResult {
2971
- workstreamName: string;
2972
- tasksImported: number;
2973
- edgesImported: number;
2974
- notesImported: number;
2975
- tombstonesSkipped: number;
2976
- /** Per-source-ws errors that did NOT roll back this source. Empty
2977
- * on success. (Sibling failures live in their own entry.) */
2978
- errors: string[];
2979
- }
2980
- interface ImportBucketResult {
2981
- bucketLabel: string | null;
2982
- bucketVersion: number;
2983
- sources: ImportSourceResult[];
3010
+ declare class DbImportConflictError extends Error implements HasNextSteps {
3011
+ readonly workstreams: readonly string[];
3012
+ readonly name = "DbImportConflictError";
3013
+ constructor(workstreams: readonly string[]);
3014
+ errorNextSteps(): NextStep[];
3015
+ }
3016
+ type DbImportDecision = "IDENTICAL" | "FAST_FORWARD" | "LOCAL_AHEAD" | "CONFLICT" | "IMPORT" | "LEAVE_ALONE";
3017
+ interface DbImportSummaryItem {
3018
+ workstream: string;
3019
+ decision: DbImportDecision;
3020
+ delta: Record<string, unknown>;
3021
+ needs?: string;
3022
+ parkPath?: string;
3023
+ }
3024
+ interface ImportDbOptions {
3025
+ apply?: boolean;
3026
+ forceSource?: boolean;
3027
+ onlyWorkstreams?: readonly string[];
3028
+ }
3029
+ interface ImportDbResult {
3030
+ machineId: string;
3031
+ sourceFile: string;
3032
+ dryRun: boolean;
3033
+ applied: boolean;
3034
+ snapshotId?: number;
3035
+ summary: DbImportSummaryItem[];
3036
+ }
3037
+ declare function exportDb(db: Db, file: string, opts?: ExportDbOptions): ExportDbResult;
3038
+ declare function importDb(db: Db, file: string, opts?: ImportDbOptions): ImportDbResult;
3039
+ declare function buildImportPlan(localDb: Db, manifest: DbExportManifest, sourceFile: string, onlyWorkstreams?: readonly string[]): DbImportSummaryItem[];
3040
+
3041
+ interface Track {
3042
+ /** Goal tasks (no outgoing edges) belonging to this track. */
3043
+ roots: TaskRow[];
3044
+ /** Every task id reachable as a prerequisite of any root in this track. */
3045
+ taskIds: ReadonlySet<string>;
3046
+ /** Number of READY tasks (per the SQL view) within this track's subgraph. */
3047
+ readyCount: number;
2984
3048
  }
2985
3049
  /**
2986
- * Import a v0.3 bucket directory back into the DB. One source-ws
2987
- * subdirectory becomes one workstream + N tasks + M edges + K notes.
2988
- * Per source-ws transactional: a failure in source A rolls back A
2989
- * but leaves source B's import committed.
3050
+ * Identify independent task subtrees suitable for parallel assignment
3051
+ * within a workstream. Open goals only; CLOSED goals are excluded as
3052
+ * they no longer represent work to schedule.
2990
3053
  *
2991
- * Throws on unrecoverable bucket-level errors (no manifest,
2992
- * --workstream override against multi-source). Per-source
2993
- * errors (frontmatter parse, edge ref, target name collision) leave
2994
- * the failing source's `errors` array populated and that source's
2995
- * counts at zero; siblings still attempt their own import.
3054
+ * Scoping: only goals belonging to `workstream` are considered.
3055
+ * Cross-workstream edges are forbidden by addTask, so a goal's
3056
+ * prerequisite subgraph is naturally workstream-internal.
2996
3057
  */
2997
- declare function importBucket(db: Db, opts: ImportBucketOptions): ImportBucketResult;
3058
+ declare function getParallelTracks(db: Db, workstream: string): Track[];
2998
3059
 
2999
- interface WorkspaceRow {
3000
- agentName: string;
3001
- workstreamName: string;
3002
- backend: VcsBackendName;
3060
+ declare const EXPORT_MANIFEST_VERSION = 2;
3061
+ /** One per-task summary inside a per-source-ws section of the manifest. */
3062
+ interface ExportTaskEntry {
3063
+ /** Task local_id == filename stem (`<id>.md`). Kept for v1 manifest compatibility. */
3064
+ id: string;
3065
+ /** Task local_id, duplicated under the operator-facing SDK name so bucket INDEX can render from manifest alone. */
3066
+ name: string;
3067
+ /** Compact summary fields needed for bucket-level INDEX.md without re-reading the DB. */
3068
+ title: string;
3069
+ status: TaskRow["status"];
3070
+ impact: number;
3071
+ effortDays: number;
3072
+ /** Path relative to the bucket root (e.g. `auth/tasks/design.md`). */
3003
3073
  path: string;
3004
- parentRef: string | null;
3005
- createdAt: string;
3006
- /** How many commits the workspace's parent_ref is behind the project's
3007
- * default branch HEAD, as of the last time the workspace's local refs
3008
- * cache was updated. Undefined when not yet computed (the listWorkspaces
3009
- * fast path leaves it unset; call decorateWithStaleness to populate).
3010
- * Null when staleness was queried but cannot be computed (no main found,
3011
- * none-backend, missing parent_ref, command failure). */
3012
- commitsBehindMain?: number | null;
3074
+ /** sha256 of the markdown body bytes; idempotency key. */
3075
+ sha256: string;
3076
+ /** ISO timestamp of the first observed export at which the task
3077
+ * was missing from the source. Absent for tasks still present. */
3078
+ deletedAt?: string;
3013
3079
  }
3014
- declare class WorkspaceExistsError extends Error implements HasNextSteps {
3015
- readonly agent: string;
3016
- readonly name = "WorkspaceExistsError";
3017
- constructor(agent: string);
3018
- errorNextSteps(): NextStep[];
3080
+ /** Per-source-ws entry under `manifest.sources`. */
3081
+ interface ExportSourceManifest {
3082
+ /** ISO timestamp the source was first added to the bucket. */
3083
+ addedAt: string;
3084
+ /** ISO timestamp of the most recent re-export of this source. */
3085
+ lastReExportedAt: string;
3086
+ /** `latestSeq(db)` at the most recent re-export; for live workstreams
3087
+ * this is the live `agent_logs.seq` cursor. For archive sources
3088
+ * there is no equivalent live counter — we record the seq at
3089
+ * archive-add time when available, else 0. */
3090
+ eventsSeqAtExport: number;
3091
+ /** Per-task entries; sorted by id for stable diffs. */
3092
+ tasks: ExportTaskEntry[];
3019
3093
  }
3020
- declare class WorkspaceNotFoundError extends Error implements HasNextSteps {
3021
- readonly agent: string;
3022
- readonly name = "WorkspaceNotFoundError";
3023
- constructor(agent: string);
3024
- errorNextSteps(): NextStep[];
3094
+ /** Top-level bucket manifest. `bucketVersion: 2` the v0.3 disk layout.
3095
+ * `manifest_version` is the schema of the manifest JSON payload itself:
3096
+ * v1 lacked task summaries, v2 stores enough per-task data to render
3097
+ * bucket INDEX.md from `manifest.sources` alone. Manifests without
3098
+ * `bucketVersion: 2` fall through to the `corrupt` lane in `readManifest`. */
3099
+ interface ExportManifest {
3100
+ /** Disk-layout discriminator. Always 2 in this codebase. */
3101
+ bucketVersion: 2;
3102
+ /** Manifest-payload discriminator. Always 2 when written by this codebase. */
3103
+ manifest_version: typeof EXPORT_MANIFEST_VERSION;
3104
+ /** Operator-chosen bucket label (an archive label, or null for a
3105
+ * one-shot `mu workstream export`). Surfaced in README only. */
3106
+ bucketLabel: string | null;
3107
+ bucketCreatedAt: string;
3108
+ bucketLastUpdatedAt: string;
3109
+ muVersion: string;
3110
+ /** Per-source-ws map; key is the source workstream's TEXT name. */
3111
+ sources: Record<string, ExportSourceManifest>;
3025
3112
  }
3026
- /**
3027
- * Thrown by createWorkspace when the on-disk path it would create is
3028
- * already occupied. Distinct from WorkspaceExistsError (which is about
3029
- * the DB row) so the recovery is clear: the dir is orphaned (no DB
3030
- * row points at it) and needs cleanup.
3031
- *
3032
- * Surfaced as a real bug from the multi-agent dogfood (mufeedback note
3033
- * #143): users hit a bare 'vcs git: workspacePath already exists' from
3034
- * the backend, with no nextSteps. After the cccba88 fix (close-refuses-
3035
- * with-workspace), this case only fires when an orphan from a previous
3036
- * mu version persists OR when the dir was manually rm-rf'd while a
3037
- * stale registration remains (the git-worktree case).
3038
- *
3039
- * Maps to exit code 4 (conflict).
3040
- */
3041
- declare class WorkspacePathNotEmptyError extends Error implements HasNextSteps {
3042
- readonly agent: string;
3043
- readonly workstream: string;
3044
- readonly workspacePath: string;
3045
- readonly name = "WorkspacePathNotEmptyError";
3046
- constructor(agent: string, workstream: string, workspacePath: string);
3047
- errorNextSteps(): NextStep[];
3113
+ /** One source's worth of input: the per-task data the renderer needs.
3114
+ * Both entry points (workstream / archive) collapse to this shape. */
3115
+ interface ExportSource {
3116
+ /** Source workstream name. Becomes the subdirectory name. */
3117
+ name: string;
3118
+ tasks: TaskRow[];
3119
+ /** Per-task edges keyed on task name. Missing keys no edges. */
3120
+ edges: Map<string, {
3121
+ blockers: string[];
3122
+ dependents: string[];
3123
+ }>;
3124
+ /** Per-task notes keyed on task name. Missing keys → no notes. */
3125
+ notes: Map<string, TaskNoteRow[]>;
3126
+ /** `agent_logs.seq` cursor at this source's snapshot moment. 0 for
3127
+ * archive sources (no live cursor). */
3128
+ eventsSeqAtExport: number;
3129
+ }
3130
+ interface RenderBucketInput {
3131
+ sources: ExportSource[];
3132
+ /** Operator-chosen archive label, or null for a workstream export. */
3133
+ bucketLabel: string | null;
3134
+ outDir: string;
3135
+ }
3136
+ interface RenderBucketResult {
3137
+ outDir: string;
3138
+ /** Per-source-ws stat: how many task files were rewritten across
3139
+ * every source in this call. */
3140
+ written: number;
3141
+ /** Per-source-ws stat: how many task files were sha256-skipped. */
3142
+ unchanged: number;
3143
+ /** Per-source-ws stat: how many task files exist for a task that
3144
+ * has since vanished from the source. Banner is added once. */
3145
+ preserved: number;
3146
+ manifestPath: string;
3147
+ manifest: ExportManifest;
3048
3148
  }
3049
3149
  /**
3050
- * Thrown by createWorkspace when the resolved projectRoot is the
3051
- * user's $HOME. Surfaced by snap_dogfood Finding 4: a `mu workspace
3052
- * create` invoked from cwd=$HOME with no --project-root began a
3053
- * recursive `cp -a` of $HOME (~/Music, ~/.config, ...) into the
3054
- * workspace dir, stalled on DRM-protected files, and on ctrl-C left
3055
- * a partial dir behind with no DB row.
3056
- *
3057
- * The guard's whole point is to make the user pick a real project
3058
- * deliberately — there's no --force escape hatch on purpose. The
3059
- * resolution is `--project-root <real-path>` (or `cd` into a real
3060
- * project first).
3150
+ * Render `input.sources` to disk under `input.outDir` in the v0.3
3151
+ * bucket layout. Idempotent + additive:
3152
+ * - If the bucket doesn't exist, scaffold it.
3153
+ * - If it does exist with bucketVersion 2, MERGE: each source in
3154
+ * `input.sources` either appends (new) or refreshes (existing)
3155
+ * its subdirectory; sources NOT in `input.sources` are left
3156
+ * untouched.
3061
3157
  *
3062
- * Maps to exit code 4 (conflict).
3158
+ * Per-task idempotency is sha256-keyed: a re-export of the same
3159
+ * source against an unchanged DB rewrites zero task files. Tasks
3160
+ * that disappear from a source between re-exports are preserved on
3161
+ * disk with a one-time `> **Deleted from DB on <ts>**` banner.
3063
3162
  */
3064
- declare class HomeDirAsProjectRootError extends Error implements HasNextSteps {
3065
- readonly agent: string;
3163
+ declare function renderToBucket(input: RenderBucketInput): RenderBucketResult;
3164
+ /** Construct an ExportSource for one live workstream by reading the
3165
+ * current DB. Pure data assembly; renderer does the I/O. */
3166
+ declare function exportSourceForWorkstream(db: Db, workstream: string): ExportSource;
3167
+ /** Construct ExportSources for every source workstream that
3168
+ * contributed to an archive label. One ExportSource per
3169
+ * (archive_id, source_workstream) partition. The TaskRow shapes are
3170
+ * reconstructed from archived_* rows; `workstreamName` is set to
3171
+ * the source workstream so the rendered frontmatter reflects the
3172
+ * task's original home. */
3173
+ declare function exportSourcesForArchive(db: Db, label: string): ExportSource[];
3174
+ interface ExportArchiveOptions {
3175
+ label: string;
3176
+ /** Output directory (the bucket). Created if missing. */
3177
+ outDir: string;
3178
+ }
3179
+ interface ExportArchiveResult extends RenderBucketResult {
3180
+ archiveLabel: string;
3181
+ /** Number of source workstreams the renderer wrote / refreshed. */
3182
+ sourceCount: number;
3183
+ }
3184
+ /** Render every source-ws in an archive to a bucket directory.
3185
+ * Throws `ArchiveNotFoundError` (via listArchivedTasks) when the
3186
+ * label doesn't exist. */
3187
+ declare function exportArchive(db: Db, opts: ExportArchiveOptions): ExportArchiveResult;
3188
+
3189
+ declare function isValidWorkstreamName(name: string): boolean;
3190
+ /** Thrown by `ensureWorkstream` and `mu workstream init` when the name
3191
+ * doesn't match the rules. */
3192
+ declare class WorkstreamExistsError extends Error implements HasNextSteps {
3066
3193
  readonly workstream: string;
3067
- readonly homeDir: string;
3068
- readonly name = "HomeDirAsProjectRootError";
3069
- constructor(agent: string, workstream: string, homeDir: string);
3194
+ readonly name: string;
3195
+ constructor(workstream: string);
3070
3196
  errorNextSteps(): NextStep[];
3071
3197
  }
3072
- /**
3073
- * Compose the canonical on-disk path for an agent's workspace. Used by
3074
- * createWorkspace and reachable from `mu workspace path` so the user
3075
- * can `cd $(mu workspace path foo)` even before the directory exists.
3076
- */
3077
- declare function workspacePath(workstream: string, agent: string): string;
3078
- /** Root dir for a workstream's workspaces — the parent of all
3079
- * per-agent workspace dirs. Used by listWorkspaceOrphans to scan
3080
- * the filesystem. */
3081
- declare function workspacesRoot(workstream: string): string;
3082
- interface WorkspaceOrphan {
3083
- /** The on-disk dir name (the agent name it WOULD be for, if mu had
3084
- * registered it). */
3085
- agentName: string;
3086
- /** Workstream the dir is filed under. */
3087
- workstreamName: string;
3088
- /** Absolute path to the orphan dir. */
3089
- path: string;
3090
- }
3091
- /**
3092
- * Like WorkspaceOrphan but additionally flags whether the parent
3093
- * workstream itself is gone (no row in `workstreams`). Returned by
3094
- * listAllOrphanWorkspaces; the per-workstream listWorkspaceOrphans
3095
- * doesn't carry this since by construction it only runs against an
3096
- * existing workstream.
3097
- */
3098
- interface StrandedWorkspaceOrphan extends WorkspaceOrphan {
3099
- /** True iff the parent workstream has no DB row (the dir was left
3100
- * behind by a `mu workstream destroy` or a manual DELETE). */
3101
- stranded: boolean;
3198
+ declare class WorkstreamNameInvalidError extends Error implements HasNextSteps {
3199
+ readonly attempted: string;
3200
+ readonly name = "WorkstreamNameInvalidError";
3201
+ constructor(attempted: string);
3202
+ errorNextSteps(): NextStep[];
3102
3203
  }
3103
3204
  /**
3104
- * Scan `<state-dir>/workspaces/<workstream>/` for directories that
3105
- * have no row in `vcs_workspaces`. These are the result of:
3106
- * - pre-cccba88 agents closed without --discard-workspace
3107
- * - failed spawn rollbacks (pre-bug_agent_spawn_workspace_fk_failure fix)
3108
- * - manual cleanup that left the dir but not the row
3109
- * - any case where the operator manually rm-rf'd vcs_workspaces rows
3110
- *
3111
- * Returns `[]` when the workstream's workspaces dir doesn't exist,
3112
- * or when every dir on disk has a corresponding DB row. Filesystem
3113
- * read is best-effort: a missing/inaccessible dir returns `[]`
3114
- * (caller doesn't have to check existsSync first).
3115
- *
3116
- * Surfaced by bug_workspace_orphan_not_in_state: orphan dirs were
3117
- * invisible to `mu state` and `mu workspace list`, but blocked
3118
- * subsequent `--workspace` spawns with WorkspacePathNotEmptyError.
3119
- */
3120
- declare function listWorkspaceOrphans(db: Db, workstream: string): WorkspaceOrphan[];
3121
- /**
3122
- * Cross-workstream variant of listWorkspaceOrphans. Reads
3123
- * `<state-dir>/workspaces/`, recurses one level (per-ws subdir →
3124
- * per-agent subdir), and surfaces every dir with no row in
3125
- * `vcs_workspaces`.
3205
+ * Ensure a row exists in the `workstreams` table for `name`. Idempotent;
3206
+ * INSERT OR IGNORE so concurrent callers race safely. Called by
3207
+ * `insertAgent` and `addTask` so callers don't need to remember to call
3208
+ * `mu init` before adding a task / spawning an agent (preserves the
3209
+ * spawn-without-init ergonomics now that agents.workstream and
3210
+ * tasks.workstream are real FKs into this table).
3126
3211
  *
3127
- * Each entry is additionally tagged with `stranded: boolean`: true
3128
- * when the parent workstream has no row in `workstreams`. Stranded
3129
- * orphans are the failure mode this verb was added for — workstreams
3130
- * destroyed before the close-refuses-with-workspace fix landed (or
3131
- * via `mu sql DELETE FROM workstreams ...`) would leave their entire
3132
- * workspace subtree invisible to `mu workspace orphans -w <ws>`,
3133
- * because the user couldn't know to ask for the right name.
3212
+ * Validates the name before inserting; throws `WorkstreamNameInvalidError`
3213
+ * for names tmux would silently mangle (containing '.' or ':') or that
3214
+ * exceed 32 chars / start with a non-letter.
3134
3215
  *
3135
- * Surfaced by workspace_orphans_misses_destroyed_workstreams. Returns
3136
- * `[]` when the workspaces root itself doesn't exist; otherwise scans
3137
- * best-effort and skips any subdir that fails to read.
3216
+ * Returns true iff a row was actually inserted (vs. already present).
3138
3217
  */
3139
- declare function listAllOrphanWorkspaces(db: Db): StrandedWorkspaceOrphan[];
3140
- interface CreateWorkspaceOptions {
3218
+ declare function ensureWorkstream(db: Db, name: string): boolean;
3219
+ interface WorkstreamSummary {
3220
+ /** The workstream's own name. */
3221
+ name: string;
3222
+ /** Tmux session name, defaults to `mu-<name>`. */
3223
+ tmuxSession: string;
3224
+ /** True iff `tmux has-session -t <tmuxSession>` succeeds right now. */
3225
+ tmuxAlive: boolean;
3226
+ /** Rows in `agents` for this workstream. */
3227
+ agentCount: number;
3228
+ /** Rows in `tasks` for this workstream. */
3229
+ taskCount: number;
3230
+ /** Rows in `task_notes` whose task is in this workstream. */
3231
+ noteCount: number;
3232
+ /** Rows in `task_edges` whose `from_task` is in this workstream. */
3233
+ edgeCount: number;
3234
+ /** Rows in `vcs_workspaces` for this workstream. Surfaced so the
3235
+ * destroy dry-run can warn about per-agent worktrees that need
3236
+ * cleanup before the FK cascade silently nukes their rows. */
3237
+ workspaceCount: number;
3238
+ /** True iff a row exists in the `workstreams` table itself. False
3239
+ * for tmux-only `mu-*` sessions that mu never observed via
3240
+ * `mu workstream init`. Surfaced so destroy can clean up bare
3241
+ * registry rows (workstream row exists, no agents/tasks/etc.) —
3242
+ * otherwise such rows are orphaned forever (the previous
3243
+ * `nothingToDo` heuristic short-circuited on them). */
3244
+ registered: boolean;
3245
+ }
3246
+ interface DestroyResult {
3247
+ /** True iff `tmux kill-session` actually killed something. */
3248
+ killedTmux: boolean;
3249
+ /** Number of `agents` rows deleted. */
3250
+ deletedAgents: number;
3251
+ /** Number of `tasks` rows deleted (edges/notes cascade via FK). */
3252
+ deletedTasks: number;
3253
+ /** Number of `task_notes` deleted by the cascade — informational. */
3254
+ deletedNotes: number;
3255
+ /** Number of `task_edges` deleted by the cascade — informational. */
3256
+ deletedEdges: number;
3257
+ /** Number of vcs_workspaces whose on-disk path was actually
3258
+ * removed by the backend on this destroy. Excludes
3259
+ * `alreadyGoneWorkspaces` (those were no-ops on disk). */
3260
+ freedWorkspaces: number;
3261
+ /** Number of vcs_workspaces whose registry row existed but
3262
+ * whose on-disk path was already gone (manual rm -rf or a prior
3263
+ * interrupted destroy). The DB row was cascade-deleted; the
3264
+ * backend did no filesystem work. Tracked separately so the
3265
+ * destroy report doesn't lie about how much cleanup it actually
3266
+ * performed. */
3267
+ alreadyGoneWorkspaces: number;
3268
+ /** Workspaces whose backend cleanup failed (e.g. `git worktree
3269
+ * remove` refused because of uncommitted changes). The DB row
3270
+ * was still cascade-deleted; the on-disk path remains and needs
3271
+ * manual cleanup. */
3272
+ failedWorkspaces: WorkspaceFailure[];
3273
+ }
3274
+ interface WorkspaceFailure {
3141
3275
  agent: string;
3276
+ backend: string;
3277
+ path: string;
3278
+ error: string;
3279
+ }
3280
+ interface WorkstreamOptions {
3142
3281
  workstream: string;
3143
- /** Project root to branch from. Defaults to the current working
3144
- * directory (the `mu` invocation site, which is normally what the
3145
- * user wants). */
3146
- projectRoot?: string;
3147
- /** Override backend detection. Default: walk `detectBackend`.
3148
- * Accepts either a name ("jj" / "sl" / "git" / "none") OR a
3149
- * pre-built `VcsBackend` object the object form lets tests inject
3150
- * a fresh fake backend without mutating the exported singletons. */
3151
- backend?: VcsBackendName | VcsBackend;
3152
- /** Optional ref to base the workspace on. Backend-specific. */
3153
- parentRef?: string;
3154
- /** INTERNAL. When false, suppress the `workspace create` system
3155
- * event. Used by `recreateWorkspace` so the audit trail records
3156
- * ONE atomic `workspace recreate` line instead of separate
3157
- * free + create entries. Defaults to true. */
3158
- _suppressEvent?: boolean;
3282
+ /** Override the tmux session name. Defaults to `mu-<workstream>`. */
3283
+ tmuxSession?: string;
3284
+ /** Override the per-name VcsBackend resolver. Defaults to
3285
+ * `backendByName`. Lets tests inject a fake backend (e.g. one whose
3286
+ * `freeWorkspace` throws) without mutating the exported singletons —
3287
+ * same pattern as `createWorkspace`'s `opts.backend` accepting a
3288
+ * pre-built `VcsBackend` object. Production callers leave this
3289
+ * unset. */
3290
+ resolveBackend?: (name: VcsBackendName) => VcsBackend;
3291
+ }
3292
+ interface DestroyWorkstreamOptions extends WorkstreamOptions {
3293
+ /** Skip the per-workstream pre-mutation snapshot because the caller
3294
+ * already captured a broader snapshot for the whole destructive
3295
+ * operation. Used by `mu workstream destroy --empty` after its
3296
+ * sweep-level safety snapshot; direct destroy callers leave this
3297
+ * false so the default safety net is unchanged. */
3298
+ suppressSnapshot?: boolean;
3159
3299
  }
3160
3300
  /**
3161
- * Create a fresh workspace for an agent. Allocates the on-disk
3162
- * directory, records the row, emits a system event. Idempotent ONLY
3163
- * to the extent that the row check is up-front; if the row exists
3164
- * we throw `WorkspaceExistsError` rather than silently re-using a
3165
- * possibly-stale on-disk state. Callers should `freeWorkspace` first.
3301
+ * Discover every workstream visible on this machine. The union of:
3302
+ * - rows in the `workstreams` table (canonical DB source; populated by
3303
+ * `mu init` and auto-created by insertAgent / addTask)
3304
+ * - tmux sessions named `mu-*` (with the prefix stripped) — catches
3305
+ * externally-created `tmux new-session -s mu-foo` that mu hasn't
3306
+ * observed yet
3307
+ *
3308
+ * Returns one `WorkstreamSummary` per workstream, sorted by name.
3309
+ * Useful as a pre-flight before `mu init` ("is this name taken?") and
3310
+ * for `mu doctor`-style diagnostics.
3166
3311
  */
3167
- declare function createWorkspace(db: Db, opts: CreateWorkspaceOptions): Promise<WorkspaceRow>;
3168
- declare function getWorkspaceForAgent(db: Db, agent: string, workstream: string): WorkspaceRow | undefined;
3169
- declare function listWorkspaces(db: Db, workstream?: string): WorkspaceRow[];
3170
- declare function decorateWithStaleness(rows: readonly WorkspaceRow[]): Promise<WorkspaceRow[]>;
3171
- interface FreeWorkspaceOptions {
3172
- /** If true, attempt to commit pending changes before tearing down.
3173
- * Backend-specific; see VcsBackend.freeWorkspace. */
3174
- commit?: boolean;
3175
- /** INTERNAL. When false, suppress the `workspace free` system
3176
- * event AND skip the pre-mutation snapshot capture. Used by
3177
- * `recreateWorkspace` so the audit trail records ONE atomic
3178
- * `workspace recreate` line and one snapshot for the whole
3179
- * free+create cycle. Defaults to true. */
3180
- _suppressEvent?: boolean;
3181
- }
3182
- interface FreeWorkspaceResult {
3183
- /** The committed ref, when `commit` was true and there was something
3184
- * to commit. */
3185
- committedRef?: string;
3186
- /** True iff the on-disk path was actually removed. */
3187
- removed: boolean;
3188
- /** True iff the DB row was actually deleted. */
3189
- rowDeleted: boolean;
3190
- }
3312
+ declare function listWorkstreams(db: Db): Promise<WorkstreamSummary[]>;
3313
+ declare function summarizeWorkstream(db: Db, opts: WorkstreamOptions): Promise<WorkstreamSummary>;
3191
3314
  /**
3192
- * Tear down an agent's workspace. Calls the backend to remove the
3193
- * on-disk directory (with optional auto-commit), then DELETEs the row.
3194
- * Idempotent on a missing workspace (returns all-false).
3315
+ * Tear down a workstream: kill its tmux session and delete every DB row
3316
+ * tagged with its name. Cascades on `tasks` clean up `task_edges` and
3317
+ * `task_notes` automatically (FK ON DELETE CASCADE in the schema).
3318
+ *
3319
+ * Idempotent: safe to call against a workstream that never existed; safe
3320
+ * to call repeatedly. Returns counts so the caller can print a useful
3321
+ * summary.
3195
3322
  */
3196
- declare function freeWorkspace(db: Db, agent: string, opts: FreeWorkspaceOptions & {
3323
+ declare function destroyWorkstream(db: Db, opts: DestroyWorkstreamOptions): Promise<DestroyResult>;
3324
+ interface ExportWorkstreamOptions {
3197
3325
  workstream: string;
3198
- }): Promise<FreeWorkspaceResult>;
3199
- interface RecreateWorkspaceOptions {
3200
- /** Same as createWorkspace; defaults to cwd. */
3201
- projectRoot?: string;
3202
- /** Same as createWorkspace; if undefined the previous backend is
3203
- * reused (auto-detection re-runs only when --backend was passed). */
3204
- backend?: VcsBackendName | VcsBackend;
3205
- /** Same as createWorkspace; if undefined the new workspace bases on
3206
- * the backend's current head (for git/jj/sl: the project's main),
3207
- * which is the whole point of the verb. */
3208
- parentRef?: string;
3209
- /** When true, skip the dirty-check refusal and discard any
3210
- * uncommitted changes in the existing workspace. The lossy escape
3211
- * hatch — mirrors the implicit semantics of `mu workspace free`
3212
- * without --commit. */
3213
- force?: boolean;
3326
+ /** Output directory (the bucket). Defaults to `./<workstream>/`
3327
+ * in the cwd — i.e. the bucket and its single source-ws subdir
3328
+ * share a name. */
3329
+ outDir?: string;
3214
3330
  }
3215
- interface RecreateWorkspaceResult {
3216
- /** The freshly-created workspace row (the previous row is already
3217
- * gone by the time we return). */
3218
- workspace: WorkspaceRow;
3219
- /** parent_ref of the WORKSPACE BEFORE recreate, so callers (and the
3220
- * CLI's success message) can show "bumped from <old> -> <new>". */
3221
- previousParentRef: string | null;
3331
+ interface ExportResult {
3332
+ outDir: string;
3333
+ /** Per-task files rewritten this call. */
3334
+ written: number;
3335
+ /** Per-task files sha256-skipped this call. */
3336
+ unchanged: number;
3337
+ /** Tasks present in a prior manifest that are no longer in the DB.
3338
+ * Their .md stays on disk; a banner is added once. */
3339
+ preserved: number;
3340
+ manifestPath: string;
3341
+ manifest: ExportManifest;
3342
+ /** Per-source-ws manifest entry for this workstream — convenience
3343
+ * for callers who only want one source's view. */
3344
+ source: ExportSourceManifest;
3222
3345
  }
3223
3346
  /**
3224
- * Free + create in one atomic-ish verb. The whole point: between
3225
- * waves the operator wants the SAME agent name with a fresh workspace
3226
- * pinned to current main; doing `free` then `create` manually was the
3227
- * dogfood-painful pattern (mufeedback note add_mu_workspace_recreate_free_create).
3228
- *
3229
- * Behaviour:
3230
- * - Refuses with WorkspaceDirtyError if the existing workspace has
3231
- * uncommitted changes (git/sl), UNLESS `force: true` is passed
3232
- * (lossy escape hatch). jj is always-snapshotted so dirty is never
3233
- * an issue; `none` has no VCS to consult so it never refuses.
3234
- * - Reuses the SAME backend the previous workspace had unless
3235
- * `backend` is explicitly overridden — a between-wave refresh
3236
- * should not silently swap from git to none because the operator
3237
- * happened to cd into a non-VCS dir. The override path matches
3238
- * `mu workspace create --backend ...` semantics.
3239
- * - Emits ONE `workspace recreate <agent>` event (with both old and
3240
- * new parent_ref in the payload) instead of separate free + create
3241
- * events. One pre-mutation snapshot is captured under the same
3242
- * label so the undo trail shows one step.
3243
- *
3244
- * Throws:
3245
- * - WorkspaceNotFoundError — no row for this (agent, workstream).
3246
- * - AgentNotFoundError — propagated from createWorkspace's
3247
- * typed agent check (the agent row
3248
- * was deleted between the lookup and
3249
- * the re-INSERT; vanishingly rare).
3250
- * - WorkspaceDirtyError — dirty + !force.
3251
- * - any backend-level Error — free or create failed.
3252
- *
3253
- * On a free-side failure the row + on-disk dir are best-effort gone;
3254
- * on a create-side failure we surface the create error and the row is
3255
- * already deleted (the operator can re-run `mu workspace create`).
3347
+ * Export one live workstream to a bucket directory. Idempotent +
3348
+ * additive: re-exporting the same workstream is sha256-skipped,
3349
+ * exporting a different workstream into the same bucket appends a
3350
+ * sibling subdir.
3351
+ *
3256
3352
  */
3257
- declare function recreateWorkspace(db: Db, agent: string, opts: RecreateWorkspaceOptions & {
3258
- workstream: string;
3259
- }): Promise<RecreateWorkspaceResult>;
3353
+ declare function exportWorkstream(db: Db, opts: ExportWorkstreamOptions): ExportResult;
3260
3354
 
3261
3355
  type LogKind = "message" | "event" | "broadcast" | string;
3262
3356
  interface LogRow {
@@ -3314,7 +3408,7 @@ declare function listLogs(db: Db, opts?: ListLogsOptions): LogRow[];
3314
3408
  * by `mu log --tail` to start the cursor at "now" so the subscriber
3315
3409
  * only sees NEW entries unless they explicitly pass `--since 0`.
3316
3410
  */
3317
- declare function latestSeq(db: Db): number;
3411
+ declare function latestSeq(db: Db, workstreamId?: number): number;
3318
3412
  /**
3319
3413
  * One-line helper for state-changing SDK functions to auto-emit a
3320
3414
  * `kind='event'` log entry. Called AFTER the mutation succeeds, only
@@ -3325,6 +3419,213 @@ declare function latestSeq(db: Db): number;
3325
3419
  * by callers like `claimTask` (source = the claiming agent).
3326
3420
  */
3327
3421
  declare function emitEvent(db: Db, workstream: string | null, payload: string, source?: string): void;
3422
+ /**
3423
+ * Canonical list of two-token verb prefixes that `emitEvent` callers
3424
+ * use as the leading words of a payload. Single source of truth for
3425
+ * event renderers so they can never drift away from the actual emitter
3426
+ * sites.
3427
+ *
3428
+ * Maintenance contract: when you add an `emitEvent(...)` call whose
3429
+ * payload starts with a new two-word verb, add the verb here. A
3430
+ * regression test walks every entry and asserts the classifier
3431
+ * recognises it; the test fails if you add an emitter without adding
3432
+ * its verb here.
3433
+ *
3434
+ * Audit (2026-05): every `emitEvent` callsite under src/ produces a
3435
+ * payload that starts with one of these. Verified by
3436
+ * `grep -rn emitEvent src/ | grep -v import`.
3437
+ */
3438
+ declare const EVENT_VERB_PREFIXES: readonly string[];
3439
+ interface ClassifiedEvent {
3440
+ /** One of EVENT_VERB_PREFIXES. */
3441
+ verb: string;
3442
+ /** Payload past the verb token; preserves leading separator (" " or "\t"). */
3443
+ rest: string;
3444
+ }
3445
+ /**
3446
+ * Match `payload` against EVENT_VERB_PREFIXES. Returns {verb, rest} on
3447
+ * match; null otherwise. The verb-boundary check is `next is space, tab,
3448
+ * or end-of-string` so we don't false-match e.g. `task addnote`.
3449
+ *
3450
+ * Pure parser. Consumers (the static state card, the ink Activity-log
3451
+ * card) apply their own colour to `verb` after matching.
3452
+ */
3453
+ declare function classifyEventVerb(payload: string): ClassifiedEvent | null;
3454
+
3455
+ type DoctorStatus = "ok" | "warn" | "fail";
3456
+ interface DoctorCheck {
3457
+ /** Short, stable identifier — used as the row label. Lowercase
3458
+ * one-word tokens so the column-aligned card layout looks tidy. */
3459
+ name: string;
3460
+ status: DoctorStatus;
3461
+ /** Free-form prose for the row's right-hand column. Kept short so
3462
+ * the card's CLIP column doesn't truncate it on common widths. */
3463
+ detail: string;
3464
+ }
3465
+ interface DoctorSummary {
3466
+ /** Every check that ran, in stable display order. The card filters
3467
+ * to non-OK rows for its body but keeps the OK rows so the popup
3468
+ * (when it ships under feat_more_cards_umbrella) can render the
3469
+ * full list. */
3470
+ checks: readonly DoctorCheck[];
3471
+ /** Convenience: how many rows are warn or fail. Card subtitle
3472
+ * reads this directly. Pure derivation from `checks`. */
3473
+ problemCount: number;
3474
+ }
3475
+ /**
3476
+ * Compute the doctor summary for a workstream. Pure-ish: runs cheap
3477
+ * synchronous DB queries + reads from the supplied snapshot. Callers
3478
+ * that don't want to compute a snapshot first (or are running inside
3479
+ * `loadWorkstreamSnapshot` mid-build) can omit `snapshot` — the
3480
+ * snapshot-derived checks (ghosts / orphans / workspace-orphans) are
3481
+ * skipped in that case.
3482
+ */
3483
+ declare function loadDoctorSummary(db: Db, snapshot: WorkstreamSnapshot | null): DoctorSummary;
3484
+ /** Count of warn + fail rows. Pure; exported for unit tests. */
3485
+ declare function countProblems(checks: readonly DoctorCheck[]): number;
3486
+ /**
3487
+ * Return the full check array (OK + warn + fail) in stable display
3488
+ * order. Used by the TUI's slot-9 Doctor popup
3489
+ * (feat_popup_9_doctor, workstream `tui-impl`) which renders every
3490
+ * row — not just the non-OK subset Card 9 surfaces.
3491
+ *
3492
+ * Thin wrapper over `loadDoctorSummary` so the SDK seam stays
3493
+ * single: `loadDoctorSummary` is the source of truth for the
3494
+ * check vocabulary, and the popup's `loadDoctorChecks` view is
3495
+ * just `.checks`. Pure-ish (same cheap synchronous DB reads as
3496
+ * `loadDoctorSummary`).
3497
+ */
3498
+ declare function loadDoctorChecks(db: Db, snapshot: WorkstreamSnapshot | null): readonly DoctorCheck[];
3499
+ /**
3500
+ * Map a check row to the most useful informational command the
3501
+ * operator might paste. Read-only by construction: `mu agent list`,
3502
+ * `mu workspace orphans`, `mu doctor` are all SELECT-shape verbs;
3503
+ * `# ...` lines are visibly inert. Per the slot-9 popup spec KEY
3504
+ * MAP block this is INFORMATIONAL, never a mutating recipe — so
3505
+ * even when the check is `fail`, we yank the diagnostic verb the
3506
+ * operator should RUN MANUALLY, not a fix command.
3507
+ *
3508
+ * Pure; exported for unit tests + SDK reuse.
3509
+ */
3510
+ declare function yankCommandForCheck(check: Pick<DoctorCheck, "name" | "status">): string;
3511
+ /**
3512
+ * A short paragraph (one paragraph per check name) explaining the
3513
+ * shape of the failure / warning. Returned as a `readonly string[]`
3514
+ * so the popup's drill body can interleave the lines with other
3515
+ * content; CLI consumers can `.join("\n")` themselves.
3516
+ *
3517
+ * Pure; exported for unit tests + SDK reuse.
3518
+ */
3519
+ declare function remediationParagraph(check: DoctorCheck): readonly string[];
3520
+
3521
+ interface WorkstreamSnapshot {
3522
+ workstreamName: string;
3523
+ view: LiveAgentsView;
3524
+ tracks: Track[];
3525
+ ready: TaskRow[];
3526
+ inProgress: TaskRow[];
3527
+ blocked: TaskRow[];
3528
+ recentClosed: TaskRow[];
3529
+ /** Populated only when callers explicitly pass `withAllTasks: true`.
3530
+ * The TUI dashboard fast tick leaves this empty and the all-tasks
3531
+ * popup reads its exhaustive list directly from SQLite while open. */
3532
+ allTasks: TaskRow[];
3533
+ workspaces: WorkspaceRow[];
3534
+ workspaceOrphans: WorkspaceOrphan[];
3535
+ recent: LogRow[];
3536
+ /** Last N commits from the project root (process.cwd()), populated
3537
+ * when `loadWorkstreamSnapshot` is called with withRecentCommits.
3538
+ * This is intentionally NOT a per-agent workspace log. */
3539
+ recentCommits: CommitSummary[];
3540
+ /** Backend that produced recentCommits. Null when recent commits were
3541
+ * not requested or no VCS backend was detected. */
3542
+ commitsBackend?: VcsBackendName | null;
3543
+ /** Populated when `loadWorkstreamSnapshot` is called with
3544
+ * `withDoctor: true`. Used by the TUI's slot-9 Doctor card to
3545
+ * render a glanceable health badge on the dashboard
3546
+ * (feat_card_9_doctor, workstream `tui-impl`). The static `mu
3547
+ * state` card and `mu doctor` itself don't consume it — they
3548
+ * read the textual doctor card directly. Null when not requested. */
3549
+ doctor: DoctorSummary | null;
3550
+ }
3551
+ interface LoadWorkstreamSnapshotOptions {
3552
+ /** Recent-events cap (default 200). */
3553
+ eventLimit?: number;
3554
+ /** When true, slow snapshot loading also populates `WorkspaceRow.dirty`
3555
+ * via decorateWithDirty (one `git status --porcelain` shellout per row,
3556
+ * capped at DECORATE_CONCURRENCY). The TUI caches this slow-tier value
3557
+ * and merges it into every fast SQL tick. */
3558
+ withDirty?: boolean;
3559
+ /** When true, slow snapshot loading also populates
3560
+ * `WorkstreamSnapshot.doctor` via `loadDoctorSummary`. The summary is
3561
+ * cheap SQL, but it reports tmux/workspace drift from slow-tier fields,
3562
+ * so the TUI refreshes it with the subprocess tier. */
3563
+ withDoctor?: boolean;
3564
+ /** Optional full task list for the TUI all-tasks popup. */
3565
+ withAllTasks?: true;
3566
+ /** Optional recent-project-commits slice for the TUI Commits card /
3567
+ * popup. Uses process.cwd() as the project root on purpose: the TUI
3568
+ * is launched from the project checkout, while worker workspaces live
3569
+ * elsewhere under the mu state dir. */
3570
+ withRecentCommits?: {
3571
+ limit: number;
3572
+ };
3573
+ }
3574
+ interface WorkstreamSnapshotSlowFields {
3575
+ view: LiveAgentsView;
3576
+ /** Workspace rows decorated with slow-tier VCS observations
3577
+ * (`commitsBehindMain`, and `dirty` when requested). */
3578
+ workspaces: WorkspaceRow[];
3579
+ recentCommits: CommitSummary[];
3580
+ commitsBackend?: VcsBackendName | null;
3581
+ doctor: DoctorSummary | null;
3582
+ }
3583
+ /**
3584
+ * Fast TUI/state snapshot tier: pure SQLite reads only. Subprocess-backed
3585
+ * fields are intentionally empty placeholders so callers can merge the last
3586
+ * slow-tier values without blocking a 1s render tick on tmux or VCS probes.
3587
+ */
3588
+ declare function loadWorkstreamSnapshotFast(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions): Promise<WorkstreamSnapshot>;
3589
+ /**
3590
+ * Slow snapshot tier: fields backed by tmux / VCS subprocess probes (plus
3591
+ * doctor, which reports over those slow-tier observations). Returns only the
3592
+ * fields the fast snapshot deliberately leaves empty or undecorated.
3593
+ *
3594
+ * status-only refresh: don't prune mid-spawn placeholders or reap
3595
+ * unreachable agents — every render-mode is a polling read surface.
3596
+ */
3597
+ declare function loadWorkstreamSnapshotSlow(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions, baseSnapshot?: WorkstreamSnapshot): Promise<WorkstreamSnapshotSlowFields>;
3598
+ /** Merge the latest slow-tier subprocess observations into a fresh fast tier. */
3599
+ declare function mergeSnapshotFastSlow(fast: WorkstreamSnapshot, slow: WorkstreamSnapshotSlowFields | null): WorkstreamSnapshot;
3600
+ /**
3601
+ * Back-compat wrapper for non-TUI callers: return the historical union shape
3602
+ * by composing the new fast SQL tier with one slow subprocess tier.
3603
+ */
3604
+ declare function loadWorkstreamSnapshot(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions): Promise<WorkstreamSnapshot>;
3605
+ /**
3606
+ * ROI tiers used to colour task rows. Pure: returns the bucket name; the
3607
+ * consumer maps bucket → picocolors function (or ink text colour).
3608
+ * Magic numbers (≥100 high, ≥50 mid) lifted from the previous HUD impl.
3609
+ */
3610
+ type RoiBucket = "high" | "mid" | "low" | "infinite";
3611
+ declare function roiBucket(impact: number, effortDays: number): RoiBucket;
3612
+ /** Histogram of agents by status. Pure derivation (no colour render). */
3613
+ declare function agentStatusHistogram(agents: readonly AgentRow[]): ReadonlyMap<AgentStatus, number>;
3614
+ interface OwnedTasksSummary {
3615
+ /** Display token: "—" (none) | "<task_id>" (one) | "⊕<N>" (many). */
3616
+ bit: string;
3617
+ /** Underlying count for callers that want their own format. */
3618
+ count: number;
3619
+ /** The single owned task's local id, when count===1. */
3620
+ onlyTaskId?: string;
3621
+ }
3622
+ /**
3623
+ * Per-agent task summary: condensed display token + raw count. Used by
3624
+ * both the static Agents table and the ink Agents card. Pure on the
3625
+ * input rows — caller (e.g. loadWorkstreamSnapshot consumer) does the
3626
+ * listTasksByOwner query upstream and feeds the rows in.
3627
+ */
3628
+ declare function summarizeOwnedTasks(owned: readonly TaskRow[]): OwnedTasksSummary;
3328
3629
 
3329
3630
  interface SnapshotRow {
3330
3631
  /** Operator-facing snapshot id. EXCEPTION to the no-surrogate-ids rule:
@@ -3368,13 +3669,6 @@ declare class SnapshotNotFoundError extends Error implements HasNextSteps {
3368
3669
  constructor(snapshotId: number);
3369
3670
  errorNextSteps(): NextStep[];
3370
3671
  }
3371
- /**
3372
- * Thrown by restoreSnapshot when the snapshot's schema_version doesn't
3373
- * match the live DB's CURRENT_SCHEMA_VERSION. Maps to exit code 4
3374
- * (conflict). Auto-migration of snapshot files was deliberately rejected
3375
- * in snap_design note #293 (mutates forensic data; migrations are
3376
- * forward-only).
3377
- */
3378
3672
  declare class SnapshotVersionMismatchError extends Error implements HasNextSteps {
3379
3673
  readonly snapshotId: number;
3380
3674
  readonly snapshotVersion: number;
@@ -3383,11 +3677,6 @@ declare class SnapshotVersionMismatchError extends Error implements HasNextSteps
3383
3677
  constructor(snapshotId: number, snapshotVersion: number, currentVersion: number);
3384
3678
  errorNextSteps(): NextStep[];
3385
3679
  }
3386
- /**
3387
- * Thrown when the snapshot's .db file has been removed from disk (manual
3388
- * cleanup, fs corruption) but the row still exists. Maps to exit code 3
3389
- * (not found).
3390
- */
3391
3680
  declare class SnapshotFileMissingError extends Error implements HasNextSteps {
3392
3681
  readonly snapshotId: number;
3393
3682
  readonly dbPath: string;
@@ -3395,169 +3684,47 @@ declare class SnapshotFileMissingError extends Error implements HasNextSteps {
3395
3684
  constructor(snapshotId: number, dbPath: string);
3396
3685
  errorNextSteps(): NextStep[];
3397
3686
  }
3398
- /** Read the operator-tunable count cap (`MU_SNAPSHOT_KEEP_LAST`). */
3399
3687
  declare function gcMaxCount(): number;
3400
- /** Read the operator-tunable age cap (`MU_SNAPSHOT_MAX_AGE_DAYS`). */
3401
3688
  declare function gcMaxAgeDays(): number;
3402
- /**
3403
- * Resolve the snapshots directory.
3404
- *
3405
- * If a live `Db` handle is supplied, snapshots land under
3406
- * `<dirname(db-path)>/snapshots/` — colocated with the DB they back.
3407
- * This keeps snapshots discoverable for non-default DB paths
3408
- * (`MU_DB_PATH=/some/place/foo.db` users) AND keeps tests that use
3409
- * temp-dir DBs from polluting the user's `~/.local/state/mu/`.
3410
- *
3411
- * Without a Db handle, falls back to `<state-dir>/snapshots/` (the
3412
- * canonical default per snap_design §WHERE).
3413
- *
3414
- * Flat (not per-workstream) by design: workstream-destroy snapshots
3415
- * span every workstream so subdirs would lie about scope.
3416
- */
3417
3689
  declare function snapshotsDir(db?: Db): string;
3418
- /**
3419
- * Take a whole-DB snapshot before a destructive verb mutates state.
3420
- *
3421
- * Steps:
3422
- * 1. INSERT a row to claim an id.
3423
- * 2. VACUUM INTO <state-dir>/snapshots/<id>.db. Synchronous; runs
3424
- * page-level on the live DB without extra locks beyond SQLite's
3425
- * existing busy_timeout.
3426
- * 3. UPDATE the row with the canonical db_path (we couldn't know it
3427
- * before step 1 because id is AUTOINCREMENT).
3428
- * 4. Run opportunistic GC.
3429
- *
3430
- * If VACUUM INTO fails (disk full, perms, race), the row is rolled back
3431
- * so the DB never points at a non-existent file. The original verb's
3432
- * exception path still surfaces the underlying error.
3433
- *
3434
- * Idempotent on a same-instant double-call (each call gets its own id).
3435
- */
3690
+ declare function isStaleVersion(row: {
3691
+ schemaVersion: number;
3692
+ }): boolean;
3693
+ declare function snapshotFileSize(snapshot: SnapshotRow): number | null;
3694
+
3436
3695
  declare function captureSnapshot(db: Db, label: string, workstream?: string | null): CaptureSnapshotResult;
3437
- /**
3438
- * List snapshots, newest first. When `workstream` is set, returns rows
3439
- * for that workstream PLUS rows with workstream = NULL (workstream-
3440
- * destroy snapshots span every workstream so excluding them would hide
3441
- * the most-recent restorable point during recovery).
3442
- */
3443
3696
  declare function listSnapshots(db: Db, opts?: ListSnapshotsOptions): SnapshotRow[];
3444
- /**
3445
- * Restore a snapshot by file-swapping its .db onto the live DB path.
3446
- *
3447
- * Caller contract: pass the live `Db` handle so we can read the live DB
3448
- * path, the snapshot row, and emit a pre-restore self-snapshot for the
3449
- * "undo of undo" case (snap_design §EDGE CASES > snapshot-of-snapshot).
3450
- *
3451
- * The caller is expected to be a short-lived `mu undo` process: this
3452
- * function CLOSES `db` after taking the pre-restore snapshot, then
3453
- * fs.copyFileSync's the snapshot file onto the live DB path and unlinks
3454
- * any -wal / -shm sidecars. Any other live mu process holding the DB
3455
- * will see SQLITE_BUSY / disk-image-malformed on next write and exit
3456
- * cleanly (snap_design recommends gating the verb behind --yes for
3457
- * exactly this reason; that's snap_undo_verb's surface, not ours).
3458
- */
3459
- declare function restoreSnapshot(db: Db, snapshotId: number): RestoreSnapshotResult;
3460
- /**
3461
- * Drop snapshots that are EITHER past the count cap OR past the age
3462
- * cap — "whichever cap is more permissive wins" (snap_design §GC).
3463
- * Concretely: keep the N most recent AND keep everything <D days old;
3464
- * delete the rest (and their on-disk .db files).
3465
- *
3466
- * The caps come from `gcMaxCount()` / `gcMaxAgeDays()` (env-tunable
3467
- * via `MU_SNAPSHOT_KEEP_LAST` / `MU_SNAPSHOT_MAX_AGE_DAYS`).
3468
- *
3469
- * NOTE: prior to snapshot_gc_caps_too_lax_no_cleanup_verb the WHERE
3470
- * was `created_at < cutoff AND id NOT IN protected`, i.e. "delete
3471
- * only if BOTH old AND past the count cap". That made the count cap
3472
- * effectively dead under bursty use (every row was younger than the
3473
- * 14-day age cap, so the date filter spared everything regardless of
3474
- * row count). The fix flips AND→OR.
3475
- *
3476
- * Best-effort on file unlink: if a file is already gone, the row goes
3477
- * anyway (the user's intent — "this snapshot is gone" — is satisfied).
3478
- */
3479
3697
  declare function gcSnapshots(db: Db): {
3480
3698
  deletedRows: number;
3481
3699
  deletedFiles: number;
3482
3700
  };
3701
+
3483
3702
  type PruneMode = "gc" | "keep-last" | "older-than" | "stale-version" | "all";
3484
3703
  interface PruneOptions {
3485
3704
  mode: PruneMode;
3486
- /** For mode='keep-last'. Required by the CLI. */
3487
3705
  keepLast?: number;
3488
- /** For mode='older-than'. Days; required by the CLI. */
3489
3706
  olderThanDays?: number;
3490
- /** When true, return the would-delete shape but don't touch the DB
3491
- * or the on-disk .db files. */
3492
3707
  dryRun?: boolean;
3493
3708
  }
3494
3709
  interface PruneResult {
3495
- /** Rows that would be / were deleted. Always populated, even on
3496
- * dry-run (the CLI's summary uses it). */
3497
3710
  victims: SnapshotRow[];
3498
- /** Total bytes that would be / were freed (sum of victim file
3499
- * sizes; missing files contribute 0). */
3500
3711
  freedBytes: number;
3501
- /** Number of `snapshots` rows actually deleted. 0 on dry-run. */
3502
3712
  deletedRows: number;
3503
- /** Number of on-disk .db files actually unlinked. 0 on dry-run. */
3504
3713
  deletedFiles: number;
3505
- /** Set when mode='all' and dryRun=false: id of the safety-net
3506
- * snapshot captured BEFORE the wipe. (Survives the wipe.) */
3507
3714
  safetyNetSnapshotId?: number;
3508
3715
  }
3509
3716
  declare class PruneOptionsInvalidError extends Error implements HasNextSteps {
3510
3717
  readonly name = "PruneOptionsInvalidError";
3511
3718
  errorNextSteps(): NextStep[];
3512
3719
  }
3513
- /** True if a snapshot row's schema_version doesn't match the live DB's
3514
- * CURRENT_SCHEMA_VERSION. Stale snapshots are unrestorable (restore
3515
- * raises SnapshotVersionMismatchError) — surfaced dimmed in
3516
- * `mu snapshot list` and as the target set of `prune --stale-version`. */
3517
- declare function isStaleVersion(row: {
3518
- schemaVersion: number;
3519
- }): boolean;
3520
- /**
3521
- * Bulk policy-driven cleanup. The CLI's `mu snapshot prune` verb is
3522
- * a thin wrapper. Modes:
3523
- *
3524
- * gc — apply the auto-GC policy explicitly (same as the
3525
- * opportunistic call inside captureSnapshot).
3526
- * keep-last — keep only the N newest rows.
3527
- * older-than — drop rows whose created_at is older than D days.
3528
- * stale-version — drop rows whose schema_version != current.
3529
- * all — drop EVERY row. dryRun=false additionally captures
3530
- * a safety-net snapshot of the live DB FIRST, so a
3531
- * subsequent `mu undo` can recover; the safety-net
3532
- * row survives the wipe.
3533
- *
3534
- * On dryRun=true: returns the victim set + freed-bytes total without
3535
- * touching the DB or the filesystem.
3536
- */
3537
3720
  declare function pruneSnapshots(db: Db, opts: PruneOptions): PruneResult;
3538
3721
  interface DeleteSnapshotResult {
3539
- /** Always true on success. (Misses raise SnapshotNotFoundError; the
3540
- * shape mirrors `deleteTask`'s structured-result style.) */
3541
3722
  deleted: true;
3542
- /** 1 if the .db file was on disk + unlinked; 0 if it was already
3543
- * gone (orphaned row). */
3544
3723
  deletedFiles: 0 | 1;
3545
- /** Bytes freed by unlinking the .db file. 0 when the file was
3546
- * already gone. */
3547
3724
  freedBytes: number;
3548
3725
  }
3549
- /**
3550
- * Surgical removal of one snapshot: drop the `snapshots` row + unlink
3551
- * the on-disk .db file. Mirrors `mu task delete`. Errors with
3552
- * `SnapshotNotFoundError` on miss.
3553
- *
3554
- * No auto-snapshot before the delete: the point IS to delete one row,
3555
- * and removing one stepping-stone can't break `mu undo` (it still has
3556
- * every other snapshot). Auto-snapshotting here would be circular.
3557
- */
3558
3726
  declare function deleteSnapshot(db: Db, snapshotId: number): DeleteSnapshotResult;
3559
- /** Return the on-disk size of the snapshot file in bytes, or null if
3560
- * the file is missing. Useful for `mu snapshot list --json` output. */
3561
- declare function snapshotFileSize(snapshot: SnapshotRow): number | null;
3562
3727
 
3563
- export { type AddNoteOptions, type AddTaskOptions, type AddToArchiveResult, type AdoptAgentOptions, type AdoptAgentResult, AgentDiedOnSpawnError, AgentExistsError, AgentNotFoundError, AgentNotInWorkstreamError, type AgentRow, AgentSpawnCliNotFoundError, AgentSpawnStartupError, 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, type CommandResolutionResult, type CommandResolver, 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, type ImportSourceResult, type InsertAgentInput, type KickAgentOptions, type KickAgentResult, type KickProcessExecutor, type KickSignal, type ListArchivedTasksOptions, type ListLiveAgentsOptions, type ListLogsOptions, type ListNotesOptions, type ListReadyOptions, type ListSnapshotsOptions, type ListTasksOptions, type LiveAgentsView, type LogKind, type LogRow, type NewSessionOptions, type NewWindowOptions, NoForegroundProcessError, 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 RecreateWorkspaceOptions as WorkspaceRecreateOptions, type RecreateWorkspaceResult as WorkspaceRecreateResult, type WorkspaceRow, WorkstreamAlreadyExistsError, WorkstreamNameInvalidError, type WorkstreamOptions, type WorkstreamSummary, addBlockEdge, addNote, addTask, addToArchive, adoptAgent, appendLog, assertValidPaneId, backendByName, capturePane, captureSnapshot, checkCommandResolvable, claimTask, closeAgent, closeTask, composeAgentTitle, createArchive, createWorkspace, currentAgentName, currentPaneTitle, decorateWithStaleness, defaultDbPath, defaultSendDelayMs, defaultSpawnLivenessMs, defaultStateDir, deferTask, deleteAgent, deleteArchive, deleteSnapshot, deleteTask, destroyWorkstream, detectBackend, detectPiStatus, emitEvent, ensureWorkstream, ensureWorkstreamStateDir, envVarNameForCli, exportArchive, exportSourceForWorkstream, exportSourcesForArchive, exportWorkstream, extractTail, foregroundPgid, freeAgent, freeWorkspace, gcMaxAgeDays, gcMaxCount, gcSnapshots, getAgent, getAgentByPane, getArchive, getParallelTracks, getPrerequisites, getTask, getTaskEdges, getTaskEdgesWithStatus, getWaitPollCount, getWorkspaceForAgent, gitBackend, idFromTitle, idFromTitleVerbose, importBucket, insertAgent, isKickSignal, isStaleVersion, isTaskStatus, isValidAgentName, isValidArchiveLabel, isValidPaneId, isValidTaskId, isValidWorkstreamName, jjBackend, kickAgent, 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, paneTTY, parseAgentNameFromTitle, parsePsTtyOutput, pruneSnapshots, readAgent, reconcile, recreateWorkspace, refreshAgentTitle, rejectTask, releaseTask, removeBlockEdge, removeFromArchive, renderToBucket, reparentTask, resetCommandResolverForTests, resetKickProcessExecutor, resetSleep, resetTmuxExecutor, resetWaitPollCount, resolveActorIdentity, resolveCliCommand, resolveCliCommandWithSource, restoreSnapshot, searchArchives, searchTasks, selectLayout, sendToAgent, sendToPane, sessionExists, setCommandResolverForTests, setKickProcessExecutor, 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 };
3728
+ declare function restoreSnapshot(db: Db, snapshotId: number): RestoreSnapshotResult;
3729
+
3730
+ export { type AddNoteOptions, type AddTaskOptions, type AddToArchiveResult, type AdoptAgentOptions, type AdoptAgentResult, AgentDiedOnSpawnError, AgentExistsError, AgentNotFoundError, AgentNotInWorkstreamError, type AgentRow, AgentSpawnCliNotFoundError, AgentSpawnStartupError, type AgentStatus, type AppendLogOptions, type Archive, ArchiveAlreadyExistsError, ArchiveLabelInvalidError, ArchiveNotFoundError, type ArchiveSearchHit, ArchiveSourceAmbiguousError, type ArchiveSourceSummary, type ArchiveSummary, type ArchivedTaskRow, type BlockEdgeResult, CURRENT_SCHEMA_VERSION, type CaptureOptions, type CaptureSnapshotResult, type ClaimResult, type ClaimTaskOptions, ClaimerNotRegisteredError, type ClassifiedEvent, type CloseAgentOptions, type CloseAgentResult, type CommandResolutionResult, type CommandResolver, CrossWorkstreamEdgeError, CycleError, type Db, type DbExportManifest, type DbExportManifestWorkstream, DbExportTargetExistsError, DbImportConflictError, type DbImportDecision, DbImportManifestMissingError, DbImportSchemaTooNewError, DbImportSchemaTooOldError, DbImportSourceStaleError, type DbImportSummaryItem, type DbReplayEdgeItem, DbReplayLocalIdConflictError, type DbReplayNoteItem, type DbReplayPlan, type DbReplayResult, type DbReplayTaskConflict, type DbReplayTaskItem, DbReplayWorkstreamMissingError, type DeleteSnapshotResult, type DeleteTaskResult, type DestroyResult, type DetectedStatus, type DoctorCheck, type DoctorStatus, type DoctorSummary, EVENT_VERB_PREFIXES, EXPECTED_TABLES, type EvidenceOption, type ExportArchiveOptions, type ExportArchiveResult, type ExportDbOptions, type ExportDbResult, type ExportManifest, type ExportResult, type ExportSource, type ExportSourceManifest, type ExportTaskEntry, type ExportWorkstreamOptions, type FreeAgentResult, type FullDag, HomeDirAsProjectRootError, type IdFromTitleResult, type ImportDbOptions, type ImportDbResult, type InsertAgentInput, type KickAgentOptions, type KickAgentResult, type KickProcessExecutor, type KickSignal, type ListArchivedTasksOptions, type ListLiveAgentsOptions, type ListLogsOptions, type ListNotesOptions, type ListReadyOptions, type ListSnapshotsOptions, type ListTasksOptions, type LiveAgentsView, type LoadFullDagOptions, type LoadWorkstreamSnapshotOptions, type LogKind, type LogRow, type NewSessionOptions, type NewWindowOptions, NoForegroundProcessError, type OpenDbOptions, type OwnedTasksSummary, 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 ReplayDbOptions, type RestoreArchiveOptions, type RestoreArchiveResult, type RestoreSnapshotResult, type RoiBucket, 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, WORKSPACE_STALE_THRESHOLD, type CreateWorkspaceOptions as WorkspaceCreateOptions, WorkspaceExistsError, type WorkspaceFailure, type FreeWorkspaceOptions as WorkspaceFreeOptions, type FreeWorkspaceResult as WorkspaceFreeResult, WorkspaceNotFoundError, type WorkspaceOrphan, WorkspacePathNotEmptyError, WorkspacePreservedError, type RecreateWorkspaceOptions as WorkspaceRecreateOptions, type RecreateWorkspaceResult as WorkspaceRecreateResult, type WorkspaceRow, type WorkspaceStaleness, WorkstreamExistsError, WorkstreamNameInvalidError, type WorkstreamOptions, type WorkstreamSnapshot, type WorkstreamSnapshotSlowFields, type WorkstreamSummary, addBlockEdge, addNote, addTask, addToArchive, adoptAgent, agentStatusHistogram, appendLog, assertValidPaneId, backendByName, buildImportPlan, buildReplayPlan, capturePane, captureSnapshot, checkCommandResolvable, claimTask, classifyEventVerb, closeAgent, closeTask, composeAgentTitle, countProblems, createArchive, createWorkspace, currentAgentName, currentPaneTitle, decorateWithDirty, decorateWithStaleness, defaultDbPath, defaultSendDelayMs, defaultSpawnLivenessMs, defaultStateDir, deferTask, deleteAgent, deleteArchive, deleteSnapshot, deleteTask, destroyWorkstream, detectBackend, detectPiStatus, emitEvent, ensureWorkstream, envVarNameForCli, exportArchive, exportDb, exportSourceForWorkstream, exportSourcesForArchive, exportWorkstream, extractTail, foregroundPgid, freeAgent, freeWorkspace, gcMaxAgeDays, gcMaxCount, gcSnapshots, getAgent, getAgentByPane, getArchive, getParallelTracks, getPrerequisites, getTask, getTaskEdges, getTaskEdgesWithStatus, getWaitPollCount, getWorkspaceForAgent, getWorkspaceStaleness, gitBackend, idFromTitle, idFromTitleVerbose, importDb, insertAgent, isKickSignal, isStaleVersion, isTaskStatus, isValidAgentName, isValidArchiveLabel, isValidPaneId, isValidTaskId, isValidWorkstreamName, isWorkspaceStale, jjBackend, kickAgent, 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, loadDoctorChecks, loadDoctorSummary, loadFullDag, loadWorkstreamSnapshot, loadWorkstreamSnapshotFast, loadWorkstreamSnapshotSlow, mergeSnapshotFastSlow, newSession, newSessionWithPane, newWindow, noneBackend, openDb, openTask, paneExists, paneTTY, parseAgentNameFromTitle, parsePsTtyOutput, pruneSnapshots, readAgent, reconcile, recreateWorkspace, refreshAgentTitle, rejectTask, releaseTask, remediationParagraph, removeBlockEdge, removeFromArchive, renderForest, renderTaskTree, renderToBucket, reparentTask, replayDb, resetCommandResolverForTests, resetKickProcessExecutor, resetSleep, resetTmuxExecutor, resetWaitPollCount, resolveActorIdentity, resolveCliCommand, resolveCliCommandWithSource, restoreArchive, restoreSnapshot, roiBucket, searchArchives, searchTasks, selectLayout, sendToAgent, sendToPane, sessionExists, setCommandResolverForTests, setKickProcessExecutor, setPaneTitle, setSleepForTests, setTaskStatus, setTmuxExecutor, setWaitSleepForTests, setWaitStuckWarnForTests, slBackend, sleep, slugifyTitle, slugifyTitleVerbose, snapshotFileSize, snapshotsDir, spawnAgent, splitWindow, summarizeOwnedTasks, summarizeWorkstream, tmux$1 as tmux, updateAgentStatus, updateTask, waitForTasks, workspacePath, workspacesRoot, yankCommandForCheck };