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