@martintrojer/mu 0.3.2 → 0.4.0
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/AGENTS.md +181 -42
- package/README.md +63 -22
- package/dist/cli.js +13348 -5236
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1428 -1369
- package/dist/index.js +4002 -3110
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +296 -56
- package/docs/HANDOVER.md +461 -0
- package/docs/ROADMAP.md +120 -498
- package/docs/USAGE_GUIDE.md +445 -143
- package/docs/VISION.md +48 -4
- package/docs/VOCABULARY.md +18 -5
- package/docs/img/tui-dashboard.png +0 -0
- package/package.json +12 -6
- package/skills/mu/SKILL.md +273 -469
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,8 +62,6 @@ 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
65
|
/** The schema version a fresh DB starts at. v7 drops the
|
|
77
66
|
* `approvals` table on top of v6 (which added 5 archive_* tables
|
|
78
67
|
* on top of v5's surrogate-PK substrate; docs/ARCHITECTURE.md §
|
|
@@ -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
|
-
/**
|
|
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
|
|
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$
|
|
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
|
|
499
|
-
* `mu
|
|
500
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
*
|
|
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
|
|
1563
|
+
declare function addToArchive(db: Db, label: string, workstream: string): AddToArchiveResult;
|
|
1561
1564
|
/**
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
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
|
|
1601
|
-
*
|
|
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
|
|
1590
|
+
declare function createArchive(db: Db, label: string, description?: string): Archive;
|
|
1604
1591
|
/**
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
* (
|
|
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
|
|
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,17 @@ interface ArchiveSearchHit {
|
|
|
1641
1624
|
matchSnippet: string;
|
|
1642
1625
|
}
|
|
1643
1626
|
interface SearchArchivesOptions {
|
|
1644
|
-
/** LIKE-style needle. Wrapped in `%…%` automatically
|
|
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.
|
|
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[];
|
|
1682
1638
|
|
|
1683
1639
|
type TaskStatus = "OPEN" | "IN_PROGRESS" | "CLOSED" | "REJECTED" | "DEFERRED";
|
|
1684
1640
|
/** Every legal task status, in canonical order (matches the schema
|
|
@@ -1702,935 +1658,1207 @@ declare function isTaskStatus(s: string): s is TaskStatus;
|
|
|
1702
1658
|
* doesn't leave stale lists rotting in the CLI surface. */
|
|
1703
1659
|
declare const TASK_STATUS_LIST: string;
|
|
1704
1660
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1661
|
+
interface TaskRow {
|
|
1662
|
+
/** Per-workstream-unique TEXT name. The operator-facing identifier. */
|
|
1663
|
+
name: string;
|
|
1664
|
+
/** Foreign-name reference to the owning workstream. */
|
|
1665
|
+
workstreamName: string;
|
|
1666
|
+
title: string;
|
|
1667
|
+
status: TaskStatus;
|
|
1668
|
+
impact: number;
|
|
1669
|
+
effortDays: number;
|
|
1670
|
+
/** Foreign-name reference to the owning agent (NULL when unowned). */
|
|
1671
|
+
ownerName: string | null;
|
|
1672
|
+
createdAt: string;
|
|
1673
|
+
updatedAt: string;
|
|
1710
1674
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1675
|
+
interface TaskNoteRow {
|
|
1676
|
+
author: string | null;
|
|
1677
|
+
content: string;
|
|
1678
|
+
createdAt: string;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
interface TaskEdges {
|
|
1682
|
+
/** Tasks that must close before this one can start (blockers). */
|
|
1683
|
+
blockers: string[];
|
|
1684
|
+
/** Tasks that this one blocks (dependents). */
|
|
1685
|
+
dependents: string[];
|
|
1686
|
+
}
|
|
1687
|
+
/** One end of an edge with the neighbour's current status attached.
|
|
1688
|
+
* Used by `mu task show` to group blockers/dependents into
|
|
1689
|
+
* "still gating" vs "satisfied" buckets without making the renderer
|
|
1690
|
+
* do a second round-trip to the DB per neighbour. */
|
|
1691
|
+
interface TaskEdgeWithStatus {
|
|
1692
|
+
name: string;
|
|
1693
|
+
status: TaskStatus;
|
|
1694
|
+
}
|
|
1695
|
+
interface TaskEdgesWithStatus {
|
|
1696
|
+
/** Tasks that must close before this one can start (blockers),
|
|
1697
|
+
* carrying each blocker's current status. */
|
|
1698
|
+
blockers: TaskEdgeWithStatus[];
|
|
1699
|
+
/** Tasks that this one blocks (dependents), carrying each
|
|
1700
|
+
* dependent's current status. */
|
|
1701
|
+
dependents: TaskEdgeWithStatus[];
|
|
1716
1702
|
}
|
|
1717
1703
|
/**
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
* (which surfaces as `TaskNotFoundError`). Maps to exit code 4
|
|
1722
|
-
* (conflict / wrong scope).
|
|
1704
|
+
* Direct (one-hop) edges for a task. For transitive prerequisites, use
|
|
1705
|
+
* `getPrerequisites()`; this helper is the immediate-neighbour view used
|
|
1706
|
+
* by `mu task show`.
|
|
1723
1707
|
*/
|
|
1724
|
-
declare
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1708
|
+
declare function getTaskEdges(db: Db, taskLocalId: string, workstream: string): TaskEdges;
|
|
1709
|
+
/**
|
|
1710
|
+
* Same one-hop edge view as `getTaskEdges`, but each neighbour is
|
|
1711
|
+
* returned as `{ name, status }` so callers can group / colour by
|
|
1712
|
+
* status without an N+1 round-trip. Used by `mu task show` to split
|
|
1713
|
+
* "blocked by" (still-gating) from "satisfied" (already-CLOSED)
|
|
1714
|
+
* blockers, and the symmetric split on the dependents side
|
|
1715
|
+
* (task_show_blocked_by_renders_closed). The status is the neighbour's
|
|
1716
|
+
* full TaskStatus, not just OPEN/CLOSED — REJECTED/DEFERRED still
|
|
1717
|
+
* gate downstream work, so the renderer keeps them in the
|
|
1718
|
+
* still-gating bucket.
|
|
1719
|
+
*/
|
|
1720
|
+
declare function getTaskEdgesWithStatus(db: Db, taskLocalId: string, workstream: string): TaskEdgesWithStatus;
|
|
1721
|
+
/**
|
|
1722
|
+
* All tasks transitively reachable from `taskId` via reverse-edge
|
|
1723
|
+
* traversal (i.e. the set of tasks that block this one), including the
|
|
1724
|
+
* task itself.
|
|
1725
|
+
*/
|
|
1726
|
+
declare function getPrerequisites(db: Db, taskLocalId: string, workstream: string): Set<string>;
|
|
1727
|
+
interface BlockEdgeResult {
|
|
1728
|
+
/** True iff a row was actually inserted (vs. already present). */
|
|
1729
|
+
added: boolean;
|
|
1731
1730
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1731
|
+
/**
|
|
1732
|
+
* Add the edge `blocker → blocked` ('blocker blocks blocked').
|
|
1733
|
+
* Idempotent (existing edge → `added: false`). Validates:
|
|
1734
|
+
*
|
|
1735
|
+
* - both tasks exist
|
|
1736
|
+
* - same workstream (cross-workstream edges forbidden)
|
|
1737
|
+
* - no cycle (the new edge wouldn't form a path blocked → ... → blocker)
|
|
1738
|
+
* - blocker ≠ blocked (no self-reference)
|
|
1739
|
+
*/
|
|
1740
|
+
declare function addBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): BlockEdgeResult;
|
|
1741
|
+
interface RemoveBlockEdgeResult {
|
|
1742
|
+
/** True iff a row was actually deleted (vs. no such edge). */
|
|
1743
|
+
removed: boolean;
|
|
1738
1744
|
}
|
|
1739
1745
|
/**
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1742
|
-
*
|
|
1743
|
-
*
|
|
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.
|
|
1746
|
+
* Remove the edge `blocker → blocked`. Idempotent (no edge →
|
|
1747
|
+
* `removed: false`). Does NOT validate task existence — if the
|
|
1748
|
+
* edge is gone there's nothing to do, regardless of whether the
|
|
1749
|
+
* tasks are gone too.
|
|
1748
1750
|
*/
|
|
1749
|
-
declare
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
errorNextSteps(): NextStep[];
|
|
1751
|
+
declare function removeBlockEdge(db: Db, workstream: string, blocked: string, blocker: string): RemoveBlockEdgeResult;
|
|
1752
|
+
interface ReparentTaskResult {
|
|
1753
|
+
/** Edges removed (i.e. all incoming `to_task = taskId` edges). */
|
|
1754
|
+
removedEdges: number;
|
|
1755
|
+
/** Edges added (after duplicate blockers are canonicalised). */
|
|
1756
|
+
addedEdges: number;
|
|
1756
1757
|
}
|
|
1757
1758
|
/**
|
|
1758
|
-
*
|
|
1759
|
-
*
|
|
1759
|
+
* Atomically replace every incoming edge of `taskId` with new ones
|
|
1760
|
+
* `blocker[i] → taskId`. Pass an empty `blockers` array to clear all
|
|
1761
|
+
* incoming edges (the task becomes ready iff its status allows).
|
|
1760
1762
|
*
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
* steps (run `mu agent adopt <pane-id>` to register, or use --for to pick a
|
|
1765
|
-
* different agent).
|
|
1763
|
+
* Validates ALL new blockers up-front (existence + same workstream +
|
|
1764
|
+
* cycle check); if any fails, no DELETE happens — the call is fully
|
|
1765
|
+
* atomic via a single transaction.
|
|
1766
1766
|
*
|
|
1767
|
-
*
|
|
1767
|
+
* Cycle reasoning: removing the existing incoming edges to `taskId`
|
|
1768
|
+
* doesn't change `taskId`'s OUTGOING reachability, so
|
|
1769
|
+
* `wouldCreateCycle(db, blocker, taskId)` evaluated against the
|
|
1770
|
+
* pre-state gives the right answer for each new edge.
|
|
1768
1771
|
*/
|
|
1769
|
-
declare
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1772
|
+
declare function reparentTask(db: Db, taskLocalId: string, blockers: readonly string[], scope: {
|
|
1773
|
+
workstream: string;
|
|
1774
|
+
}): ReparentTaskResult;
|
|
1775
|
+
|
|
1776
|
+
interface AddTaskOptions {
|
|
1777
|
+
localId: string;
|
|
1778
|
+
workstream: string;
|
|
1779
|
+
title: string;
|
|
1780
|
+
/** 1..100; enforced by schema CHECK. */
|
|
1781
|
+
impact: number;
|
|
1782
|
+
/** > 0; enforced by schema CHECK. */
|
|
1783
|
+
effortDays: number;
|
|
1774
1784
|
/**
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1785
|
+
* Tasks that block this one. Edges inserted as `blocker -> newTask`.
|
|
1786
|
+
* Each blocker must already exist AND share this task's workstream
|
|
1787
|
+
* (cross-workstream edges are forbidden); cycle check guards each
|
|
1788
|
+
* edge. The CLI surfaces this as `--blocked-by`; the SDK key matches.
|
|
1779
1789
|
*/
|
|
1780
|
-
|
|
1790
|
+
blockedBy?: string[];
|
|
1781
1791
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
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;
|
|
1818
|
-
}
|
|
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>;
|
|
1792
|
+
/**
|
|
1793
|
+
* Atomically create a task and (optionally) its incoming blocked-by
|
|
1794
|
+
* edges.
|
|
1795
|
+
*
|
|
1796
|
+
* The task insert + every edge insert + cycle check happen inside one
|
|
1797
|
+
* SQLite transaction. If any blocker is missing or any edge would
|
|
1798
|
+
* create a cycle, the entire add rolls back.
|
|
1799
|
+
*
|
|
1800
|
+
* Cycle check for `addTask` is structurally trivial (a fresh task has
|
|
1801
|
+
* no outgoing edges, so `to -> ... -> from` is impossible). It's still
|
|
1802
|
+
* called here so the same primitive is exercised by tests.
|
|
1803
|
+
*/
|
|
1804
|
+
declare function addTask(db: Db, opts: AddTaskOptions): TaskRow;
|
|
1805
|
+
interface AddNoteOptions {
|
|
1806
|
+
/** Free-form author label. Convention: agent name, "user", or "orchestrator". */
|
|
1807
|
+
author?: string;
|
|
1808
|
+
/** Workstream context (operator-facing name). v5: tasks.local_id is
|
|
1809
|
+
* per-workstream unique, so this is required to disambiguate. */
|
|
1810
|
+
workstream: string;
|
|
1877
1811
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
/**
|
|
1888
|
-
*
|
|
1889
|
-
|
|
1890
|
-
/**
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
*
|
|
1895
|
-
*
|
|
1896
|
-
*
|
|
1897
|
-
|
|
1812
|
+
declare function addNote(db: Db, taskLocalId: string, content: string, opts: AddNoteOptions): {
|
|
1813
|
+
author: string | null;
|
|
1814
|
+
content: string;
|
|
1815
|
+
createdAt: string;
|
|
1816
|
+
};
|
|
1817
|
+
interface DeleteTaskResult {
|
|
1818
|
+
/** True iff the row existed and was deleted. False on a dry-run
|
|
1819
|
+
* (preview) AND on the idempotent missing-row case. */
|
|
1820
|
+
deleted: boolean;
|
|
1821
|
+
/** Number of `task_edges` rows cascaded out (informational). On a
|
|
1822
|
+
* dry-run, this is the would-be count. */
|
|
1823
|
+
deletedEdges: number;
|
|
1824
|
+
/** Number of `task_notes` rows cascaded out (informational). On a
|
|
1825
|
+
* dry-run, this is the would-be count. */
|
|
1826
|
+
deletedNotes: number;
|
|
1827
|
+
/** True iff this was a dry-run (`opts.dryRun: true`). On a
|
|
1828
|
+
* dry-run `deleted` is false and the counts are the would-be
|
|
1829
|
+
* counts; the DB is unchanged. Always false on a commit / on a
|
|
1830
|
+
* missing-row idempotent no-op. */
|
|
1831
|
+
dryRun: boolean;
|
|
1832
|
+
/** True iff a matching task row was found at the time of the
|
|
1833
|
+
* call. Discriminator for the CLI: a dry-run that found nothing
|
|
1834
|
+
* (`present: false`) renders differently from a dry-run that
|
|
1835
|
+
* found an existing task with zero edges and zero notes
|
|
1836
|
+
* (`present: true, deletedEdges: 0, deletedNotes: 0`). */
|
|
1837
|
+
present: boolean;
|
|
1898
1838
|
}
|
|
1899
|
-
interface
|
|
1900
|
-
/**
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1908
|
-
|
|
1839
|
+
interface DeleteTaskOptions {
|
|
1840
|
+
/** When true, return the cascade preview (would-be edge / note
|
|
1841
|
+
* counts) without mutating and without snapshotting. The CLI uses
|
|
1842
|
+
* this to power the bare `mu task delete <id>` two-phase pattern
|
|
1843
|
+
* (mirrors `mu workstream destroy` / `mu archive delete` /
|
|
1844
|
+
* `mu snapshot prune`). Surfaced by feedback ws task
|
|
1845
|
+
* fb_task_delete_no_yes (impact=30): a dogfood report typed
|
|
1846
|
+
* `mu task delete X --yes` (mirroring workstream destroy) and got
|
|
1847
|
+
* 'unknown option --yes' — the verb took no confirmation flag at
|
|
1848
|
+
* all. Two failed deletes left long-named tasks lingering. */
|
|
1849
|
+
dryRun?: boolean;
|
|
1909
1850
|
}
|
|
1910
1851
|
/**
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
* (the CLI maps a clean exit to 0, a timeout to 5).
|
|
1852
|
+
* Delete a task. FK CASCADE on `task_edges` (from + to) and
|
|
1853
|
+
* `task_notes` cleans the joined rows automatically. Idempotent on
|
|
1854
|
+
* a missing task (returns `deleted: false`).
|
|
1915
1855
|
*
|
|
1916
|
-
* Pre-
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
1856
|
+
* Pre-counts the cascade victims for reporting because SQLite's
|
|
1857
|
+
* `changes()` only reports rows directly affected by the DELETE.
|
|
1858
|
+
*
|
|
1859
|
+
* With `opts.dryRun: true`, returns the would-be counts without
|
|
1860
|
+
* touching the DB and without taking a snapshot (no mutation = no
|
|
1861
|
+
* snapshot — same reasoning that gates the closeTask snap on the
|
|
1862
|
+
* idempotent no-op path). The CLI bare `mu task delete <id>` form
|
|
1863
|
+
* uses this; `--yes` calls through with `dryRun: false`.
|
|
1919
1864
|
*/
|
|
1920
|
-
declare function
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
/**
|
|
1924
|
-
|
|
1925
|
-
/**
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1865
|
+
declare function deleteTask(db: Db, localId: string, workstream: string, opts?: DeleteTaskOptions): DeleteTaskResult;
|
|
1866
|
+
interface UpdateTaskOptions {
|
|
1867
|
+
title?: string;
|
|
1868
|
+
/** 1..100; enforced by schema CHECK. */
|
|
1869
|
+
impact?: number;
|
|
1870
|
+
/** > 0; enforced by schema CHECK. */
|
|
1871
|
+
effortDays?: number;
|
|
1872
|
+
}
|
|
1873
|
+
interface UpdateTaskResult {
|
|
1874
|
+
/** True iff at least one field actually changed. */
|
|
1875
|
+
updated: boolean;
|
|
1876
|
+
/** The fields whose values differ post-update (in `UpdateTaskOptions`'s
|
|
1877
|
+
* camelCase shape). Empty when `updated: false`. */
|
|
1878
|
+
changedFields: string[];
|
|
1929
1879
|
}
|
|
1930
1880
|
/**
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1881
|
+
* Update scalar fields on a task. Each option is independently optional;
|
|
1882
|
+
* passing none is a typed no-op (returns `updated: false, changedFields: []`).
|
|
1883
|
+
* Fields whose new value equals the current value are skipped (no row change).
|
|
1884
|
+
*
|
|
1885
|
+
* NOT for status (use `closeTask` / `openTask` / `setTaskStatus`), owner
|
|
1886
|
+
* (use `claimTask` / `releaseTask`), local_id (rename is deferred), or
|
|
1887
|
+
* workstream (cross-workstream moves are deferred).
|
|
1937
1888
|
*/
|
|
1938
|
-
interface
|
|
1939
|
-
|
|
1889
|
+
interface UpdateTaskScopeOption {
|
|
1890
|
+
workstream: string;
|
|
1940
1891
|
}
|
|
1892
|
+
declare function updateTask(db: Db, localId: string, opts: UpdateTaskOptions, scope: UpdateTaskScopeOption): UpdateTaskResult;
|
|
1893
|
+
|
|
1894
|
+
declare function isValidTaskId(id: string): boolean;
|
|
1941
1895
|
/**
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1944
|
-
*
|
|
1896
|
+
* Lowercase title; collapse non-alnum runs into single `_`; trim
|
|
1897
|
+
* leading/trailing `_`; prefix `t_` if the result starts with a digit
|
|
1898
|
+
* (schema requires first char letter); apply the soft cap with
|
|
1899
|
+
* word-boundary trim (cut at the last `_` at-or-before SLUG_SOFT_CAP
|
|
1900
|
+
* when one exists, else hard-truncate). Mirrors `tg`'s `id_from_title`
|
|
1901
|
+
* but adds the soft cap.
|
|
1902
|
+
*
|
|
1903
|
+
* Throws if `title` yields an empty slug after stripping.
|
|
1945
1904
|
*/
|
|
1946
|
-
declare function
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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.
|
|
1905
|
+
declare function slugifyTitle(title: string): string;
|
|
1906
|
+
/**
|
|
1907
|
+
* Result of `slugifyTitleVerbose`: the slug plus enough metadata for
|
|
1908
|
+
* the CLI to decide whether to warn the user that meaning was lost.
|
|
1954
1909
|
*
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1910
|
+
* slug — the same string `slugifyTitle` returns.
|
|
1911
|
+
* strippedLength — length of the post-strip pre-cap slug. When this
|
|
1912
|
+
* exceeds the SLUG_SOFT_CAP the verbose form had to
|
|
1913
|
+
* cut at a word boundary (or hard-truncate); the
|
|
1914
|
+
* cut clauses are gone with no in-band signal.
|
|
1915
|
+
* originalSlug — what the slug WOULD have been without the
|
|
1916
|
+
* SLUG_SOFT_CAP cut: full stripped slug with the
|
|
1917
|
+
* same `t_` digit-prefix correction and the same
|
|
1918
|
+
* SLUG_HARD_CAP ceiling, but no word-boundary
|
|
1919
|
+
* truncation. Equal to `slug` when nothing was
|
|
1920
|
+
* cut. The CLI surfaces this in `mu task add
|
|
1921
|
+
* --json` so scripted callers can detect the
|
|
1922
|
+
* truncation without grepping stderr.
|
|
1923
|
+
* truncated — true iff `slug.length < strippedLength` AFTER the
|
|
1924
|
+
* `t_` digit-prefix correction, i.e. real bytes were
|
|
1925
|
+
* dropped. False for any title that fits under the
|
|
1926
|
+
* soft cap or whose only diff vs the stripped slug
|
|
1927
|
+
* is the `t_` prefix.
|
|
1928
|
+
*
|
|
1929
|
+
* The CLI's `mu task add` uses `truncated` to print a one-line stderr
|
|
1930
|
+
* hint pointing at the `<id>` positional override and (under --json)
|
|
1931
|
+
* to surface `originalSlug` alongside `truncated:true`
|
|
1932
|
+
* (slugifytitle_silently_drops_clauses; task_add_slugify_silently_truncates_ids).
|
|
1933
|
+
*/
|
|
1934
|
+
interface SlugifyResult {
|
|
1935
|
+
slug: string;
|
|
1936
|
+
strippedLength: number;
|
|
1937
|
+
originalSlug: string;
|
|
1938
|
+
truncated: boolean;
|
|
1979
1939
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1940
|
+
/**
|
|
1941
|
+
* Verbose sibling of `slugifyTitle`: returns the slug AND a
|
|
1942
|
+
* `truncated` flag so the CLI can hint to the user when the soft cap
|
|
1943
|
+
* dropped clauses (the meaning-shift hazard documented in
|
|
1944
|
+
* slugifytitle_silently_drops_clauses).
|
|
1945
|
+
*
|
|
1946
|
+
* Algorithm is byte-for-byte identical to `slugifyTitle`; this just
|
|
1947
|
+
* surfaces the metadata that the plain form throws away.
|
|
1948
|
+
*/
|
|
1949
|
+
declare function slugifyTitleVerbose(title: string): SlugifyResult;
|
|
1950
|
+
/**
|
|
1951
|
+
* Generate a unique task id from a title. v5: tasks.local_id is
|
|
1952
|
+
* per-workstream unique, so the collision check scopes to one
|
|
1953
|
+
* workstream. On collision, appends `_2`, `_3`, … until unique.
|
|
1954
|
+
*/
|
|
1955
|
+
declare function idFromTitle(db: Db, workstream: string, title: string): string;
|
|
1956
|
+
/**
|
|
1957
|
+
* Result of `idFromTitleVerbose`: the unique-in-workstream id plus the
|
|
1958
|
+
* truncated flag from the underlying slugify pass. Used by `mu task
|
|
1959
|
+
* add` to decide whether to surface the stderr hint about lost clauses
|
|
1960
|
+
* (slugifytitle_silently_drops_clauses) and to surface the un-truncated
|
|
1961
|
+
* slug in `--json` (task_add_slugify_silently_truncates_ids).
|
|
1962
|
+
*
|
|
1963
|
+
* id — the unique-in-workstream task id.
|
|
1964
|
+
* truncated — true iff the underlying slugify pass cut real
|
|
1965
|
+
* characters (collision-suffixing does NOT flip
|
|
1966
|
+
* this).
|
|
1967
|
+
* originalSlug — what the slug would have been without the
|
|
1968
|
+
* SLUG_SOFT_CAP cut. Equal to `id` when nothing was
|
|
1969
|
+
* cut AND no collision suffix was appended; for
|
|
1970
|
+
* the truncation-detection use case the only thing
|
|
1971
|
+
* the CLI cares about is the lossy-vs-not
|
|
1972
|
+
* comparison surfaced via `truncated`.
|
|
1973
|
+
*/
|
|
1974
|
+
interface IdFromTitleResult {
|
|
1975
|
+
id: string;
|
|
1976
|
+
truncated: boolean;
|
|
1977
|
+
originalSlug: string;
|
|
1997
1978
|
}
|
|
1998
|
-
/**
|
|
1999
|
-
*
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
1979
|
+
/**
|
|
1980
|
+
* Verbose sibling of `idFromTitle`: returns the unique id, the
|
|
1981
|
+
* `truncated` flag from the slugify pass, and the un-truncated
|
|
1982
|
+
* `originalSlug` for `--json` consumers. Collision-suffixing (`_2`,
|
|
1983
|
+
* `_3`, …) does not flip `truncated` — the underlying slug's lossiness
|
|
1984
|
+
* is what the CLI hint cares about.
|
|
1985
|
+
*/
|
|
1986
|
+
declare function idFromTitleVerbose(db: Db, workstream: string, title: string): IdFromTitleResult;
|
|
1987
|
+
|
|
1988
|
+
declare function getTask(db: Db, localId: string, workstream: string): TaskRow | undefined;
|
|
1989
|
+
/**
|
|
1990
|
+
* List tasks. With no `workstream` arg returns every row — used by `mu sql`
|
|
1991
|
+
* and by tests; CLI surfaces always pass a workstream so users only see
|
|
1992
|
+
* their own.
|
|
1993
|
+
*/
|
|
1994
|
+
interface ListTasksOptions {
|
|
1995
|
+
/** Filter to one or more lifecycle statuses. Omitted = all statuses. */
|
|
1996
|
+
status?: TaskStatus | readonly TaskStatus[];
|
|
1997
|
+
}
|
|
1998
|
+
declare function listTasks(db: Db, workstream?: string, opts?: ListTasksOptions): TaskRow[];
|
|
1999
|
+
/** Options for listReady. The optional `statuses` filter composes
|
|
2000
|
+
* on top of the `ready` view (which itself constrains to
|
|
2001
|
+
* `status='OPEN'`); passing only OPEN is identical to today's no-
|
|
2002
|
+
* filter shape, passing only non-OPEN values returns []. Exists so
|
|
2003
|
+
* `mu task next --status` can mirror the multi-status flag shape
|
|
2004
|
+
* shipped on `mu task list` (task_list_multi_status_union). */
|
|
2005
|
+
interface ListReadyOptions {
|
|
2006
|
+
status?: TaskStatus | readonly TaskStatus[];
|
|
2007
|
+
}
|
|
2008
|
+
declare function listReady(db: Db, workstream: string, opts?: ListReadyOptions): TaskRow[];
|
|
2009
|
+
declare function listBlocked(db: Db, workstream: string): TaskRow[];
|
|
2010
|
+
declare function listGoals(db: Db, workstream: string): TaskRow[];
|
|
2011
|
+
/** All IN_PROGRESS tasks in a workstream, most-recently-touched first.
|
|
2012
|
+
* Used by `mu state` to populate its in-progress slice; exposed as a
|
|
2013
|
+
* named SDK helper so CLI renderers don't re-derive the row-shape
|
|
2014
|
+
* conversion (review_code_raw_task_state_duplicate). */
|
|
2015
|
+
declare function listInProgress(db: Db, workstream: string): TaskRow[];
|
|
2016
|
+
/** Most-recently-closed tasks in a workstream, newest first, capped at
|
|
2017
|
+
* `limit` (default 5). Used by `mu state` for its 'recent closed'
|
|
2018
|
+
* slice; exposed as a named SDK helper so the CLI no longer needs the
|
|
2019
|
+
* raw-row type that was duplicating RawTaskRow
|
|
2020
|
+
* (review_code_raw_task_state_duplicate). */
|
|
2021
|
+
declare function listRecentClosed(db: Db, workstream: string, limit?: number): TaskRow[];
|
|
2022
|
+
/** Optional filter knobs for `listNotes`. Default-everything-undefined
|
|
2023
|
+
* preserves the historical "return every note, oldest-first" shape so
|
|
2024
|
+
* every existing caller (cmdTaskShow's notes block, exporting.ts's
|
|
2025
|
+
* bucket renderer, agents.test.ts) keeps working unchanged.
|
|
2002
2026
|
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
*
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
*
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
*
|
|
2017
|
-
|
|
2018
|
-
/**
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
*
|
|
2026
|
-
|
|
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;
|
|
2027
|
+
* Filters compose multiplicatively when both apply (`since` AND
|
|
2028
|
+
* `tail`): the timestamp filter is applied first, then `tail` slices
|
|
2029
|
+
* the last N of what survived. The CLI surface (`mu task notes
|
|
2030
|
+
* --tail / --since / --since-claim`) lives in src/cli/tasks/edit.ts;
|
|
2031
|
+
* the mutex between `--since` and `--since-claim` is a CLI concern,
|
|
2032
|
+
* not enforced here — if both arrive at the SDK, `since` wins (it's
|
|
2033
|
+
* the explicit one) and `sinceClaim` is ignored. The auto-resolve
|
|
2034
|
+
* for `sinceClaim` (look up the most recent `task claim` event in
|
|
2035
|
+
* agent_logs) happens here so the SDK is self-contained for scripted
|
|
2036
|
+
* callers. */
|
|
2037
|
+
interface ListNotesOptions {
|
|
2038
|
+
/** Print only the last N notes (after any timestamp filter). Must
|
|
2039
|
+
* be a positive integer; a value of 0 returns no rows but is not
|
|
2040
|
+
* an error here — CLI-side validation rejects `--tail 0`. */
|
|
2041
|
+
tail?: number;
|
|
2042
|
+
/** ISO-8601 cutoff: only notes with `created_at > since` survive.
|
|
2043
|
+
* Comparison is lexicographic on the ISO string (matches the way
|
|
2044
|
+
* the rest of the codebase compares ISO timestamps). */
|
|
2045
|
+
since?: string;
|
|
2046
|
+
/** When true and `since` is unset, look up the `created_at` of the
|
|
2047
|
+
* most recent `task claim` event for this task and use it as the
|
|
2048
|
+
* cutoff. Falls back to no filter when no claim event exists
|
|
2049
|
+
* (equivalent to `--since-beginning`). */
|
|
2050
|
+
sinceClaim?: boolean;
|
|
2031
2051
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2052
|
+
/** List notes for a task. Operator-facing local_id; resolves to the
|
|
2053
|
+
* surrogate task id via taskIdFor (with optional workstream scope).
|
|
2054
|
+
*
|
|
2055
|
+
* Optional filters: see {@link ListNotesOptions}. Default behaviour
|
|
2056
|
+
* (no opts) is unchanged — every note, oldest-first. */
|
|
2057
|
+
declare function listNotes(db: Db, taskLocalId: string, workstream: string, opts?: ListNotesOptions): TaskNoteRow[];
|
|
2058
|
+
/**
|
|
2059
|
+
* All tasks currently owned by `agent` in a given workstream
|
|
2060
|
+
* (v5: agents.name is per-workstream unique). Sorted by local_id.
|
|
2061
|
+
*
|
|
2062
|
+
* Defaults to **excluding CLOSED** since the verb's purpose is "what
|
|
2063
|
+
* is X currently working on?" and a closed task is no longer being
|
|
2064
|
+
* worked on. closeTask intentionally preserves `owner` as a
|
|
2065
|
+
* historical record (so audit/notes can attribute decisions); pass
|
|
2066
|
+
* `{ includeClosed: true }` to surface that history.
|
|
2067
|
+
*/
|
|
2068
|
+
declare function listTasksByOwner(db: Db, workstream: string, owner: string, opts?: {
|
|
2069
|
+
includeClosed?: boolean;
|
|
2070
|
+
}): TaskRow[];
|
|
2071
|
+
interface SearchTasksOptions {
|
|
2072
|
+
/** Restrict to one workstream; undefined = search across all. */
|
|
2073
|
+
workstream?: string;
|
|
2074
|
+
/** Also search `task_notes.content` (default false: titles + ids only). */
|
|
2075
|
+
includeNotes?: boolean;
|
|
2048
2076
|
}
|
|
2049
|
-
/**
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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;
|
|
2077
|
+
/**
|
|
2078
|
+
* Substring search on task `title` and `local_id`, case-insensitive.
|
|
2079
|
+
* With `includeNotes: true` also searches `task_notes.content`. The
|
|
2080
|
+
* pattern is wrapped in `%...%` automatically so callers don't need
|
|
2081
|
+
* SQL LIKE knowledge — for explicit globs (or regex), use `mu sql`.
|
|
2082
|
+
*/
|
|
2083
|
+
declare function searchTasks(db: Db, pattern: string, opts?: SearchTasksOptions): TaskRow[];
|
|
2058
2084
|
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2085
|
+
declare const WORKSPACE_STALE_THRESHOLD = 10;
|
|
2086
|
+
declare function isWorkspaceStale(behind: number | null | undefined): boolean;
|
|
2087
|
+
|
|
2088
|
+
interface WorkspaceRow {
|
|
2089
|
+
agentName: string;
|
|
2090
|
+
workstreamName: string;
|
|
2091
|
+
backend: VcsBackendName;
|
|
2092
|
+
path: string;
|
|
2093
|
+
parentRef: string | null;
|
|
2094
|
+
createdAt: string;
|
|
2095
|
+
/** How many commits the workspace's parent_ref is behind the project's
|
|
2096
|
+
* default branch HEAD, as of the last time the workspace's local refs
|
|
2097
|
+
* cache was updated. Undefined when not yet computed (the listWorkspaces
|
|
2098
|
+
* fast path leaves it unset; call decorateWithStaleness to populate).
|
|
2099
|
+
* Null when staleness was queried but cannot be computed (no main found,
|
|
2100
|
+
* none-backend, missing parent_ref, command failure). */
|
|
2101
|
+
commitsBehindMain?: number | null;
|
|
2102
|
+
/** True when the workspace has uncommitted / unstaged / untracked-not-
|
|
2103
|
+
* ignored files, as observed by the backend's `listDirtyFiles`.
|
|
2104
|
+
* Undefined when not yet computed (the listWorkspaces fast path leaves
|
|
2105
|
+
* it unset; call decorateWithDirty to populate). Null when the dirty
|
|
2106
|
+
* check could not be performed (backend command failure). For jj /
|
|
2107
|
+
* none backends — which have no operator-visible "dirty" concept —
|
|
2108
|
+
* this is always false (their listDirtyFiles returns []). */
|
|
2109
|
+
dirty?: boolean | null;
|
|
2068
2110
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2111
|
+
declare class WorkspaceExistsError extends Error implements HasNextSteps {
|
|
2112
|
+
readonly agent: string;
|
|
2113
|
+
readonly name = "WorkspaceExistsError";
|
|
2114
|
+
constructor(agent: string);
|
|
2115
|
+
errorNextSteps(): NextStep[];
|
|
2116
|
+
}
|
|
2117
|
+
declare class WorkspaceNotFoundError extends Error implements HasNextSteps {
|
|
2118
|
+
readonly agent: string;
|
|
2119
|
+
readonly name = "WorkspaceNotFoundError";
|
|
2120
|
+
constructor(agent: string);
|
|
2121
|
+
errorNextSteps(): NextStep[];
|
|
2080
2122
|
}
|
|
2081
2123
|
/**
|
|
2082
|
-
*
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2085
|
-
*
|
|
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.
|
|
2124
|
+
* Thrown by createWorkspace when the on-disk path it would create is
|
|
2125
|
+
* already occupied. Distinct from WorkspaceExistsError (which is about
|
|
2126
|
+
* the DB row) so the recovery is clear: the dir is orphaned (no DB
|
|
2127
|
+
* row points at it) and needs cleanup.
|
|
2091
2128
|
*
|
|
2092
|
-
*
|
|
2093
|
-
* no IN_PROGRESS status is a no-op (returns `changed: false`).
|
|
2094
|
-
* Throws TaskNotFoundError on missing.
|
|
2129
|
+
* Maps to exit code 4 (conflict).
|
|
2095
2130
|
*/
|
|
2096
|
-
declare
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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;
|
|
2158
|
-
}
|
|
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;
|
|
2131
|
+
declare class WorkspacePathNotEmptyError extends Error implements HasNextSteps {
|
|
2132
|
+
readonly agent: string;
|
|
2133
|
+
readonly workstream: string;
|
|
2134
|
+
readonly workspacePath: string;
|
|
2135
|
+
readonly name = "WorkspacePathNotEmptyError";
|
|
2136
|
+
constructor(agent: string, workstream: string, workspacePath: string);
|
|
2137
|
+
errorNextSteps(): NextStep[];
|
|
2171
2138
|
}
|
|
2172
2139
|
/**
|
|
2173
|
-
*
|
|
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.
|
|
2140
|
+
* Thrown by createWorkspace when the resolved projectRoot is the
|
|
2141
|
+
* user's $HOME.
|
|
2189
2142
|
*
|
|
2190
|
-
*
|
|
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).
|
|
2143
|
+
* Maps to exit code 4 (conflict).
|
|
2196
2144
|
*/
|
|
2197
|
-
declare
|
|
2145
|
+
declare class HomeDirAsProjectRootError extends Error implements HasNextSteps {
|
|
2146
|
+
readonly agent: string;
|
|
2147
|
+
readonly workstream: string;
|
|
2148
|
+
readonly homeDir: string;
|
|
2149
|
+
readonly name = "HomeDirAsProjectRootError";
|
|
2150
|
+
constructor(agent: string, workstream: string, homeDir: string);
|
|
2151
|
+
errorNextSteps(): NextStep[];
|
|
2152
|
+
}
|
|
2198
2153
|
/**
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2154
|
+
* Compose the canonical on-disk path for an agent's workspace. Used by
|
|
2155
|
+
* createWorkspace and reachable from `mu workspace path` so the user
|
|
2156
|
+
* can `cd $(mu workspace path foo)` even before the directory exists.
|
|
2157
|
+
*/
|
|
2158
|
+
declare function workspacePath(workstream: string, agent: string): string;
|
|
2159
|
+
/** Root dir for a workstream's workspaces — the parent of all
|
|
2160
|
+
* per-agent workspace dirs. Used by listWorkspaceOrphans to scan
|
|
2161
|
+
* the filesystem. */
|
|
2162
|
+
declare function workspacesRoot(workstream: string): string;
|
|
2163
|
+
interface WorkspaceStaleness {
|
|
2164
|
+
agentName: string;
|
|
2165
|
+
workstreamName: string;
|
|
2166
|
+
commitsBehindMain: number | null;
|
|
2167
|
+
isStale: boolean;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
interface CreateWorkspaceOptions {
|
|
2171
|
+
agent: string;
|
|
2172
|
+
workstream: string;
|
|
2173
|
+
/** Project root to branch from. Defaults to the current working
|
|
2174
|
+
* directory (the `mu` invocation site, which is normally what the
|
|
2175
|
+
* user wants). */
|
|
2176
|
+
projectRoot?: string;
|
|
2177
|
+
/** Override backend detection. Default: walk `detectBackend`.
|
|
2178
|
+
* Accepts either a name ("jj" / "sl" / "git" / "none") OR a
|
|
2179
|
+
* pre-built `VcsBackend` object — the object form lets tests inject
|
|
2180
|
+
* a fresh fake backend without mutating the exported singletons. */
|
|
2181
|
+
backend?: VcsBackendName | VcsBackend;
|
|
2182
|
+
/** Optional ref to base the workspace on. Backend-specific. */
|
|
2183
|
+
parentRef?: string;
|
|
2184
|
+
/** INTERNAL. When false, suppress the `workspace create` system
|
|
2185
|
+
* event. Used by `recreateWorkspace` so the audit trail records
|
|
2186
|
+
* ONE atomic `workspace recreate` line instead of separate
|
|
2187
|
+
* free + create entries. Defaults to true. */
|
|
2188
|
+
_suppressEvent?: boolean;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Create a fresh workspace for an agent. Allocates the on-disk
|
|
2192
|
+
* directory, records the row, emits a system event. Idempotent ONLY
|
|
2193
|
+
* to the extent that the row check is up-front; if the row exists
|
|
2194
|
+
* we throw `WorkspaceExistsError` rather than silently re-using a
|
|
2195
|
+
* possibly-stale on-disk state. Callers should `freeWorkspace` first.
|
|
2196
|
+
*/
|
|
2197
|
+
declare function createWorkspace(db: Db, opts: CreateWorkspaceOptions): Promise<WorkspaceRow>;
|
|
2198
|
+
declare function getWorkspaceForAgent(db: Db, agent: string, workstream: string): WorkspaceRow | undefined;
|
|
2199
|
+
declare function listWorkspaces(db: Db, workstream?: string): WorkspaceRow[];
|
|
2200
|
+
interface FreeWorkspaceOptions {
|
|
2201
|
+
/** If true, attempt to commit pending changes before tearing down.
|
|
2202
|
+
* Backend-specific; see VcsBackend.freeWorkspace. */
|
|
2203
|
+
commit?: boolean;
|
|
2204
|
+
/** INTERNAL. When false, suppress the `workspace free` system
|
|
2205
|
+
* event AND skip the pre-mutation snapshot capture. Used by
|
|
2206
|
+
* `recreateWorkspace` so the audit trail records ONE atomic
|
|
2207
|
+
* `workspace recreate` line and one snapshot for the whole
|
|
2208
|
+
* free+create cycle. Defaults to true. */
|
|
2209
|
+
_suppressEvent?: boolean;
|
|
2210
|
+
}
|
|
2211
|
+
interface FreeWorkspaceResult {
|
|
2212
|
+
/** The committed ref, when `commit` was true and there was something
|
|
2213
|
+
* to commit. */
|
|
2214
|
+
committedRef?: string;
|
|
2215
|
+
/** True iff the on-disk path was actually removed. */
|
|
2216
|
+
removed: boolean;
|
|
2217
|
+
/** True iff the DB row was actually deleted. */
|
|
2218
|
+
rowDeleted: boolean;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Tear down an agent's workspace. Calls the backend to remove the
|
|
2222
|
+
* on-disk directory (with optional auto-commit), then DELETEs the row.
|
|
2223
|
+
* Idempotent on a missing workspace (returns all-false).
|
|
2224
|
+
*/
|
|
2225
|
+
declare function freeWorkspace(db: Db, agent: string, opts: FreeWorkspaceOptions & {
|
|
2226
|
+
workstream: string;
|
|
2227
|
+
}): Promise<FreeWorkspaceResult>;
|
|
2228
|
+
|
|
2229
|
+
declare function getWorkspaceStaleness(db: Db, agentName: string, workstreamName: string): Promise<WorkspaceStaleness | null>;
|
|
2230
|
+
/**
|
|
2231
|
+
* Decorate each row with `commitsBehindMain` by asking the row's backend
|
|
2232
|
+
* how far the parent_ref is behind the project's default branch HEAD.
|
|
2233
|
+
* Cheap, pure observation: NO automatic `git fetch` / `jj git fetch` /
|
|
2234
|
+
* `sl pull`. The number is as fresh as the workspace's local refs cache.
|
|
2201
2235
|
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
*
|
|
2209
|
-
*
|
|
2236
|
+
* Returns a NEW array; does not mutate the input. Rows whose parent_ref
|
|
2237
|
+
* is missing, or whose backend's commitsBehind throws / returns null,
|
|
2238
|
+
* get `commitsBehindMain: null`.
|
|
2239
|
+
*/
|
|
2240
|
+
declare function decorateWithStaleness(rows: readonly WorkspaceRow[]): Promise<WorkspaceRow[]>;
|
|
2241
|
+
/**
|
|
2242
|
+
* Decorate every row with a `dirty` marker — true when the backend's
|
|
2243
|
+
* `listDirtyFiles` reports any uncommitted / unstaged / untracked-not-
|
|
2244
|
+
* ignored files; false when clean; null on backend-command failure.
|
|
2210
2245
|
*
|
|
2211
|
-
*
|
|
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.
|
|
2246
|
+
* Returns a NEW array; does not mutate the input.
|
|
2216
2247
|
*/
|
|
2217
|
-
declare function
|
|
2248
|
+
declare function decorateWithDirty(rows: readonly WorkspaceRow[]): Promise<WorkspaceRow[]>;
|
|
2218
2249
|
|
|
2219
|
-
interface
|
|
2220
|
-
/**
|
|
2221
|
-
|
|
2222
|
-
|
|
2250
|
+
interface WorkspaceOrphan {
|
|
2251
|
+
/** The on-disk dir name (the agent name it WOULD be for, if mu had
|
|
2252
|
+
* registered it). */
|
|
2253
|
+
agentName: string;
|
|
2254
|
+
/** Workstream the dir is filed under. */
|
|
2223
2255
|
workstreamName: string;
|
|
2224
|
-
|
|
2225
|
-
|
|
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;
|
|
2256
|
+
/** Absolute path to the orphan dir. */
|
|
2257
|
+
path: string;
|
|
2232
2258
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2259
|
+
/**
|
|
2260
|
+
* Like WorkspaceOrphan but additionally flags whether the parent
|
|
2261
|
+
* workstream itself is gone (no row in `workstreams`). Returned by
|
|
2262
|
+
* listAllOrphanWorkspaces; the per-workstream listWorkspaceOrphans
|
|
2263
|
+
* doesn't carry this since by construction it only runs against an
|
|
2264
|
+
* existing workstream.
|
|
2265
|
+
*/
|
|
2266
|
+
interface StrandedWorkspaceOrphan extends WorkspaceOrphan {
|
|
2267
|
+
/** True iff the parent workstream has no DB row (the dir was left
|
|
2268
|
+
* behind by a `mu workstream destroy` or a manual DELETE). */
|
|
2269
|
+
stranded: boolean;
|
|
2237
2270
|
}
|
|
2238
|
-
declare function isValidTaskId(id: string): boolean;
|
|
2239
2271
|
/**
|
|
2240
|
-
*
|
|
2241
|
-
*
|
|
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.
|
|
2272
|
+
* Scan `<state-dir>/workspaces/<workstream>/` for directories that
|
|
2273
|
+
* have no row in `vcs_workspaces`.
|
|
2246
2274
|
*
|
|
2247
|
-
*
|
|
2275
|
+
* Returns `[]` when the workstream's workspaces dir doesn't exist,
|
|
2276
|
+
* or when every dir on disk has a corresponding DB row. Filesystem
|
|
2277
|
+
* read is best-effort: a missing/inaccessible dir returns `[]`.
|
|
2248
2278
|
*/
|
|
2249
|
-
declare function
|
|
2279
|
+
declare function listWorkspaceOrphans(db: Db, workstream: string): WorkspaceOrphan[];
|
|
2250
2280
|
/**
|
|
2251
|
-
*
|
|
2252
|
-
*
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
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.
|
|
2272
|
-
*
|
|
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).
|
|
2281
|
+
* Cross-workstream variant of listWorkspaceOrphans. Reads
|
|
2282
|
+
* `<state-dir>/workspaces/`, recurses one level (per-ws subdir →
|
|
2283
|
+
* per-agent subdir), and surfaces every dir with no row in
|
|
2284
|
+
* `vcs_workspaces`.
|
|
2277
2285
|
*/
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2286
|
+
declare function listAllOrphanWorkspaces(db: Db): StrandedWorkspaceOrphan[];
|
|
2287
|
+
|
|
2288
|
+
interface RecreateWorkspaceOptions {
|
|
2289
|
+
/** Same as createWorkspace; defaults to cwd. */
|
|
2290
|
+
projectRoot?: string;
|
|
2291
|
+
/** Same as createWorkspace; if undefined the previous backend is
|
|
2292
|
+
* reused (auto-detection re-runs only when --backend was passed). */
|
|
2293
|
+
backend?: VcsBackendName | VcsBackend;
|
|
2294
|
+
/** Same as createWorkspace; if undefined the new workspace bases on
|
|
2295
|
+
* the backend's current head (for git/jj/sl: the project's main),
|
|
2296
|
+
* which is the whole point of the verb. */
|
|
2297
|
+
parentRef?: string;
|
|
2298
|
+
/** When true, skip the dirty-check refusal and discard any
|
|
2299
|
+
* uncommitted changes in the existing workspace. The lossy escape
|
|
2300
|
+
* hatch — mirrors the implicit semantics of `mu workspace free`
|
|
2301
|
+
* without --commit. */
|
|
2302
|
+
force?: boolean;
|
|
2303
|
+
}
|
|
2304
|
+
interface RecreateWorkspaceResult {
|
|
2305
|
+
/** The freshly-created workspace row (the previous row is already
|
|
2306
|
+
* gone by the time we return). */
|
|
2307
|
+
workspace: WorkspaceRow;
|
|
2308
|
+
/** parent_ref of the WORKSPACE BEFORE recreate, so callers (and the
|
|
2309
|
+
* CLI's success message) can show "bumped from <old> -> <new>". */
|
|
2310
|
+
previousParentRef: string | null;
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Free + create in one atomic-ish verb. Between waves the operator
|
|
2314
|
+
* wants the SAME agent name with a fresh workspace pinned to current
|
|
2315
|
+
* main; doing `free` then `create` manually was the dogfood-painful
|
|
2316
|
+
* pattern.
|
|
2317
|
+
*/
|
|
2318
|
+
declare function recreateWorkspace(db: Db, agent: string, opts: RecreateWorkspaceOptions & {
|
|
2319
|
+
workstream: string;
|
|
2320
|
+
}): Promise<RecreateWorkspaceResult>;
|
|
2321
|
+
|
|
2322
|
+
declare class TaskNotFoundError extends Error implements HasNextSteps {
|
|
2323
|
+
readonly taskId: string;
|
|
2324
|
+
readonly name = "TaskNotFoundError";
|
|
2325
|
+
constructor(taskId: string);
|
|
2326
|
+
errorNextSteps(): NextStep[];
|
|
2327
|
+
}
|
|
2328
|
+
declare class TaskExistsError extends Error implements HasNextSteps {
|
|
2329
|
+
readonly taskId: string;
|
|
2330
|
+
readonly name = "TaskExistsError";
|
|
2331
|
+
constructor(taskId: string);
|
|
2332
|
+
errorNextSteps(): NextStep[];
|
|
2283
2333
|
}
|
|
2284
2334
|
/**
|
|
2285
|
-
*
|
|
2286
|
-
*
|
|
2287
|
-
*
|
|
2288
|
-
*
|
|
2289
|
-
*
|
|
2290
|
-
* Algorithm is byte-for-byte identical to `slugifyTitle`; this just
|
|
2291
|
-
* surfaces the metadata that the plain form throws away.
|
|
2335
|
+
* Thrown when a verb is invoked with `-w/--workstream <name>` but the
|
|
2336
|
+
* named task lives in a different workstream. Distinguishes "the user
|
|
2337
|
+
* typo'd the workstream" from "the task doesn't exist anywhere"
|
|
2338
|
+
* (which surfaces as `TaskNotFoundError`). Maps to exit code 4
|
|
2339
|
+
* (conflict / wrong scope).
|
|
2292
2340
|
*/
|
|
2293
|
-
declare
|
|
2341
|
+
declare class TaskNotInWorkstreamError extends Error implements HasNextSteps {
|
|
2342
|
+
readonly taskId: string;
|
|
2343
|
+
readonly expectedWorkstream: string;
|
|
2344
|
+
readonly actualWorkstream: string;
|
|
2345
|
+
readonly name = "TaskNotInWorkstreamError";
|
|
2346
|
+
constructor(taskId: string, expectedWorkstream: string, actualWorkstream: string);
|
|
2347
|
+
errorNextSteps(): NextStep[];
|
|
2348
|
+
}
|
|
2349
|
+
declare class TaskAlreadyOwnedError extends Error implements HasNextSteps {
|
|
2350
|
+
readonly taskId: string;
|
|
2351
|
+
readonly currentOwner: string;
|
|
2352
|
+
readonly name = "TaskAlreadyOwnedError";
|
|
2353
|
+
constructor(taskId: string, currentOwner: string);
|
|
2354
|
+
errorNextSteps(): NextStep[];
|
|
2355
|
+
}
|
|
2294
2356
|
/**
|
|
2295
|
-
*
|
|
2296
|
-
*
|
|
2297
|
-
*
|
|
2357
|
+
* Thrown by `rejectTask` / `deferTask` when the target task has
|
|
2358
|
+
* dependents that are still OPEN or IN_PROGRESS. Rejecting or
|
|
2359
|
+
* deferring such a task would silently strand the dependents (they'd
|
|
2360
|
+
* remain blocked by a prereq that's never going to satisfy the edge),
|
|
2361
|
+
* so we refuse and force an explicit decision: pass `--cascade` to
|
|
2362
|
+
* apply the same status to every transitive dependent, drop the
|
|
2363
|
+
* blocking edge first with `mu task unblock`, or address the
|
|
2364
|
+
* dependents individually. Maps to exit code 4.
|
|
2298
2365
|
*/
|
|
2299
|
-
declare
|
|
2366
|
+
declare class TaskHasOpenDependentsError extends Error implements HasNextSteps {
|
|
2367
|
+
readonly taskId: string;
|
|
2368
|
+
readonly verb: "reject" | "defer";
|
|
2369
|
+
readonly dependents: readonly string[];
|
|
2370
|
+
readonly name = "TaskHasOpenDependentsError";
|
|
2371
|
+
constructor(taskId: string, verb: "reject" | "defer", dependents: readonly string[]);
|
|
2372
|
+
errorNextSteps(): NextStep[];
|
|
2373
|
+
}
|
|
2300
2374
|
/**
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
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).
|
|
2375
|
+
* Thrown when `mu task claim` resolves a claimer agent name (from the
|
|
2376
|
+
* pane title or --for) that has no matching row in the agents table.
|
|
2306
2377
|
*
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2309
|
-
*
|
|
2310
|
-
*
|
|
2311
|
-
*
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
2314
|
-
* the truncation-detection use case the only thing
|
|
2315
|
-
* the CLI cares about is the lossy-vs-not
|
|
2316
|
-
* comparison surfaced via `truncated`.
|
|
2378
|
+
* The FK on `tasks.owner` references `agents.name`; without this guard
|
|
2379
|
+
* the claim attempt would fail with the unhelpful 'FOREIGN KEY constraint
|
|
2380
|
+
* failed' from SQLite. This typed error gives the user actionable next
|
|
2381
|
+
* steps (run `mu agent adopt <pane-id>` to register, or use --for to pick a
|
|
2382
|
+
* different agent).
|
|
2383
|
+
*
|
|
2384
|
+
* Maps to exit code 4 (conflict) via the cli.ts handler.
|
|
2317
2385
|
*/
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2386
|
+
declare class ClaimerNotRegisteredError extends Error implements HasNextSteps {
|
|
2387
|
+
readonly agentName: string;
|
|
2388
|
+
readonly paneId: string | null;
|
|
2389
|
+
readonly name = "ClaimerNotRegisteredError";
|
|
2390
|
+
constructor(agentName: string, paneId: string | null);
|
|
2391
|
+
/**
|
|
2392
|
+
* Three actionable resolutions in expected-frequency order:
|
|
2393
|
+
* 1. --self : orchestrator pattern (working directly)
|
|
2394
|
+
* 2. --for : dispatcher pattern (assigning to a worker)
|
|
2395
|
+
* 3. mu agent adopt: registration pattern (promote pane to worker)
|
|
2396
|
+
*/
|
|
2397
|
+
errorNextSteps(): NextStep[];
|
|
2398
|
+
}
|
|
2399
|
+
declare class CycleError extends Error implements HasNextSteps {
|
|
2400
|
+
readonly from: string;
|
|
2401
|
+
readonly to: string;
|
|
2402
|
+
readonly name = "CycleError";
|
|
2403
|
+
constructor(from: string, to: string);
|
|
2404
|
+
errorNextSteps(): NextStep[];
|
|
2405
|
+
}
|
|
2406
|
+
declare class CrossWorkstreamEdgeError extends Error implements HasNextSteps {
|
|
2407
|
+
readonly blocker: string;
|
|
2408
|
+
readonly blockerWorkstream: string;
|
|
2409
|
+
readonly dependent: string;
|
|
2410
|
+
readonly dependentWorkstream: string;
|
|
2411
|
+
readonly name = "CrossWorkstreamEdgeError";
|
|
2412
|
+
constructor(blocker: string, blockerWorkstream: string, dependent: string, dependentWorkstream: string);
|
|
2413
|
+
errorNextSteps(): NextStep[];
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
interface SetStatusResult {
|
|
2417
|
+
/** Status before the call. */
|
|
2418
|
+
previousStatus: TaskStatus;
|
|
2419
|
+
/** Status after the call (== requested status). */
|
|
2420
|
+
status: TaskStatus;
|
|
2421
|
+
/** True iff the row actually changed. False on idempotent no-op. */
|
|
2422
|
+
changed: boolean;
|
|
2322
2423
|
}
|
|
2323
2424
|
/**
|
|
2324
|
-
*
|
|
2325
|
-
*
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2425
|
+
* Optional evidence string carried on lifecycle verbs (close / open /
|
|
2426
|
+
* claim / release). Lands in the auto-emitted `kind='event'` payload
|
|
2427
|
+
* verbatim, prefixed with `evidence=`. The first inch of distinguishing
|
|
2428
|
+
* "observed" from "claimed" state per an internal critique: the
|
|
2429
|
+
* verb still trusts the caller (it's not a verifier), but the audit
|
|
2430
|
+
* trail records what the caller said it relied on.
|
|
2329
2431
|
*/
|
|
2330
|
-
|
|
2331
|
-
|
|
2432
|
+
interface EvidenceOption {
|
|
2433
|
+
evidence?: string;
|
|
2434
|
+
}
|
|
2332
2435
|
/**
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2436
|
+
* Flip a task's status to any of OPEN / IN_PROGRESS / CLOSED.
|
|
2437
|
+
* Idempotent: setting a task to its current status is a no-op (returns
|
|
2438
|
+
* `changed: false`) rather than throwing. Owner is unchanged.
|
|
2336
2439
|
*/
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
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[];
|
|
2350
|
-
}
|
|
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.
|
|
2440
|
+
declare function setTaskStatus(db: Db, localId: string, status: TaskStatus, opts: EvidenceOption & {
|
|
2441
|
+
workstream: string;
|
|
2442
|
+
}): SetStatusResult;
|
|
2443
|
+
/** Result of `closeTask` when called with `ifReady: true` and the
|
|
2444
|
+
* task is NOT yet ready to close (still has at least one OPEN /
|
|
2445
|
+
* IN_PROGRESS blocker). Distinguished from a regular `SetStatusResult`
|
|
2446
|
+
* by the literal `skipped` field; the CLI keys on it to switch
|
|
2447
|
+
* between the "closed" and "waiting" rendering paths.
|
|
2369
2448
|
*
|
|
2370
|
-
*
|
|
2371
|
-
*
|
|
2372
|
-
*
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2376
|
-
* the
|
|
2377
|
-
*
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
/**
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
/**
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
2392
|
-
*
|
|
2393
|
-
|
|
2449
|
+
* Surfaced in `fb_umbrella_no_auto_close` (impact=60): a wave umbrella
|
|
2450
|
+
* with N blockers stayed OPEN after every blocker reached a terminal
|
|
2451
|
+
* status. `--if-ready` is the cheap fix: bare `mu task close` is
|
|
2452
|
+
* unchanged (closes regardless), `--if-ready` is a no-op unless every
|
|
2453
|
+
* blocker is in a terminal status (CLOSED / REJECTED / DEFERRED).
|
|
2454
|
+
* Reject and defer satisfy the predicate too because `--if-ready`'s
|
|
2455
|
+
* job is to fire when the umbrella has nothing left to wait for, and
|
|
2456
|
+
* a rejected/deferred blocker is no longer being waited on. */
|
|
2457
|
+
interface CloseSkippedResult {
|
|
2458
|
+
/** Always 'not_ready' when set; future cause-codes can extend this
|
|
2459
|
+
* without reshaping the JSON payload (the literal-union narrows
|
|
2460
|
+
* safely in the CLI rendering path). */
|
|
2461
|
+
skipped: "not_ready";
|
|
2462
|
+
/** Status before the call (always the current status, no change). */
|
|
2463
|
+
previousStatus: TaskStatus;
|
|
2464
|
+
/** Status after the call (== previousStatus, since we no-op). */
|
|
2465
|
+
status: TaskStatus;
|
|
2466
|
+
/** Always false on a skip (no row mutated). */
|
|
2467
|
+
changed: false;
|
|
2468
|
+
/** Local ids of every blocker still in OPEN or IN_PROGRESS, sorted
|
|
2469
|
+
* alphabetically for deterministic rendering. Empty list is
|
|
2470
|
+
* impossible on this branch — the no-op only fires when ≥1
|
|
2471
|
+
* blocker is non-terminal. */
|
|
2472
|
+
blockingIds: string[];
|
|
2394
2473
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
*/
|
|
2411
|
-
|
|
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;
|
|
2474
|
+
interface CloseTaskOptions extends EvidenceOption {
|
|
2475
|
+
workstream: string;
|
|
2476
|
+
/** When true, no-op the close unless every blocker is in a terminal
|
|
2477
|
+
* status (CLOSED / REJECTED / DEFERRED). Returns a
|
|
2478
|
+
* `CloseSkippedResult` carrying the still-blocking ids; the CLI
|
|
2479
|
+
* renders the skip with a Next: hint pointing at `mu task wait`.
|
|
2480
|
+
* When false / omitted, behaves as bare `closeTask` (closes
|
|
2481
|
+
* regardless of blocker status). */
|
|
2482
|
+
ifReady?: boolean;
|
|
2483
|
+
/** Optional actor identity attributed to the synthetic `CLOSE: …`
|
|
2484
|
+
* note auto-inserted when `evidence` is non-empty (see closeTask
|
|
2485
|
+
* body). The CLI resolves this via `resolveActorIdentity()` so the
|
|
2486
|
+
* note carries the closing worker's name; SDK callers (tests,
|
|
2487
|
+
* internal use) may omit it (the note then carries no author, same
|
|
2488
|
+
* as a bare `addNote` without `--author`). Surfaced in mufeedback
|
|
2489
|
+
* task_close_evidence_does_not_append_the. */
|
|
2490
|
+
author?: string;
|
|
2419
2491
|
}
|
|
2420
|
-
/**
|
|
2421
|
-
*
|
|
2422
|
-
*
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2492
|
+
/** Convenience: setTaskStatus(db, id, "CLOSED"). Accepts evidence.
|
|
2493
|
+
* Pre-snapshots the DB (snap_design §CAPTURE STRATEGY > WHEN). Skipped
|
|
2494
|
+
* for the idempotent no-op (already CLOSED) so we don't accumulate
|
|
2495
|
+
* empty-delta snapshots on retry loops.
|
|
2496
|
+
*
|
|
2497
|
+
* With `ifReady: true`, returns a `CloseSkippedResult` (no mutation,
|
|
2498
|
+
* no snapshot) when any blocker is still OPEN / IN_PROGRESS. Used by
|
|
2499
|
+
* `mu task close --if-ready` so an orchestrator can fire-and-forget
|
|
2500
|
+
* the umbrella close after every blocker resolves without first
|
|
2501
|
+
* re-querying the graph. */
|
|
2502
|
+
declare function closeTask(db: Db, localId: string, opts: CloseTaskOptions): SetStatusResult | CloseSkippedResult;
|
|
2503
|
+
/** Convenience: setTaskStatus(db, id, "OPEN"). Owner intentionally NOT
|
|
2504
|
+
* cleared — use `releaseTask` for that. Accepts evidence. */
|
|
2505
|
+
declare function openTask(db: Db, localId: string, opts: EvidenceOption & {
|
|
2506
|
+
workstream: string;
|
|
2507
|
+
}): SetStatusResult;
|
|
2508
|
+
interface RejectDeferOptions extends EvidenceOption {
|
|
2509
|
+
/** Workstream context for the root task. All internal task lookups
|
|
2510
|
+
* (including the dependent walk) scope to this workstream. */
|
|
2511
|
+
workstream: string;
|
|
2512
|
+
/** If true, walk the transitive dependent closure and (with `yes`)
|
|
2513
|
+
* apply the same status to every dependent, atomically. Without
|
|
2514
|
+
* `yes`, runs as a dry-run: returns the list of tasks that WOULD
|
|
2515
|
+
* be swept (changedIds) with `dryRun: true` and changes nothing.
|
|
2516
|
+
* Logs one event per task (via setTaskStatus) on commit. */
|
|
2517
|
+
cascade?: boolean;
|
|
2518
|
+
/** Required to actually commit a `cascade` operation. Without it,
|
|
2519
|
+
* cascade is dry-run only — prints the affected dependents so the
|
|
2520
|
+
* caller can verify before sweeping. Mirrors `mu workstream destroy
|
|
2521
|
+
* --yes`. Surfaced in mufeedback bug_cascade_reject_too_aggressive
|
|
2522
|
+
* when an accidentally-cascaded reject swept hud_dogfood (which had
|
|
2523
|
+
* independent merit and needed reopening). */
|
|
2524
|
+
yes?: boolean;
|
|
2432
2525
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
interface TaskEdgeWithStatus {
|
|
2438
|
-
name: string;
|
|
2526
|
+
interface RejectDeferResult {
|
|
2527
|
+
/** Tasks that actually changed status, in cascade order (root first). */
|
|
2528
|
+
changedIds: string[];
|
|
2529
|
+
/** The status now stamped on every changedId. */
|
|
2439
2530
|
status: TaskStatus;
|
|
2531
|
+
/** True iff anything changed. False on a clean idempotent no-op
|
|
2532
|
+
* (root task already in target status, no dependents). */
|
|
2533
|
+
changed: boolean;
|
|
2534
|
+
/** True iff this was a `cascade` dry-run (cascade requested without
|
|
2535
|
+
* `yes`). In that case `changedIds` lists tasks that WOULD be
|
|
2536
|
+
* swept; the DB is unchanged. */
|
|
2537
|
+
dryRun: boolean;
|
|
2538
|
+
/** Tasks that would be touched by a cascade. Same as `changedIds`
|
|
2539
|
+
* on a dry-run; populated even on a commit so the caller can
|
|
2540
|
+
* report what was swept. */
|
|
2541
|
+
affectedIds: string[];
|
|
2440
2542
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2543
|
+
/** Reject a task: terminal 'won't do' (out of scope, duplicate, wontfix).
|
|
2544
|
+
* Refuses if dependents are open unless `--cascade`.
|
|
2545
|
+
* Pre-snapshots once at the verb level so a cascade onto N children
|
|
2546
|
+
* produces a single snapshot, not N. Skipped for the idempotent no-op. */
|
|
2547
|
+
declare function rejectTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
|
|
2548
|
+
/** Defer a task: parked, may revisit. Same dependent-stranding semantics
|
|
2549
|
+
* as reject (DEFERRED also doesn't satisfy a `--blocked-by` edge).
|
|
2550
|
+
* Pre-snapshots once at the verb level. Skipped for the idempotent no-op. */
|
|
2551
|
+
declare function deferTask(db: Db, localId: string, opts: RejectDeferOptions): RejectDeferResult;
|
|
2552
|
+
|
|
2553
|
+
interface ReleaseResult {
|
|
2554
|
+
/** The previous owner (null if the task was already unowned). */
|
|
2555
|
+
previousOwnerName: string | null;
|
|
2556
|
+
/** Status before the release. */
|
|
2557
|
+
previousStatus: TaskStatus;
|
|
2558
|
+
/** Status after the release. */
|
|
2559
|
+
status: TaskStatus;
|
|
2560
|
+
/** True iff owner OR status actually changed. */
|
|
2561
|
+
changed: boolean;
|
|
2448
2562
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
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;
|
|
2563
|
+
interface ReleaseTaskOptions extends EvidenceOption {
|
|
2564
|
+
/** Workstream context for the task (v5: tasks.local_id is
|
|
2565
|
+
* per-workstream unique). */
|
|
2475
2566
|
workstream: string;
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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[];
|
|
2567
|
+
/** Force `status = OPEN` regardless of the current status. Without
|
|
2568
|
+
* this flag, `IN_PROGRESS` is also flipped to `OPEN` automatically
|
|
2569
|
+
* (so a released task isn't left structurally stranded with
|
|
2570
|
+
* `owner=NULL, status=IN_PROGRESS`); CLOSED / REJECTED / DEFERRED
|
|
2571
|
+
* are preserved. `--reopen` is the override for the rarer "un-
|
|
2572
|
+
* close and hand back to the pool" workflow. */
|
|
2573
|
+
reopen?: boolean;
|
|
2488
2574
|
}
|
|
2489
2575
|
/**
|
|
2490
|
-
*
|
|
2491
|
-
* edges.
|
|
2576
|
+
* Release a task: clear `tasks.owner`.
|
|
2492
2577
|
*
|
|
2493
|
-
*
|
|
2494
|
-
*
|
|
2495
|
-
*
|
|
2578
|
+
* Status side-effects (review_release_open_in_progress_inconsistency):
|
|
2579
|
+
* - IN_PROGRESS → OPEN automatically (without it, the task is
|
|
2580
|
+
* stranded: no owner to drive it forward, but `mu task next`
|
|
2581
|
+
* skips it because it's not OPEN).
|
|
2582
|
+
* - OPEN / CLOSED / REJECTED / DEFERRED preserved.
|
|
2583
|
+
* - `--reopen` forces OPEN regardless of current status — the
|
|
2584
|
+
* escape hatch for un-closing a CLOSED owned task in one verb.
|
|
2496
2585
|
*
|
|
2497
|
-
*
|
|
2498
|
-
* no
|
|
2499
|
-
*
|
|
2586
|
+
* Idempotent: releasing an already-unowned task with no `--reopen` and
|
|
2587
|
+
* no IN_PROGRESS status is a no-op (returns `changed: false`).
|
|
2588
|
+
* Throws TaskNotFoundError on missing.
|
|
2500
2589
|
*/
|
|
2501
|
-
declare function
|
|
2502
|
-
interface
|
|
2503
|
-
/**
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
*
|
|
2590
|
+
declare function releaseTask(db: Db, localId: string, opts: ReleaseTaskOptions): ReleaseResult;
|
|
2591
|
+
interface ClaimTaskOptions extends EvidenceOption {
|
|
2592
|
+
/** Workstream context for both the task and the claiming agent.
|
|
2593
|
+
* v5: agents.name and tasks.local_id are per-workstream unique;
|
|
2594
|
+
* the task lookup AND the agent FK lookup scope to this
|
|
2595
|
+
* workstream so a same-named task or worker elsewhere can't be
|
|
2596
|
+
* silently picked. The CLI always passes this from the resolved
|
|
2597
|
+
* -w / $MU_SESSION. */
|
|
2507
2598
|
workstream: string;
|
|
2599
|
+
/**
|
|
2600
|
+
* Override the agent name. If omitted, derived from the current pane's
|
|
2601
|
+
* title via `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`.
|
|
2602
|
+
*
|
|
2603
|
+
* Mutually exclusive with `self: true`.
|
|
2604
|
+
*/
|
|
2605
|
+
agentName?: string;
|
|
2606
|
+
/**
|
|
2607
|
+
* Workstream that the claimer agent lives in. When omitted, defaults
|
|
2608
|
+
* to `opts.workstream` (today's same-workstream behaviour). Set by
|
|
2609
|
+
* the CLI when `mu task claim X -w A --for B/worker-1` qualifies the
|
|
2610
|
+
* `--for` ref with a different workstream prefix
|
|
2611
|
+
* (`task_claim_for_cross_workstream`).
|
|
2612
|
+
*
|
|
2613
|
+
* Cross-workstream ownership is structurally allowed by the schema:
|
|
2614
|
+
* `tasks.owner_id` is an INTEGER FK to `agents.id` with no
|
|
2615
|
+
* workstream qualifier on the agent side. The per-workstream UNIQUE
|
|
2616
|
+
* on `agents(workstream_id, name)` is what previously made the
|
|
2617
|
+
* SDK's name → id lookup scope to one workstream; this option
|
|
2618
|
+
* widens that lookup to a different workstream when the operator
|
|
2619
|
+
* dispatches across a workstream boundary. The agent's own
|
|
2620
|
+
* workstream remains unchanged — only the task's `owner_id` points
|
|
2621
|
+
* out-of-workstream.
|
|
2622
|
+
*/
|
|
2623
|
+
agentWorkstream?: string;
|
|
2624
|
+
/**
|
|
2625
|
+
* Anonymous claim: write `owner = NULL` instead of resolving an agent
|
|
2626
|
+
* name and checking the FK. Use when the actor is the orchestrator
|
|
2627
|
+
* (or a script, or a human) doing direct work in a workstream they
|
|
2628
|
+
* aren't a registered worker in.
|
|
2629
|
+
*
|
|
2630
|
+
* The actor name is still recorded — it ends up in `agent_logs.source`
|
|
2631
|
+
* for the auto-emitted `task claim` event — so provenance is preserved.
|
|
2632
|
+
* Just not in the FK column.
|
|
2633
|
+
*
|
|
2634
|
+
* Resolution order for the actor name (used as the log source):
|
|
2635
|
+
* 1. `actor` if explicitly passed.
|
|
2636
|
+
* 2. Current pane title (when `$TMUX_PANE` is set).
|
|
2637
|
+
* 3. `$USER`.
|
|
2638
|
+
* 4. The literal string 'unknown'.
|
|
2639
|
+
*
|
|
2640
|
+
* Mutually exclusive with `agentName` (the two are alternative
|
|
2641
|
+
* answers to "who's the actor for this claim?"). Passing both is a
|
|
2642
|
+
* usage error.
|
|
2643
|
+
*/
|
|
2644
|
+
self?: boolean;
|
|
2645
|
+
/**
|
|
2646
|
+
* Override the actor name used for the log source when `self: true`.
|
|
2647
|
+
* Ignored when `self: false`. Useful when the orchestrator wants to
|
|
2648
|
+
* attribute the work to a meaningful name rather than the pane
|
|
2649
|
+
* title (e.g. "deploy-bot" rather than "pi-mu").
|
|
2650
|
+
*/
|
|
2651
|
+
actor?: string;
|
|
2508
2652
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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;
|
|
2653
|
+
interface ClaimResult {
|
|
2654
|
+
/** The agent now owning the task, or null when the claim was anonymous (--self). */
|
|
2655
|
+
ownerName: string | null;
|
|
2656
|
+
/** The actor recorded in the agent_logs event — the agent name for a
|
|
2657
|
+
* registered-worker claim, or the resolved actor for --self. */
|
|
2658
|
+
actorName: string;
|
|
2659
|
+
/** The previous owner (null if it was unowned). */
|
|
2660
|
+
previousOwnerName: string | null;
|
|
2661
|
+
/** The status BEFORE the claim; post-claim is IN_PROGRESS unless was CLOSED. */
|
|
2662
|
+
previousStatus: TaskStatus;
|
|
2663
|
+
/** The status AFTER the claim. */
|
|
2664
|
+
status: TaskStatus;
|
|
2527
2665
|
}
|
|
2528
2666
|
/**
|
|
2529
|
-
*
|
|
2530
|
-
*
|
|
2531
|
-
*
|
|
2532
|
-
*
|
|
2667
|
+
* Claim a task. Two modes:
|
|
2668
|
+
*
|
|
2669
|
+
* Worker claim (default):
|
|
2670
|
+
* Resolve an agent name from `opts.agentName` or from $TMUX_PANE's
|
|
2671
|
+
* pane title. The name MUST exist in the agents table (FK on
|
|
2672
|
+
* tasks.owner). Sets `owner = <name>`. This is what mu-spawned
|
|
2673
|
+
* workers do, and what `mu task claim --for <worker>` does for
|
|
2674
|
+
* orchestrator dispatch.
|
|
2675
|
+
*
|
|
2676
|
+
* Anonymous claim (--self):
|
|
2677
|
+
* Skip the name -> agents FK lookup entirely. Sets `owner = NULL`.
|
|
2678
|
+
* Records the actor in `agent_logs.source` instead. This is the
|
|
2679
|
+
* orchestrator-doing-direct-work path — the actor is logged but
|
|
2680
|
+
* not registered as a worker pane.
|
|
2681
|
+
*
|
|
2682
|
+
* Status side-effect: OPEN -> IN_PROGRESS; IN_PROGRESS / CLOSED unchanged.
|
|
2683
|
+
*
|
|
2684
|
+
* Concurrency: the worker-claim path uses a single-statement CAS UPDATE
|
|
2685
|
+
* with `WHERE owner IS NULL OR owner = ?` so two workers racing to
|
|
2686
|
+
* claim the same task can't both win. The anonymous path uses
|
|
2687
|
+
* `WHERE owner IS NULL` (anonymous claims don't 'own' the task in any
|
|
2688
|
+
* exclusive sense; if it's already owned by anyone, the anonymous claim
|
|
2689
|
+
* is a TaskAlreadyOwnedError just like a worker claim would be).
|
|
2533
2690
|
*/
|
|
2534
|
-
declare function
|
|
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;
|
|
2568
|
-
}
|
|
2691
|
+
declare function claimTask(db: Db, localId: string, opts: ClaimTaskOptions): Promise<ClaimResult>;
|
|
2569
2692
|
/**
|
|
2570
|
-
*
|
|
2571
|
-
*
|
|
2572
|
-
* a missing task (returns `deleted: false`).
|
|
2693
|
+
* Resolve the current actor's identity for attribution in task notes,
|
|
2694
|
+
* --self claims, and any other write that wants 'who did this?'.
|
|
2573
2695
|
*
|
|
2574
|
-
*
|
|
2575
|
-
*
|
|
2696
|
+
* Resolution order:
|
|
2697
|
+
* 1. $MU_AGENT_NAME env var (set by mu spawnAgent on every managed
|
|
2698
|
+
* pane; surfaced from the f3d4bdd commit). Authoritative when
|
|
2699
|
+
* present — you're inside a mu-spawned worker, no ambiguity.
|
|
2700
|
+
* 2. tmux pane title (the pane-title identity step). Works
|
|
2701
|
+
* when running inside any pane mu manages OR adopted.
|
|
2702
|
+
* 3. $USER (when running outside tmux entirely).
|
|
2703
|
+
* 4. The literal 'orchestrator' as a last-resort default.
|
|
2576
2704
|
*
|
|
2577
|
-
*
|
|
2578
|
-
*
|
|
2579
|
-
*
|
|
2580
|
-
*
|
|
2581
|
-
*
|
|
2705
|
+
* Why prefer env over pane title: pane titles are a tmux-server-wide
|
|
2706
|
+
* resource that anything can rewrite. The env var is set per-pane at
|
|
2707
|
+
* spawn time and is unforgeable from outside without explicit
|
|
2708
|
+
* `--actor` override. Pane title is the only identity available for
|
|
2709
|
+
* adopted panes that didn't go through mu's spawn path.
|
|
2582
2710
|
*/
|
|
2583
|
-
declare function
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2711
|
+
declare function resolveActorIdentity(): Promise<string>;
|
|
2712
|
+
|
|
2713
|
+
declare function setWaitSleepForTests(impl: ((ms: number) => Promise<void>) | undefined): (ms: number) => Promise<void>;
|
|
2714
|
+
/** Test seam: swap the stderr writer used by the stuck-task warning so
|
|
2715
|
+
* unit tests can capture warnings without spying on process.stderr. */
|
|
2716
|
+
declare function setWaitStuckWarnForTests(impl: ((msg: string) => void) | undefined): (msg: string) => void;
|
|
2717
|
+
/** Total number of polls performed across all `waitForTasks` calls in this
|
|
2718
|
+
* process. Tests typically reset before exercising and read after. */
|
|
2719
|
+
declare function getWaitPollCount(): number;
|
|
2720
|
+
declare function resetWaitPollCount(): void;
|
|
2721
|
+
/** A single task ref the wait verb is watching. Cross-workstream
|
|
2722
|
+
* waits arrive as a heterogeneous list of (workstream, name) pairs;
|
|
2723
|
+
* the legacy single-workstream call passes the same workstream on
|
|
2724
|
+
* every ref. task_wait_cross_workstream. */
|
|
2725
|
+
interface TaskWaitRef {
|
|
2726
|
+
/** The workstream the task lives in. Each ref carries its own so
|
|
2727
|
+
* the SDK doesn't need a single "the workstream" — cross-ws waits
|
|
2728
|
+
* pass refs from multiple workstreams in one call. */
|
|
2729
|
+
workstreamName: string;
|
|
2730
|
+
/** The task's per-workstream-unique local id. */
|
|
2731
|
+
name: string;
|
|
2590
2732
|
}
|
|
2591
|
-
interface
|
|
2592
|
-
/**
|
|
2593
|
-
|
|
2594
|
-
/**
|
|
2595
|
-
*
|
|
2596
|
-
|
|
2733
|
+
interface TaskWaitOptions {
|
|
2734
|
+
/** Target status. Default 'CLOSED'. */
|
|
2735
|
+
status?: TaskStatus;
|
|
2736
|
+
/** When true, succeed as soon as ONE listed task reaches the target.
|
|
2737
|
+
* Default false: every listed task must reach the target. */
|
|
2738
|
+
any?: boolean;
|
|
2739
|
+
/** Maximum time to wait, in milliseconds. Default 600_000 (10 min).
|
|
2740
|
+
* Pass 0 to wait forever. */
|
|
2741
|
+
timeoutMs?: number;
|
|
2742
|
+
/** Polling interval. Default 1000ms; overridable for tests. */
|
|
2743
|
+
pollMs?: number;
|
|
2744
|
+
/** Workstream context applied to bare-string ids. Required when the
|
|
2745
|
+
* caller passes `string[]`; ignored when the caller passes
|
|
2746
|
+
* `TaskWaitRef[]` (each ref carries its own ws). The legacy
|
|
2747
|
+
* single-ws SDK call site keeps its today's shape; the cross-ws
|
|
2748
|
+
* callers (CLI verb) pass `TaskWaitRef[]` and omit `workstream`.
|
|
2749
|
+
* task_wait_cross_workstream. */
|
|
2750
|
+
workstream?: string;
|
|
2751
|
+
/** Emit a yellow STUCK warning to stderr (once per task per wait call)
|
|
2752
|
+
* when an IN_PROGRESS task's owner has been in `needs_input` for at
|
|
2753
|
+
* least this many milliseconds since the agent row's last update.
|
|
2754
|
+
* Default 300_000 (5 min). Pass 0 to disable.
|
|
2755
|
+
*
|
|
2756
|
+
* Surfaced by agent_close_discipline_gap in mufeedback: workers
|
|
2757
|
+
* occasionally finish + commit + go idle without running
|
|
2758
|
+
* `mu task close <id>`, leaving wait blocked indefinitely. The
|
|
2759
|
+
* warning is observation-only — wait keeps polling so the operator
|
|
2760
|
+
* (or a wrapping policy) decides whether to force-close, re-prompt,
|
|
2761
|
+
* or escalate. */
|
|
2762
|
+
stuckAfterMs?: number;
|
|
2763
|
+
/** What to do when the `--stuck-after` predicate fires on a watched
|
|
2764
|
+
* task. `'warn'` (default) = today's behaviour: yellow STUCK line
|
|
2765
|
+
* to stderr (deduped per task per wait call) + corroborating
|
|
2766
|
+
* `kind='event'` agent_logs row; wait keeps polling. `'exit'` =
|
|
2767
|
+
* same emit + persist, but THEN throw `StallDetectedDuringWaitError`
|
|
2768
|
+
* so the CLI wrapper exits 7 (STALL_DETECTED). The exit-action is
|
|
2769
|
+
* the unattended-orchestrator escape: a wrapping policy can branch
|
|
2770
|
+
* on 7 (idle, ambiguous — operator decides poke vs release) vs 6
|
|
2771
|
+
* (dead pane, unambiguous — re-dispatch).
|
|
2772
|
+
*
|
|
2773
|
+
* Carve-out (lives at the call site, not here): the CLI passes
|
|
2774
|
+
* `'exit'` only when the wait target is CLOSED — mirrors exit-6's
|
|
2775
|
+
* reaper-flip suppression. With `--status OPEN` the worker reaching
|
|
2776
|
+
* needs_input might BE the success path. See
|
|
2777
|
+
* task_wait_stall_action_flag. */
|
|
2778
|
+
onStall?: "warn" | "exit";
|
|
2779
|
+
/** Optional async hook run BEFORE every snapshot (initial + each
|
|
2780
|
+
* poll iteration). The CLI uses this to reconcile the workstream
|
|
2781
|
+
* each tick (reaper flips IN_PROGRESS → OPEN for dead-pane
|
|
2782
|
+
* workers) and to throw a typed error when a reaper-flip on a
|
|
2783
|
+
* watched task should abandon the wait — see
|
|
2784
|
+
* task_wait_reconcile_dead_panes. Throwing from `beforePoll`
|
|
2785
|
+
* propagates out of `waitForTasks` unchanged.
|
|
2786
|
+
*
|
|
2787
|
+
* Kept as a generic seam (not a `--reconcile`-shaped option) so
|
|
2788
|
+
* the SDK module stays free of tmux/reconcile imports — that
|
|
2789
|
+
* layering belongs above the SDK in the CLI wrapper. */
|
|
2790
|
+
beforePoll?: () => Promise<void>;
|
|
2791
|
+
}
|
|
2792
|
+
interface TaskWaitTaskState {
|
|
2793
|
+
/** The workstream this task lives in. Cross-workstream waits
|
|
2794
|
+
* return a mixed list; the workstream is part of identity.
|
|
2795
|
+
* task_wait_cross_workstream. */
|
|
2796
|
+
workstreamName: string;
|
|
2797
|
+
/** The task's per-workstream-unique name. */
|
|
2798
|
+
name: string;
|
|
2799
|
+
/** Current status (at the moment we exit). */
|
|
2800
|
+
status: TaskStatus;
|
|
2801
|
+
/** Owner at exit time (NULL when unowned, after release, or after
|
|
2802
|
+
* the reaper flipped IN_PROGRESS → OPEN due to a dead pane). */
|
|
2803
|
+
owner: string | null;
|
|
2804
|
+
/** True when this task's status equals the target. */
|
|
2805
|
+
reachedTarget: boolean;
|
|
2806
|
+
/** True when the task is IN_PROGRESS, owned by a registered agent
|
|
2807
|
+
* whose detected status is `needs_input` for >= `stuckAfterMs`.
|
|
2808
|
+
* Surfaces the agent_close_discipline_gap pattern: worker finished +
|
|
2809
|
+
* committed but skipped `mu task close <id>`. Backwards-compatible
|
|
2810
|
+
* signal — callers ignoring it see no behaviour change. */
|
|
2811
|
+
stuck: boolean;
|
|
2812
|
+
}
|
|
2813
|
+
interface TaskWaitResult {
|
|
2814
|
+
/** Per-task state at exit time. Same length and order as the input
|
|
2815
|
+
* list. The caller derives all-reached / any-reached / elapsed
|
|
2816
|
+
* from this list (count `r.reachedTarget`) and from its own
|
|
2817
|
+
* startedAt clock — keeping the SDK return minimal. */
|
|
2818
|
+
refs: TaskWaitTaskState[];
|
|
2819
|
+
/** True when we exited because of the timeout, not because the wait
|
|
2820
|
+
* condition was met. Refs that did reach the target are still
|
|
2821
|
+
* reflected in `refs[i].reachedTarget` on partial-progress timeout. */
|
|
2822
|
+
timedOut: boolean;
|
|
2597
2823
|
}
|
|
2598
2824
|
/**
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2825
|
+
* Block until a set of tasks reaches `opts.status` (default CLOSED).
|
|
2826
|
+
* Returns a result describing the final state — the caller decides
|
|
2827
|
+
* whether to treat partial-progress timeouts as success or failure
|
|
2828
|
+
* (the CLI maps a clean exit to 0, a timeout to 5).
|
|
2602
2829
|
*
|
|
2603
|
-
*
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2830
|
+
* Pre-flight: every task in `localIds` MUST exist; missing ones throw
|
|
2831
|
+
* TaskNotFoundError before any waiting begins. This is loud-fail by
|
|
2832
|
+
* design — a typo'd id silently waiting forever is the worst-case UX.
|
|
2606
2833
|
*/
|
|
2607
|
-
|
|
2608
|
-
|
|
2834
|
+
declare function waitForTasks(db: Db, input: readonly TaskWaitRef[] | readonly string[], opts: TaskWaitOptions): Promise<TaskWaitResult>;
|
|
2835
|
+
|
|
2836
|
+
interface FullDag {
|
|
2837
|
+
/** Root tasks: no incoming `blocks` edge (no blockers). */
|
|
2838
|
+
roots: TaskRow[];
|
|
2839
|
+
/** Edges map parent task name → child task names (what parent blocks). */
|
|
2840
|
+
edges: Map<string, string[]>;
|
|
2841
|
+
/** All tasks in the workstream, keyed by operator-facing name. */
|
|
2842
|
+
tasks: Map<string, TaskRow>;
|
|
2609
2843
|
}
|
|
2610
|
-
|
|
2611
|
-
interface
|
|
2612
|
-
/**
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2844
|
+
type TaskStatusLabelFn = (task: TaskRow) => string;
|
|
2845
|
+
interface RenderTreeOptions {
|
|
2846
|
+
/** Include the task title after the name + status label. Default: true. */
|
|
2847
|
+
includeTitle?: boolean;
|
|
2848
|
+
}
|
|
2849
|
+
interface LoadFullDagOptions {
|
|
2850
|
+
/** Optional visible-status filter. Omitted = every task status. */
|
|
2851
|
+
statuses?: ReadonlySet<TaskStatus>;
|
|
2616
2852
|
}
|
|
2853
|
+
declare function loadFullDag(db: Db, workstream: string, opts?: LoadFullDagOptions): FullDag;
|
|
2617
2854
|
/**
|
|
2618
|
-
*
|
|
2619
|
-
*
|
|
2620
|
-
*
|
|
2621
|
-
*
|
|
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.
|
|
2625
|
-
*
|
|
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.
|
|
2855
|
+
* Render a DAG forest in the same ASCII shape as `mu task tree --down`:
|
|
2856
|
+
* each root is printed as a header node, dependents are below it, and
|
|
2857
|
+
* DAG diamonds collapse after the first full subtree render with a
|
|
2858
|
+
* one-line recurrence marker.
|
|
2630
2859
|
*/
|
|
2631
|
-
declare function
|
|
2632
|
-
|
|
2633
|
-
}): ReparentTaskResult;
|
|
2860
|
+
declare function renderForest(roots: readonly TaskRow[], edges: ReadonlyMap<string, readonly string[]>, statusFn: TaskStatusLabelFn, tasksByName?: ReadonlyMap<string, TaskRow>, opts?: RenderTreeOptions): string;
|
|
2861
|
+
declare function renderTaskTree(db: Db, workstream: string, root: TaskRow, direction: "blockers" | "dependents", statusFn: TaskStatusLabelFn, opts?: RenderTreeOptions): string;
|
|
2634
2862
|
|
|
2635
2863
|
interface Track {
|
|
2636
2864
|
/** Goal tasks (no outgoing edges) belonging to this track. */
|
|
@@ -2651,10 +2879,18 @@ interface Track {
|
|
|
2651
2879
|
*/
|
|
2652
2880
|
declare function getParallelTracks(db: Db, workstream: string): Track[];
|
|
2653
2881
|
|
|
2882
|
+
declare const EXPORT_MANIFEST_VERSION = 2;
|
|
2654
2883
|
/** One per-task summary inside a per-source-ws section of the manifest. */
|
|
2655
2884
|
interface ExportTaskEntry {
|
|
2656
|
-
/** Task local_id == filename stem (`<id>.md`). */
|
|
2885
|
+
/** Task local_id == filename stem (`<id>.md`). Kept for v1 manifest compatibility. */
|
|
2657
2886
|
id: string;
|
|
2887
|
+
/** Task local_id, duplicated under the operator-facing SDK name so bucket INDEX can render from manifest alone. */
|
|
2888
|
+
name: string;
|
|
2889
|
+
/** Compact summary fields needed for bucket-level INDEX.md without re-reading the DB. */
|
|
2890
|
+
title: string;
|
|
2891
|
+
status: TaskRow["status"];
|
|
2892
|
+
impact: number;
|
|
2893
|
+
effortDays: number;
|
|
2658
2894
|
/** Path relative to the bucket root (e.g. `auth/tasks/design.md`). */
|
|
2659
2895
|
path: string;
|
|
2660
2896
|
/** sha256 of the markdown body bytes; idempotency key. */
|
|
@@ -2677,12 +2913,16 @@ interface ExportSourceManifest {
|
|
|
2677
2913
|
/** Per-task entries; sorted by id for stable diffs. */
|
|
2678
2914
|
tasks: ExportTaskEntry[];
|
|
2679
2915
|
}
|
|
2680
|
-
/** Top-level bucket manifest. `bucketVersion: 2` — the v0.3
|
|
2681
|
-
*
|
|
2682
|
-
*
|
|
2916
|
+
/** Top-level bucket manifest. `bucketVersion: 2` — the v0.3 disk layout.
|
|
2917
|
+
* `manifest_version` is the schema of the manifest JSON payload itself:
|
|
2918
|
+
* v1 lacked task summaries, v2 stores enough per-task data to render
|
|
2919
|
+
* bucket INDEX.md from `manifest.sources` alone. Manifests without
|
|
2920
|
+
* `bucketVersion: 2` fall through to the `corrupt` lane in `readManifest`. */
|
|
2683
2921
|
interface ExportManifest {
|
|
2684
|
-
/**
|
|
2922
|
+
/** Disk-layout discriminator. Always 2 in this codebase. */
|
|
2685
2923
|
bucketVersion: 2;
|
|
2924
|
+
/** Manifest-payload discriminator. Always 2 when written by this codebase. */
|
|
2925
|
+
manifest_version: typeof EXPORT_MANIFEST_VERSION;
|
|
2686
2926
|
/** Operator-chosen bucket label (an archive label, or null for a
|
|
2687
2927
|
* one-shot `mu workstream export`). Surfaced in README only. */
|
|
2688
2928
|
bucketLabel: string | null;
|
|
@@ -2865,6 +3105,14 @@ interface WorkstreamOptions {
|
|
|
2865
3105
|
* unset. */
|
|
2866
3106
|
resolveBackend?: (name: VcsBackendName) => VcsBackend;
|
|
2867
3107
|
}
|
|
3108
|
+
interface DestroyWorkstreamOptions extends WorkstreamOptions {
|
|
3109
|
+
/** Skip the per-workstream pre-mutation snapshot because the caller
|
|
3110
|
+
* already captured a broader snapshot for the whole destructive
|
|
3111
|
+
* operation. Used by `mu workstream destroy --empty` after its
|
|
3112
|
+
* sweep-level safety snapshot; direct destroy callers leave this
|
|
3113
|
+
* false so the default safety net is unchanged. */
|
|
3114
|
+
suppressSnapshot?: boolean;
|
|
3115
|
+
}
|
|
2868
3116
|
/**
|
|
2869
3117
|
* Discover every workstream visible on this machine. The union of:
|
|
2870
3118
|
* - rows in the `workstreams` table (canonical DB source; populated by
|
|
@@ -2888,7 +3136,7 @@ declare function summarizeWorkstream(db: Db, opts: WorkstreamOptions): Promise<W
|
|
|
2888
3136
|
* to call repeatedly. Returns counts so the caller can print a useful
|
|
2889
3137
|
* summary.
|
|
2890
3138
|
*/
|
|
2891
|
-
declare function destroyWorkstream(db: Db, opts:
|
|
3139
|
+
declare function destroyWorkstream(db: Db, opts: DestroyWorkstreamOptions): Promise<DestroyResult>;
|
|
2892
3140
|
interface ExportWorkstreamOptions {
|
|
2893
3141
|
workstream: string;
|
|
2894
3142
|
/** Output directory (the bucket). Defaults to `./<workstream>/`
|
|
@@ -2996,268 +3244,6 @@ interface ImportBucketResult {
|
|
|
2996
3244
|
*/
|
|
2997
3245
|
declare function importBucket(db: Db, opts: ImportBucketOptions): ImportBucketResult;
|
|
2998
3246
|
|
|
2999
|
-
interface WorkspaceRow {
|
|
3000
|
-
agentName: string;
|
|
3001
|
-
workstreamName: string;
|
|
3002
|
-
backend: VcsBackendName;
|
|
3003
|
-
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;
|
|
3013
|
-
}
|
|
3014
|
-
declare class WorkspaceExistsError extends Error implements HasNextSteps {
|
|
3015
|
-
readonly agent: string;
|
|
3016
|
-
readonly name = "WorkspaceExistsError";
|
|
3017
|
-
constructor(agent: string);
|
|
3018
|
-
errorNextSteps(): NextStep[];
|
|
3019
|
-
}
|
|
3020
|
-
declare class WorkspaceNotFoundError extends Error implements HasNextSteps {
|
|
3021
|
-
readonly agent: string;
|
|
3022
|
-
readonly name = "WorkspaceNotFoundError";
|
|
3023
|
-
constructor(agent: string);
|
|
3024
|
-
errorNextSteps(): NextStep[];
|
|
3025
|
-
}
|
|
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[];
|
|
3048
|
-
}
|
|
3049
|
-
/**
|
|
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).
|
|
3061
|
-
*
|
|
3062
|
-
* Maps to exit code 4 (conflict).
|
|
3063
|
-
*/
|
|
3064
|
-
declare class HomeDirAsProjectRootError extends Error implements HasNextSteps {
|
|
3065
|
-
readonly agent: string;
|
|
3066
|
-
readonly workstream: string;
|
|
3067
|
-
readonly homeDir: string;
|
|
3068
|
-
readonly name = "HomeDirAsProjectRootError";
|
|
3069
|
-
constructor(agent: string, workstream: string, homeDir: string);
|
|
3070
|
-
errorNextSteps(): NextStep[];
|
|
3071
|
-
}
|
|
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;
|
|
3102
|
-
}
|
|
3103
|
-
/**
|
|
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`.
|
|
3126
|
-
*
|
|
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.
|
|
3134
|
-
*
|
|
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.
|
|
3138
|
-
*/
|
|
3139
|
-
declare function listAllOrphanWorkspaces(db: Db): StrandedWorkspaceOrphan[];
|
|
3140
|
-
interface CreateWorkspaceOptions {
|
|
3141
|
-
agent: string;
|
|
3142
|
-
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;
|
|
3159
|
-
}
|
|
3160
|
-
/**
|
|
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.
|
|
3166
|
-
*/
|
|
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
|
-
}
|
|
3191
|
-
/**
|
|
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).
|
|
3195
|
-
*/
|
|
3196
|
-
declare function freeWorkspace(db: Db, agent: string, opts: FreeWorkspaceOptions & {
|
|
3197
|
-
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;
|
|
3214
|
-
}
|
|
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;
|
|
3222
|
-
}
|
|
3223
|
-
/**
|
|
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`).
|
|
3256
|
-
*/
|
|
3257
|
-
declare function recreateWorkspace(db: Db, agent: string, opts: RecreateWorkspaceOptions & {
|
|
3258
|
-
workstream: string;
|
|
3259
|
-
}): Promise<RecreateWorkspaceResult>;
|
|
3260
|
-
|
|
3261
3247
|
type LogKind = "message" | "event" | "broadcast" | string;
|
|
3262
3248
|
interface LogRow {
|
|
3263
3249
|
/** Monotonic AUTOINCREMENT id. Use as the cursor for `--since`. */
|
|
@@ -3325,6 +3311,213 @@ declare function latestSeq(db: Db): number;
|
|
|
3325
3311
|
* by callers like `claimTask` (source = the claiming agent).
|
|
3326
3312
|
*/
|
|
3327
3313
|
declare function emitEvent(db: Db, workstream: string | null, payload: string, source?: string): void;
|
|
3314
|
+
/**
|
|
3315
|
+
* Canonical list of two-token verb prefixes that `emitEvent` callers
|
|
3316
|
+
* use as the leading words of a payload. Single source of truth for
|
|
3317
|
+
* event renderers so they can never drift away from the actual emitter
|
|
3318
|
+
* sites.
|
|
3319
|
+
*
|
|
3320
|
+
* Maintenance contract: when you add an `emitEvent(...)` call whose
|
|
3321
|
+
* payload starts with a new two-word verb, add the verb here. A
|
|
3322
|
+
* regression test walks every entry and asserts the classifier
|
|
3323
|
+
* recognises it; the test fails if you add an emitter without adding
|
|
3324
|
+
* its verb here.
|
|
3325
|
+
*
|
|
3326
|
+
* Audit (2026-05): every `emitEvent` callsite under src/ produces a
|
|
3327
|
+
* payload that starts with one of these. Verified by
|
|
3328
|
+
* `grep -rn emitEvent src/ | grep -v import`.
|
|
3329
|
+
*/
|
|
3330
|
+
declare const EVENT_VERB_PREFIXES: readonly string[];
|
|
3331
|
+
interface ClassifiedEvent {
|
|
3332
|
+
/** One of EVENT_VERB_PREFIXES. */
|
|
3333
|
+
verb: string;
|
|
3334
|
+
/** Payload past the verb token; preserves leading separator (" " or "\t"). */
|
|
3335
|
+
rest: string;
|
|
3336
|
+
}
|
|
3337
|
+
/**
|
|
3338
|
+
* Match `payload` against EVENT_VERB_PREFIXES. Returns {verb, rest} on
|
|
3339
|
+
* match; null otherwise. The verb-boundary check is `next is space, tab,
|
|
3340
|
+
* or end-of-string` so we don't false-match e.g. `task addnote`.
|
|
3341
|
+
*
|
|
3342
|
+
* Pure parser. Consumers (the static state card, the ink Activity-log
|
|
3343
|
+
* card) apply their own colour to `verb` after matching.
|
|
3344
|
+
*/
|
|
3345
|
+
declare function classifyEventVerb(payload: string): ClassifiedEvent | null;
|
|
3346
|
+
|
|
3347
|
+
type DoctorStatus = "ok" | "warn" | "fail";
|
|
3348
|
+
interface DoctorCheck {
|
|
3349
|
+
/** Short, stable identifier — used as the row label. Lowercase
|
|
3350
|
+
* one-word tokens so the column-aligned card layout looks tidy. */
|
|
3351
|
+
name: string;
|
|
3352
|
+
status: DoctorStatus;
|
|
3353
|
+
/** Free-form prose for the row's right-hand column. Kept short so
|
|
3354
|
+
* the card's CLIP column doesn't truncate it on common widths. */
|
|
3355
|
+
detail: string;
|
|
3356
|
+
}
|
|
3357
|
+
interface DoctorSummary {
|
|
3358
|
+
/** Every check that ran, in stable display order. The card filters
|
|
3359
|
+
* to non-OK rows for its body but keeps the OK rows so the popup
|
|
3360
|
+
* (when it ships under feat_more_cards_umbrella) can render the
|
|
3361
|
+
* full list. */
|
|
3362
|
+
checks: readonly DoctorCheck[];
|
|
3363
|
+
/** Convenience: how many rows are warn or fail. Card subtitle
|
|
3364
|
+
* reads this directly. Pure derivation from `checks`. */
|
|
3365
|
+
problemCount: number;
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Compute the doctor summary for a workstream. Pure-ish: runs cheap
|
|
3369
|
+
* synchronous DB queries + reads from the supplied snapshot. Callers
|
|
3370
|
+
* that don't want to compute a snapshot first (or are running inside
|
|
3371
|
+
* `loadWorkstreamSnapshot` mid-build) can omit `snapshot` — the
|
|
3372
|
+
* snapshot-derived checks (ghosts / orphans / workspace-orphans) are
|
|
3373
|
+
* skipped in that case.
|
|
3374
|
+
*/
|
|
3375
|
+
declare function loadDoctorSummary(db: Db, snapshot: WorkstreamSnapshot | null): DoctorSummary;
|
|
3376
|
+
/** Count of warn + fail rows. Pure; exported for unit tests. */
|
|
3377
|
+
declare function countProblems(checks: readonly DoctorCheck[]): number;
|
|
3378
|
+
/**
|
|
3379
|
+
* Return the full check array (OK + warn + fail) in stable display
|
|
3380
|
+
* order. Used by the TUI's slot-9 Doctor popup
|
|
3381
|
+
* (feat_popup_9_doctor, workstream `tui-impl`) which renders every
|
|
3382
|
+
* row — not just the non-OK subset Card 9 surfaces.
|
|
3383
|
+
*
|
|
3384
|
+
* Thin wrapper over `loadDoctorSummary` so the SDK seam stays
|
|
3385
|
+
* single: `loadDoctorSummary` is the source of truth for the
|
|
3386
|
+
* check vocabulary, and the popup's `loadDoctorChecks` view is
|
|
3387
|
+
* just `.checks`. Pure-ish (same cheap synchronous DB reads as
|
|
3388
|
+
* `loadDoctorSummary`).
|
|
3389
|
+
*/
|
|
3390
|
+
declare function loadDoctorChecks(db: Db, snapshot: WorkstreamSnapshot | null): readonly DoctorCheck[];
|
|
3391
|
+
/**
|
|
3392
|
+
* Map a check row to the most useful informational command the
|
|
3393
|
+
* operator might paste. Read-only by construction: `mu agent list`,
|
|
3394
|
+
* `mu workspace orphans`, `mu doctor` are all SELECT-shape verbs;
|
|
3395
|
+
* `# ...` lines are visibly inert. Per the slot-9 popup spec KEY
|
|
3396
|
+
* MAP block this is INFORMATIONAL, never a mutating recipe — so
|
|
3397
|
+
* even when the check is `fail`, we yank the diagnostic verb the
|
|
3398
|
+
* operator should RUN MANUALLY, not a fix command.
|
|
3399
|
+
*
|
|
3400
|
+
* Pure; exported for unit tests + SDK reuse.
|
|
3401
|
+
*/
|
|
3402
|
+
declare function yankCommandForCheck(check: Pick<DoctorCheck, "name" | "status">): string;
|
|
3403
|
+
/**
|
|
3404
|
+
* A short paragraph (one paragraph per check name) explaining the
|
|
3405
|
+
* shape of the failure / warning. Returned as a `readonly string[]`
|
|
3406
|
+
* so the popup's drill body can interleave the lines with other
|
|
3407
|
+
* content; CLI consumers can `.join("\n")` themselves.
|
|
3408
|
+
*
|
|
3409
|
+
* Pure; exported for unit tests + SDK reuse.
|
|
3410
|
+
*/
|
|
3411
|
+
declare function remediationParagraph(check: DoctorCheck): readonly string[];
|
|
3412
|
+
|
|
3413
|
+
interface WorkstreamSnapshot {
|
|
3414
|
+
workstreamName: string;
|
|
3415
|
+
view: LiveAgentsView;
|
|
3416
|
+
tracks: Track[];
|
|
3417
|
+
ready: TaskRow[];
|
|
3418
|
+
inProgress: TaskRow[];
|
|
3419
|
+
blocked: TaskRow[];
|
|
3420
|
+
recentClosed: TaskRow[];
|
|
3421
|
+
/** Populated only when callers explicitly pass `withAllTasks: true`.
|
|
3422
|
+
* The TUI dashboard fast tick leaves this empty and the all-tasks
|
|
3423
|
+
* popup reads its exhaustive list directly from SQLite while open. */
|
|
3424
|
+
allTasks: TaskRow[];
|
|
3425
|
+
workspaces: WorkspaceRow[];
|
|
3426
|
+
workspaceOrphans: WorkspaceOrphan[];
|
|
3427
|
+
recent: LogRow[];
|
|
3428
|
+
/** Last N commits from the project root (process.cwd()), populated
|
|
3429
|
+
* when `loadWorkstreamSnapshot` is called with withRecentCommits.
|
|
3430
|
+
* This is intentionally NOT a per-agent workspace log. */
|
|
3431
|
+
recentCommits: CommitSummary[];
|
|
3432
|
+
/** Backend that produced recentCommits. Null when recent commits were
|
|
3433
|
+
* not requested or no VCS backend was detected. */
|
|
3434
|
+
commitsBackend?: VcsBackendName | null;
|
|
3435
|
+
/** Populated when `loadWorkstreamSnapshot` is called with
|
|
3436
|
+
* `withDoctor: true`. Used by the TUI's slot-9 Doctor card to
|
|
3437
|
+
* render a glanceable health badge on the dashboard
|
|
3438
|
+
* (feat_card_9_doctor, workstream `tui-impl`). The static `mu
|
|
3439
|
+
* state` card and `mu doctor` itself don't consume it — they
|
|
3440
|
+
* read the textual doctor card directly. Null when not requested. */
|
|
3441
|
+
doctor: DoctorSummary | null;
|
|
3442
|
+
}
|
|
3443
|
+
interface LoadWorkstreamSnapshotOptions {
|
|
3444
|
+
/** Recent-events cap (default 200). */
|
|
3445
|
+
eventLimit?: number;
|
|
3446
|
+
/** When true, slow snapshot loading also populates `WorkspaceRow.dirty`
|
|
3447
|
+
* via decorateWithDirty (one `git status --porcelain` shellout per row,
|
|
3448
|
+
* capped at DECORATE_CONCURRENCY). The TUI caches this slow-tier value
|
|
3449
|
+
* and merges it into every fast SQL tick. */
|
|
3450
|
+
withDirty?: boolean;
|
|
3451
|
+
/** When true, slow snapshot loading also populates
|
|
3452
|
+
* `WorkstreamSnapshot.doctor` via `loadDoctorSummary`. The summary is
|
|
3453
|
+
* cheap SQL, but it reports tmux/workspace drift from slow-tier fields,
|
|
3454
|
+
* so the TUI refreshes it with the subprocess tier. */
|
|
3455
|
+
withDoctor?: boolean;
|
|
3456
|
+
/** Optional full task list for the TUI all-tasks popup. */
|
|
3457
|
+
withAllTasks?: true;
|
|
3458
|
+
/** Optional recent-project-commits slice for the TUI Commits card /
|
|
3459
|
+
* popup. Uses process.cwd() as the project root on purpose: the TUI
|
|
3460
|
+
* is launched from the project checkout, while worker workspaces live
|
|
3461
|
+
* elsewhere under the mu state dir. */
|
|
3462
|
+
withRecentCommits?: {
|
|
3463
|
+
limit: number;
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
interface WorkstreamSnapshotSlowFields {
|
|
3467
|
+
view: LiveAgentsView;
|
|
3468
|
+
/** Workspace rows decorated with slow-tier VCS observations
|
|
3469
|
+
* (`commitsBehindMain`, and `dirty` when requested). */
|
|
3470
|
+
workspaces: WorkspaceRow[];
|
|
3471
|
+
recentCommits: CommitSummary[];
|
|
3472
|
+
commitsBackend?: VcsBackendName | null;
|
|
3473
|
+
doctor: DoctorSummary | null;
|
|
3474
|
+
}
|
|
3475
|
+
/**
|
|
3476
|
+
* Fast TUI/state snapshot tier: pure SQLite reads only. Subprocess-backed
|
|
3477
|
+
* fields are intentionally empty placeholders so callers can merge the last
|
|
3478
|
+
* slow-tier values without blocking a 1s render tick on tmux or VCS probes.
|
|
3479
|
+
*/
|
|
3480
|
+
declare function loadWorkstreamSnapshotFast(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions): Promise<WorkstreamSnapshot>;
|
|
3481
|
+
/**
|
|
3482
|
+
* Slow snapshot tier: fields backed by tmux / VCS subprocess probes (plus
|
|
3483
|
+
* doctor, which reports over those slow-tier observations). Returns only the
|
|
3484
|
+
* fields the fast snapshot deliberately leaves empty or undecorated.
|
|
3485
|
+
*
|
|
3486
|
+
* status-only refresh: don't prune mid-spawn placeholders or reap
|
|
3487
|
+
* unreachable agents — every render-mode is a polling read surface.
|
|
3488
|
+
*/
|
|
3489
|
+
declare function loadWorkstreamSnapshotSlow(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions, baseSnapshot?: WorkstreamSnapshot): Promise<WorkstreamSnapshotSlowFields>;
|
|
3490
|
+
/** Merge the latest slow-tier subprocess observations into a fresh fast tier. */
|
|
3491
|
+
declare function mergeSnapshotFastSlow(fast: WorkstreamSnapshot, slow: WorkstreamSnapshotSlowFields | null): WorkstreamSnapshot;
|
|
3492
|
+
/**
|
|
3493
|
+
* Back-compat wrapper for non-TUI callers: return the historical union shape
|
|
3494
|
+
* by composing the new fast SQL tier with one slow subprocess tier.
|
|
3495
|
+
*/
|
|
3496
|
+
declare function loadWorkstreamSnapshot(db: Db, workstream: string, opts?: LoadWorkstreamSnapshotOptions): Promise<WorkstreamSnapshot>;
|
|
3497
|
+
/**
|
|
3498
|
+
* ROI tiers used to colour task rows. Pure: returns the bucket name; the
|
|
3499
|
+
* consumer maps bucket → picocolors function (or ink text colour).
|
|
3500
|
+
* Magic numbers (≥100 high, ≥50 mid) lifted from the previous HUD impl.
|
|
3501
|
+
*/
|
|
3502
|
+
type RoiBucket = "high" | "mid" | "low" | "infinite";
|
|
3503
|
+
declare function roiBucket(impact: number, effortDays: number): RoiBucket;
|
|
3504
|
+
/** Histogram of agents by status. Pure derivation (no colour render). */
|
|
3505
|
+
declare function agentStatusHistogram(agents: readonly AgentRow[]): ReadonlyMap<AgentStatus, number>;
|
|
3506
|
+
interface OwnedTasksSummary {
|
|
3507
|
+
/** Display token: "—" (none) | "<task_id>" (one) | "⊕<N>" (many). */
|
|
3508
|
+
bit: string;
|
|
3509
|
+
/** Underlying count for callers that want their own format. */
|
|
3510
|
+
count: number;
|
|
3511
|
+
/** The single owned task's local id, when count===1. */
|
|
3512
|
+
onlyTaskId?: string;
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* Per-agent task summary: condensed display token + raw count. Used by
|
|
3516
|
+
* both the static Agents table and the ink Agents card. Pure on the
|
|
3517
|
+
* input rows — caller (e.g. loadWorkstreamSnapshot consumer) does the
|
|
3518
|
+
* listTasksByOwner query upstream and feeds the rows in.
|
|
3519
|
+
*/
|
|
3520
|
+
declare function summarizeOwnedTasks(owned: readonly TaskRow[]): OwnedTasksSummary;
|
|
3328
3521
|
|
|
3329
3522
|
interface SnapshotRow {
|
|
3330
3523
|
/** Operator-facing snapshot id. EXCEPTION to the no-surrogate-ids rule:
|
|
@@ -3368,13 +3561,6 @@ declare class SnapshotNotFoundError extends Error implements HasNextSteps {
|
|
|
3368
3561
|
constructor(snapshotId: number);
|
|
3369
3562
|
errorNextSteps(): NextStep[];
|
|
3370
3563
|
}
|
|
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
3564
|
declare class SnapshotVersionMismatchError extends Error implements HasNextSteps {
|
|
3379
3565
|
readonly snapshotId: number;
|
|
3380
3566
|
readonly snapshotVersion: number;
|
|
@@ -3383,11 +3569,6 @@ declare class SnapshotVersionMismatchError extends Error implements HasNextSteps
|
|
|
3383
3569
|
constructor(snapshotId: number, snapshotVersion: number, currentVersion: number);
|
|
3384
3570
|
errorNextSteps(): NextStep[];
|
|
3385
3571
|
}
|
|
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
3572
|
declare class SnapshotFileMissingError extends Error implements HasNextSteps {
|
|
3392
3573
|
readonly snapshotId: number;
|
|
3393
3574
|
readonly dbPath: string;
|
|
@@ -3395,169 +3576,47 @@ declare class SnapshotFileMissingError extends Error implements HasNextSteps {
|
|
|
3395
3576
|
constructor(snapshotId: number, dbPath: string);
|
|
3396
3577
|
errorNextSteps(): NextStep[];
|
|
3397
3578
|
}
|
|
3398
|
-
/** Read the operator-tunable count cap (`MU_SNAPSHOT_KEEP_LAST`). */
|
|
3399
3579
|
declare function gcMaxCount(): number;
|
|
3400
|
-
/** Read the operator-tunable age cap (`MU_SNAPSHOT_MAX_AGE_DAYS`). */
|
|
3401
3580
|
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
3581
|
declare function snapshotsDir(db?: Db): string;
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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
|
-
*/
|
|
3582
|
+
declare function isStaleVersion(row: {
|
|
3583
|
+
schemaVersion: number;
|
|
3584
|
+
}): boolean;
|
|
3585
|
+
declare function snapshotFileSize(snapshot: SnapshotRow): number | null;
|
|
3586
|
+
|
|
3436
3587
|
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
3588
|
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
3589
|
declare function gcSnapshots(db: Db): {
|
|
3480
3590
|
deletedRows: number;
|
|
3481
3591
|
deletedFiles: number;
|
|
3482
3592
|
};
|
|
3593
|
+
|
|
3483
3594
|
type PruneMode = "gc" | "keep-last" | "older-than" | "stale-version" | "all";
|
|
3484
3595
|
interface PruneOptions {
|
|
3485
3596
|
mode: PruneMode;
|
|
3486
|
-
/** For mode='keep-last'. Required by the CLI. */
|
|
3487
3597
|
keepLast?: number;
|
|
3488
|
-
/** For mode='older-than'. Days; required by the CLI. */
|
|
3489
3598
|
olderThanDays?: number;
|
|
3490
|
-
/** When true, return the would-delete shape but don't touch the DB
|
|
3491
|
-
* or the on-disk .db files. */
|
|
3492
3599
|
dryRun?: boolean;
|
|
3493
3600
|
}
|
|
3494
3601
|
interface PruneResult {
|
|
3495
|
-
/** Rows that would be / were deleted. Always populated, even on
|
|
3496
|
-
* dry-run (the CLI's summary uses it). */
|
|
3497
3602
|
victims: SnapshotRow[];
|
|
3498
|
-
/** Total bytes that would be / were freed (sum of victim file
|
|
3499
|
-
* sizes; missing files contribute 0). */
|
|
3500
3603
|
freedBytes: number;
|
|
3501
|
-
/** Number of `snapshots` rows actually deleted. 0 on dry-run. */
|
|
3502
3604
|
deletedRows: number;
|
|
3503
|
-
/** Number of on-disk .db files actually unlinked. 0 on dry-run. */
|
|
3504
3605
|
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
3606
|
safetyNetSnapshotId?: number;
|
|
3508
3607
|
}
|
|
3509
3608
|
declare class PruneOptionsInvalidError extends Error implements HasNextSteps {
|
|
3510
3609
|
readonly name = "PruneOptionsInvalidError";
|
|
3511
3610
|
errorNextSteps(): NextStep[];
|
|
3512
3611
|
}
|
|
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
3612
|
declare function pruneSnapshots(db: Db, opts: PruneOptions): PruneResult;
|
|
3538
3613
|
interface DeleteSnapshotResult {
|
|
3539
|
-
/** Always true on success. (Misses raise SnapshotNotFoundError; the
|
|
3540
|
-
* shape mirrors `deleteTask`'s structured-result style.) */
|
|
3541
3614
|
deleted: true;
|
|
3542
|
-
/** 1 if the .db file was on disk + unlinked; 0 if it was already
|
|
3543
|
-
* gone (orphaned row). */
|
|
3544
3615
|
deletedFiles: 0 | 1;
|
|
3545
|
-
/** Bytes freed by unlinking the .db file. 0 when the file was
|
|
3546
|
-
* already gone. */
|
|
3547
3616
|
freedBytes: number;
|
|
3548
3617
|
}
|
|
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
3618
|
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
3619
|
|
|
3563
|
-
|
|
3620
|
+
declare function restoreSnapshot(db: Db, snapshotId: number): RestoreSnapshotResult;
|
|
3621
|
+
|
|
3622
|
+
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 ClassifiedEvent, type CloseAgentOptions, type CloseAgentResult, type CommandResolutionResult, type CommandResolver, CrossWorkstreamEdgeError, CycleError, type Db, 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 ExportManifest, type ExportResult, type ExportSource, type ExportSourceManifest, type ExportTaskEntry, type ExportWorkstreamOptions, type FreeAgentResult, type FullDag, 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 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 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, WorkstreamAlreadyExistsError, WorkstreamNameInvalidError, type WorkstreamOptions, type WorkstreamSnapshot, type WorkstreamSnapshotSlowFields, type WorkstreamSummary, addBlockEdge, addNote, addTask, addToArchive, adoptAgent, agentStatusHistogram, appendLog, assertValidPaneId, backendByName, 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, exportSourceForWorkstream, exportSourcesForArchive, exportWorkstream, extractTail, foregroundPgid, freeAgent, freeWorkspace, gcMaxAgeDays, gcMaxCount, gcSnapshots, getAgent, getAgentByPane, getArchive, getParallelTracks, getPrerequisites, getTask, getTaskEdges, getTaskEdgesWithStatus, getWaitPollCount, getWorkspaceForAgent, getWorkspaceStaleness, gitBackend, idFromTitle, idFromTitleVerbose, importBucket, 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, resetCommandResolverForTests, resetKickProcessExecutor, resetSleep, resetTmuxExecutor, resetWaitPollCount, resolveActorIdentity, resolveCliCommand, resolveCliCommandWithSource, 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 };
|