@mu-crew/mu 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.js +4070 -3223
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +102 -36
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +438 -92
- package/dist/index.js +904 -329
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +5 -3
- package/docs/VISION.md +14 -7
- package/docs/VOCABULARY.md +5 -6
- package/docs/architecture/control-socket.md +1 -1
- package/docs/architecture/mux.md +4 -2
- package/docs/architecture/ops-log.md +2 -1
- package/docs/architecture/sdk.md +3 -1
- package/docs/architecture/sync.md +14 -6
- package/docs/architecture/tui.md +18 -8
- package/docs/guide/backends.md +5 -3
- package/docs/guide/cleanup.md +8 -3
- package/docs/guide/delegate.md +3 -1
- package/docs/guide/dispatch.md +5 -3
- package/docs/guide/recovery.md +19 -8
- package/docs/guide/sql.md +10 -3
- package/docs/guide/stop-a-worker.md +3 -1
- package/docs/guide/tui.md +3 -3
- package/docs/guide/upgrade.md +8 -2
- package/docs/reference/env.md +9 -4
- package/docs/reference/naming.md +3 -4
- package/package.json +1 -1
- package/skills/mu/SKILL.md +10 -3
- package/skills/mu/recipes/drift-audit.md +5 -3
package/dist/index.js
CHANGED
|
@@ -151,7 +151,7 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
151
151
|
import { randomUUID } from "crypto";
|
|
152
152
|
import { mkdirSync } from "fs";
|
|
153
153
|
import { homedir, hostname } from "os";
|
|
154
|
-
import { dirname, join, resolve } from "path";
|
|
154
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
155
155
|
import Database from "better-sqlite3";
|
|
156
156
|
|
|
157
157
|
// src/capture.ts
|
|
@@ -257,6 +257,23 @@ var CAPTURED_COLUMNS = {
|
|
|
257
257
|
task_notes: ["author", "content", "created_at"],
|
|
258
258
|
task_edges: ["created_at"]
|
|
259
259
|
};
|
|
260
|
+
var KEY_COLUMNS = {
|
|
261
|
+
workstreams: ["name"],
|
|
262
|
+
tasks: ["workstream_id", "local_id"],
|
|
263
|
+
task_notes: ["id", "task_id"],
|
|
264
|
+
task_edges: ["from_task_id", "to_task_id"]
|
|
265
|
+
};
|
|
266
|
+
function keyGuard(table) {
|
|
267
|
+
const cols = KEY_COLUMNS[table];
|
|
268
|
+
const msg = `mu: ${table}.${cols.join("/")} is part of the natural key that every op is filed under; changing it would desync the ops log, sync and undo. Add a new row (mu task add / mu workstream init) instead of moving or renaming one.`;
|
|
269
|
+
return `
|
|
270
|
+
CREATE TEMP TRIGGER IF NOT EXISTS _cap_${table}_key
|
|
271
|
+
BEFORE UPDATE OF ${cols.join(", ")} ON ${table}
|
|
272
|
+
WHEN ${anyChanged(cols)}
|
|
273
|
+
BEGIN
|
|
274
|
+
SELECT RAISE(ABORT, '${msg.replaceAll("'", "''")}');
|
|
275
|
+
END;`;
|
|
276
|
+
}
|
|
260
277
|
var DYING_STASH = (kind, keyExpr) => `INSERT OR REPLACE INTO _op_dying (kind, id, key) VALUES ('${kind}', OLD.id, ${keyExpr});`;
|
|
261
278
|
function buildTriggerDdl() {
|
|
262
279
|
const wsCols = CAPTURED_COLUMNS.workstreams;
|
|
@@ -268,6 +285,12 @@ function buildTriggerDdl() {
|
|
|
268
285
|
const edgeKeyNew = `${taskKey("NEW.from_task_id")} || '->' || ${taskKey("NEW.to_task_id")}`;
|
|
269
286
|
const edgeKeyOld = `${taskKey("OLD.from_task_id")} || '->' || ${taskKey("OLD.to_task_id")}`;
|
|
270
287
|
return `
|
|
288
|
+
-- \u2500\u2500\u2500 natural-key guards (see KEY_COLUMNS) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
289
|
+
${keyGuard("workstreams")}
|
|
290
|
+
${keyGuard("tasks")}
|
|
291
|
+
${keyGuard("task_notes")}
|
|
292
|
+
${keyGuard("task_edges")}
|
|
293
|
+
|
|
271
294
|
-- \u2500\u2500\u2500 workstreams \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
272
295
|
CREATE TEMP TRIGGER IF NOT EXISTS _cap_workstreams_ins
|
|
273
296
|
AFTER INSERT ON workstreams WHEN ${NOT_APPLYING}
|
|
@@ -403,8 +426,11 @@ function formatPair(pair) {
|
|
|
403
426
|
// src/db.ts
|
|
404
427
|
function defaultStateDir() {
|
|
405
428
|
if (process.env.MU_STATE_DIR) return process.env.MU_STATE_DIR;
|
|
406
|
-
|
|
407
|
-
|
|
429
|
+
return join(xdgStateHome(homedir()), "mu");
|
|
430
|
+
}
|
|
431
|
+
function xdgStateHome(home) {
|
|
432
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
433
|
+
return xdg !== void 0 && isAbsolute(xdg) ? xdg : join(home, ".local", "state");
|
|
408
434
|
}
|
|
409
435
|
function defaultDbPath() {
|
|
410
436
|
if (process.env.MU_DB_PATH) return process.env.MU_DB_PATH;
|
|
@@ -416,8 +442,6 @@ function openDb(options = {}) {
|
|
|
416
442
|
mkdirSync(dirname(path), { recursive: true });
|
|
417
443
|
const db = new Database(path, { readonly: options.readonly ?? false });
|
|
418
444
|
if (!options.readonly) {
|
|
419
|
-
db.pragma("journal_mode = WAL");
|
|
420
|
-
db.pragma("foreign_keys = ON");
|
|
421
445
|
db.pragma("busy_timeout = 5000");
|
|
422
446
|
const detectedVersion = detectExistingSchemaVersion(db);
|
|
423
447
|
if (detectedVersion !== null && detectedVersion > CURRENT_SCHEMA_VERSION) {
|
|
@@ -434,6 +458,8 @@ function openDb(options = {}) {
|
|
|
434
458
|
}
|
|
435
459
|
throw new SchemaTooOldError(detectedVersion, MIN_ACCEPTED_SCHEMA_VERSION);
|
|
436
460
|
}
|
|
461
|
+
db.pragma("journal_mode = WAL");
|
|
462
|
+
db.pragma("foreign_keys = ON");
|
|
437
463
|
applySchema(db);
|
|
438
464
|
seedMachineIdentity(db);
|
|
439
465
|
installCapture(db);
|
|
@@ -446,8 +472,7 @@ function refuseUserDbDuringTests(path) {
|
|
|
446
472
|
const inTest = process.env.VITEST !== void 0 || process.env.NODE_ENV === "test";
|
|
447
473
|
if (!inTest) return;
|
|
448
474
|
const home = process.env.HOME ?? homedir();
|
|
449
|
-
const
|
|
450
|
-
const realDb = resolve(join(xdg, "mu", "mu.db"));
|
|
475
|
+
const realDb = resolve(join(xdgStateHome(home), "mu", "mu.db"));
|
|
451
476
|
if (resolve(path) === realDb) {
|
|
452
477
|
throw new Error(
|
|
453
478
|
`openDb refused: tests must NEVER write to the user DB (${realDb}). Set MU_DB_PATH to a per-test temp path (test/_runCli.ts does this automatically) or pass an explicit { path } argument. The leak source is the call site of openDb in this stack frame.`
|
|
@@ -502,7 +527,7 @@ var SchemaTooOldError = class extends Error {
|
|
|
502
527
|
command: `mv "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}" "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}.old"`
|
|
503
528
|
},
|
|
504
529
|
{
|
|
505
|
-
intent: `Migrate the old DB into a fresh v${this.requiredVersion} DB`,
|
|
530
|
+
intent: `Migrate the old DB into a fresh v${this.requiredVersion} DB (run in a mu git checkout: the npm package does not ship scripts/)`,
|
|
506
531
|
command: `npx tsx scripts/migrate.ts "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}.old" --out /tmp/mu-v${this.requiredVersion}.db`
|
|
507
532
|
},
|
|
508
533
|
{
|
|
@@ -810,6 +835,7 @@ CREATE INDEX IF NOT EXISTS idx_ops_group ON ops (group_id);
|
|
|
810
835
|
-- into that peer's segment we have applied. One integer suffices
|
|
811
836
|
-- because segments are append-only and ordered. Rows are created on
|
|
812
837
|
-- demand at first ingest; there is no membership list to configure.
|
|
838
|
+
-- A Syncthing conflict copy gets its own row, keyed by its file stem.
|
|
813
839
|
CREATE TABLE IF NOT EXISTS sync_peers (
|
|
814
840
|
machine_id TEXT PRIMARY KEY,
|
|
815
841
|
last_applied_seq INTEGER NOT NULL DEFAULT 0,
|
|
@@ -976,6 +1002,9 @@ var LEGACY_LOG_ONLY_INTENTS = /* @__PURE__ */ new Set(["workstream.export"]);
|
|
|
976
1002
|
function isLegacyLogOnlyIntent(intent) {
|
|
977
1003
|
return intent !== null && LEGACY_LOG_ONLY_INTENTS.has(intent);
|
|
978
1004
|
}
|
|
1005
|
+
var LEGACY_LOG_ONLY_SQL_EXCLUSION = `(intent IS NULL OR intent NOT IN (${[
|
|
1006
|
+
...LEGACY_LOG_ONLY_INTENTS
|
|
1007
|
+
].map((i) => `'${i}'`).join(", ")}))`;
|
|
979
1008
|
var LEGACY_INTENT_SYNONYMS = /* @__PURE__ */ new Map([
|
|
980
1009
|
["workstream.teardown", ["workstream.destroy"]]
|
|
981
1010
|
]);
|
|
@@ -983,9 +1012,6 @@ function intentSpellings(intent) {
|
|
|
983
1012
|
const legacy = LEGACY_INTENT_SYNONYMS.get(intent);
|
|
984
1013
|
return legacy === void 0 ? [intent] : [intent, ...legacy];
|
|
985
1014
|
}
|
|
986
|
-
var LEGACY_LOG_ONLY_SQL_EXCLUSION = `(intent IS NULL OR intent NOT IN (${[
|
|
987
|
-
...LEGACY_LOG_ONLY_INTENTS
|
|
988
|
-
].map((i) => `'${i}'`).join(", ")}))`;
|
|
989
1015
|
|
|
990
1016
|
// src/logs.ts
|
|
991
1017
|
var SELECT_LOG_COLS = `
|
|
@@ -1068,8 +1094,30 @@ function rowFromDb(row) {
|
|
|
1068
1094
|
createdAt: row.created_at
|
|
1069
1095
|
};
|
|
1070
1096
|
}
|
|
1097
|
+
var RESERVED_LOG_KINDS = new Set(
|
|
1098
|
+
SYNCED_ENTITIES.filter((e) => e !== "message")
|
|
1099
|
+
);
|
|
1100
|
+
var LogKindReservedError = class extends Error {
|
|
1101
|
+
constructor(kind) {
|
|
1102
|
+
super(
|
|
1103
|
+
`--kind ${JSON.stringify(kind)} is reserved: ${[...RESERVED_LOG_KINDS].join(", ")} name projectable op entities, and a log line under one would be synced and replayed as a real ${kind} change`
|
|
1104
|
+
);
|
|
1105
|
+
this.kind = kind;
|
|
1106
|
+
}
|
|
1107
|
+
kind;
|
|
1108
|
+
name = "LogKindReservedError";
|
|
1109
|
+
errorNextSteps() {
|
|
1110
|
+
return [
|
|
1111
|
+
{
|
|
1112
|
+
intent: "Use a channel tag that names your ledger",
|
|
1113
|
+
command: `mu log --kind ${this.kind}-log '<text>'`
|
|
1114
|
+
}
|
|
1115
|
+
];
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1071
1118
|
function appendLog(db, opts) {
|
|
1072
1119
|
const kind = opts.kind ?? "message";
|
|
1120
|
+
if (RESERVED_LOG_KINDS.has(kind)) throw new LogKindReservedError(kind);
|
|
1073
1121
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1074
1122
|
const key = opts.workstream ?? MACHINE_WIDE_KEY;
|
|
1075
1123
|
const group = randomUUID2();
|
|
@@ -1113,7 +1161,10 @@ function listLogs(db, opts = {}) {
|
|
|
1113
1161
|
conditions.push("l.seq > ?");
|
|
1114
1162
|
params.push(opts.since);
|
|
1115
1163
|
}
|
|
1116
|
-
if (opts.source
|
|
1164
|
+
if (opts.source === "system") {
|
|
1165
|
+
conditions.push("(l.actor = ? OR l.actor IS NULL)");
|
|
1166
|
+
params.push(opts.source);
|
|
1167
|
+
} else if (opts.source !== void 0) {
|
|
1117
1168
|
conditions.push("l.actor = ?");
|
|
1118
1169
|
params.push(opts.source);
|
|
1119
1170
|
}
|
|
@@ -1355,19 +1406,26 @@ function resetSleep() {
|
|
|
1355
1406
|
function sleep(ms) {
|
|
1356
1407
|
return currentSleep(ms);
|
|
1357
1408
|
}
|
|
1409
|
+
function exactSessionTarget(name) {
|
|
1410
|
+
return `=${name}:`;
|
|
1411
|
+
}
|
|
1412
|
+
var NO_SERVER_RE = /no server running|error connecting to .*\(No such file or directory\)/i;
|
|
1413
|
+
function isPaneGoneError(err) {
|
|
1414
|
+
return err instanceof TmuxError && (/can't find pane|pane not found|no current target/i.test(err.stderr) || NO_SERVER_RE.test(err.stderr));
|
|
1415
|
+
}
|
|
1358
1416
|
async function listSessions() {
|
|
1359
1417
|
try {
|
|
1360
1418
|
const out = await tmux(["list-sessions", "-F", "#{session_name}"]);
|
|
1361
1419
|
return out.split("\n").filter((line) => line.length > 0).map((name) => ({ name }));
|
|
1362
1420
|
} catch (err) {
|
|
1363
|
-
if (err instanceof TmuxError &&
|
|
1421
|
+
if (err instanceof TmuxError && (NO_SERVER_RE.test(err.stderr) || /no sessions/i.test(err.stderr))) {
|
|
1364
1422
|
return [];
|
|
1365
1423
|
}
|
|
1366
1424
|
throw err;
|
|
1367
1425
|
}
|
|
1368
1426
|
}
|
|
1369
1427
|
async function sessionExists(name) {
|
|
1370
|
-
const result = await currentExecutor(["has-session", "-t", name]);
|
|
1428
|
+
const result = await currentExecutor(["has-session", "-t", exactSessionTarget(name)]);
|
|
1371
1429
|
return result.exitCode === 0;
|
|
1372
1430
|
}
|
|
1373
1431
|
async function newSession(name, opts = {}) {
|
|
@@ -1392,19 +1450,21 @@ async function newSessionWithPane(name, opts) {
|
|
|
1392
1450
|
return out;
|
|
1393
1451
|
}
|
|
1394
1452
|
async function killSession(name) {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
result.stderr,
|
|
1400
|
-
result.stdout,
|
|
1401
|
-
result.exitCode
|
|
1402
|
-
);
|
|
1453
|
+
const args = ["kill-session", "-t", exactSessionTarget(name)];
|
|
1454
|
+
const result = await currentExecutor(args);
|
|
1455
|
+
if (result.exitCode !== 0 && !/can't find session|session not found|no current target/i.test(result.stderr) && !NO_SERVER_RE.test(result.stderr)) {
|
|
1456
|
+
throw new TmuxError(args, result.stderr, result.stdout, result.exitCode);
|
|
1403
1457
|
}
|
|
1404
1458
|
}
|
|
1405
1459
|
async function listWindows(session) {
|
|
1406
1460
|
if (session) {
|
|
1407
|
-
const out2 = await tmux([
|
|
1461
|
+
const out2 = await tmux([
|
|
1462
|
+
"list-windows",
|
|
1463
|
+
"-t",
|
|
1464
|
+
exactSessionTarget(session),
|
|
1465
|
+
"-F",
|
|
1466
|
+
"#{window_id} #{window_name}"
|
|
1467
|
+
]);
|
|
1408
1468
|
return parseWindows(out2);
|
|
1409
1469
|
}
|
|
1410
1470
|
const out = await tmux([
|
|
@@ -1435,7 +1495,7 @@ function parseWindows(output) {
|
|
|
1435
1495
|
async function newWindow(opts) {
|
|
1436
1496
|
const args = ["new-window"];
|
|
1437
1497
|
if (opts.detached !== false) args.push("-d");
|
|
1438
|
-
if (opts.session) args.push("-t", opts.session);
|
|
1498
|
+
if (opts.session) args.push("-t", exactSessionTarget(opts.session));
|
|
1439
1499
|
args.push("-n", opts.name);
|
|
1440
1500
|
if (opts.cwd) args.push("-c", opts.cwd);
|
|
1441
1501
|
appendEnvFlags(args, opts.env);
|
|
@@ -1449,15 +1509,13 @@ async function listPanesInSession(session) {
|
|
|
1449
1509
|
"list-panes",
|
|
1450
1510
|
"-s",
|
|
1451
1511
|
"-t",
|
|
1452
|
-
session,
|
|
1512
|
+
exactSessionTarget(session),
|
|
1453
1513
|
"-F",
|
|
1454
1514
|
"#{window_id} #{pane_id} #{pane_title} #{pane_current_command}"
|
|
1455
1515
|
];
|
|
1456
1516
|
const result = await currentExecutor(args);
|
|
1457
1517
|
if (result.exitCode !== 0) {
|
|
1458
|
-
if (/can't find (session|window)|no
|
|
1459
|
-
result.stderr
|
|
1460
|
-
)) {
|
|
1518
|
+
if (/can't find (session|window)|no sessions|no current target/i.test(result.stderr) || NO_SERVER_RE.test(result.stderr)) {
|
|
1461
1519
|
return [];
|
|
1462
1520
|
}
|
|
1463
1521
|
throw new TmuxError(args, result.stderr, result.stdout, result.exitCode);
|
|
@@ -1528,7 +1586,7 @@ function appendEnvFlags(args, env) {
|
|
|
1528
1586
|
async function killPane(paneId) {
|
|
1529
1587
|
assertValidPaneId(paneId);
|
|
1530
1588
|
const result = await currentExecutor(["kill-pane", "-t", paneId]);
|
|
1531
|
-
if (result.exitCode !== 0 && !/can't find pane/i.test(result.stderr)) {
|
|
1589
|
+
if (result.exitCode !== 0 && !/can't find pane/i.test(result.stderr) && !NO_SERVER_RE.test(result.stderr)) {
|
|
1532
1590
|
throw new TmuxError(["kill-pane", "-t", paneId], result.stderr, result.stdout, result.exitCode);
|
|
1533
1591
|
}
|
|
1534
1592
|
}
|
|
@@ -1582,7 +1640,7 @@ async function paneTTY(paneId) {
|
|
|
1582
1640
|
assertValidPaneId(paneId);
|
|
1583
1641
|
const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_tty}"]);
|
|
1584
1642
|
if (result.exitCode !== 0) {
|
|
1585
|
-
if (/can't find pane|pane not found/i.test(result.stderr)) {
|
|
1643
|
+
if (/can't find pane|pane not found/i.test(result.stderr) || NO_SERVER_RE.test(result.stderr)) {
|
|
1586
1644
|
throw new PaneNotFoundError(paneId, tmuxBackend);
|
|
1587
1645
|
}
|
|
1588
1646
|
throw new TmuxError(
|
|
@@ -1645,7 +1703,10 @@ async function awaitPaneQuiescence(paneId, budgetMs) {
|
|
|
1645
1703
|
const deadline = Date.now() + budgetMs;
|
|
1646
1704
|
let calm = 0;
|
|
1647
1705
|
for (; ; ) {
|
|
1648
|
-
const scrollback = await capturePane(paneId, { lines: 50 }).catch(() =>
|
|
1706
|
+
const scrollback = await capturePane(paneId, { lines: 50 }).catch((err) => {
|
|
1707
|
+
if (isPaneGoneError(err)) throw err;
|
|
1708
|
+
return void 0;
|
|
1709
|
+
});
|
|
1649
1710
|
const busy = scrollback === void 0 || paneLooksBusy(scrollback);
|
|
1650
1711
|
if (busy && scrollback !== void 0 && hasWorkMarker(scrollback)) return true;
|
|
1651
1712
|
if (!busy) {
|
|
@@ -1730,9 +1791,19 @@ async function capturePane(paneId, opts = {}) {
|
|
|
1730
1791
|
args.push("-S", "-", "-E", "-");
|
|
1731
1792
|
} else if (opts.lines > 0) {
|
|
1732
1793
|
args.push("-S", `-${opts.lines}`);
|
|
1794
|
+
return lastLines(await tmux(args), opts.lines);
|
|
1733
1795
|
}
|
|
1734
1796
|
return tmux(args);
|
|
1735
1797
|
}
|
|
1798
|
+
function lastLines(text, n) {
|
|
1799
|
+
const lines = text.split("\n");
|
|
1800
|
+
let end = lines.length;
|
|
1801
|
+
while (end > 0 && (lines[end - 1] ?? "").trim() === "") end--;
|
|
1802
|
+
if (end === 0) return "";
|
|
1803
|
+
const kept = lines.slice(Math.max(0, end - n), end).join("\n");
|
|
1804
|
+
return text.endsWith("\n") ? `${kept}
|
|
1805
|
+
` : kept;
|
|
1806
|
+
}
|
|
1736
1807
|
async function tmuxAvailable() {
|
|
1737
1808
|
try {
|
|
1738
1809
|
await tmux(["-V"]);
|
|
@@ -1827,7 +1898,7 @@ var tmuxBackend = Object.freeze({
|
|
|
1827
1898
|
// src/mux/herdr.ts
|
|
1828
1899
|
var HerdrError = class extends MuxError {
|
|
1829
1900
|
constructor(args, stderr, stdout, exitCode, code) {
|
|
1830
|
-
const detail = parseErrorEnvelope(stderr)?.message ?? stderr.trim()
|
|
1901
|
+
const detail = parseErrorEnvelope(stderr)?.message ?? (stderr.trim() || stdout.trim());
|
|
1831
1902
|
super(`herdr ${args.join(" ")} failed (exit ${exitCode}): ${detail || "no output"}`);
|
|
1832
1903
|
this.args = args;
|
|
1833
1904
|
this.stderr = stderr;
|
|
@@ -1958,8 +2029,10 @@ var HerdrWorkspaceGroupCloseError = class extends Error {
|
|
|
1958
2029
|
};
|
|
1959
2030
|
var HerdrNotImplementedError = class extends Error {
|
|
1960
2031
|
name = "HerdrNotImplementedError";
|
|
1961
|
-
constructor(method
|
|
1962
|
-
super(
|
|
2032
|
+
constructor(method) {
|
|
2033
|
+
super(
|
|
2034
|
+
`herdr backend: ${method} is not supported: herdr creation verbs always start a plain shell; create the pane without a command, then start the agent with startAgentInPane`
|
|
2035
|
+
);
|
|
1963
2036
|
}
|
|
1964
2037
|
};
|
|
1965
2038
|
var HERDR_ORDINAL = "[0-9A-Z]+";
|
|
@@ -2078,10 +2151,12 @@ function readPane(v) {
|
|
|
2078
2151
|
// herdr's optional `label` is what `pane rename` writes and is
|
|
2079
2152
|
// therefore mu's pane-title equivalent.
|
|
2080
2153
|
title: asString(v.label) ?? "",
|
|
2081
|
-
// herdr
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
command:
|
|
2154
|
+
// herdr reports no foreground command on `pane list` (that costs a
|
|
2155
|
+
// `pane process-info` round trip per pane), but it does report the
|
|
2156
|
+
// agent kind it detected (`pi`, `claude`, `codex`, ...). That is
|
|
2157
|
+
// what `command` is used for: reconcile's orphan recogniser matches
|
|
2158
|
+
// it against the agent CLIs. Empty for a plain shell.
|
|
2159
|
+
command: asString(v.agent) ?? "",
|
|
2085
2160
|
windowId: asString(v.tab_id)
|
|
2086
2161
|
};
|
|
2087
2162
|
}
|
|
@@ -2128,7 +2203,7 @@ async function newSession2(name, opts = {}) {
|
|
|
2128
2203
|
const args = ["workspace", "create", "--label", name, "--no-focus"];
|
|
2129
2204
|
if (opts.cwd) args.push("--cwd", opts.cwd);
|
|
2130
2205
|
appendEnvFlags2(args, opts.env);
|
|
2131
|
-
await herdr(args);
|
|
2206
|
+
await labelRootTab(await herdr(args), opts.windowName);
|
|
2132
2207
|
}
|
|
2133
2208
|
async function newSessionWithPane2(name, opts) {
|
|
2134
2209
|
rejectCommand("newSessionWithPane", opts.command);
|
|
@@ -2136,7 +2211,39 @@ async function newSessionWithPane2(name, opts) {
|
|
|
2136
2211
|
if (opts.cwd) args.push("--cwd", opts.cwd);
|
|
2137
2212
|
appendEnvFlags2(args, opts.env);
|
|
2138
2213
|
const result = await herdr(args);
|
|
2139
|
-
|
|
2214
|
+
const paneId = readCreatedPaneId(result, "root_pane", args);
|
|
2215
|
+
await labelRootTab(result, opts.windowName);
|
|
2216
|
+
return paneId;
|
|
2217
|
+
}
|
|
2218
|
+
async function labelRootTab(created, windowName) {
|
|
2219
|
+
if (windowName === void 0 || windowName.length === 0) return;
|
|
2220
|
+
const root = created.root_pane;
|
|
2221
|
+
const tab = created.tab;
|
|
2222
|
+
const tabId = (isRecord(root) ? asString(root.tab_id) : void 0) ?? (isRecord(tab) ? asString(tab.tab_id) : void 0);
|
|
2223
|
+
try {
|
|
2224
|
+
if (tabId === void 0) {
|
|
2225
|
+
throw new HerdrError(
|
|
2226
|
+
["workspace", "create"],
|
|
2227
|
+
`herdr response had no root tab id: ${JSON.stringify(created)}`,
|
|
2228
|
+
"",
|
|
2229
|
+
0
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
await herdr(["tab", "rename", tabId, windowName]);
|
|
2233
|
+
} catch (err) {
|
|
2234
|
+
await closeCreatedWorkspace(created);
|
|
2235
|
+
throw err;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
async function closeCreatedWorkspace(created) {
|
|
2239
|
+
const ws = created.workspace;
|
|
2240
|
+
const root = created.root_pane;
|
|
2241
|
+
const id = (isRecord(ws) ? asString(ws.workspace_id) : void 0) ?? (isRecord(root) ? asString(root.workspace_id) : void 0);
|
|
2242
|
+
if (id === void 0) return;
|
|
2243
|
+
try {
|
|
2244
|
+
await herdrTolerating(["workspace", "close", id], ["workspace_not_found"]);
|
|
2245
|
+
} catch {
|
|
2246
|
+
}
|
|
2140
2247
|
}
|
|
2141
2248
|
async function killSession2(name) {
|
|
2142
2249
|
const id = await resolveWorkspaceId(name);
|
|
@@ -2265,10 +2372,10 @@ async function paneTTY2(paneId) {
|
|
|
2265
2372
|
["pane", "process-info", "--pane", paneId],
|
|
2266
2373
|
["pane_not_found", "workspace_not_found"]
|
|
2267
2374
|
);
|
|
2268
|
-
if (result === void 0) throw new PaneNotFoundError(paneId);
|
|
2375
|
+
if (result === void 0) throw new PaneNotFoundError(paneId, herdrBackend);
|
|
2269
2376
|
const info = result.process_info;
|
|
2270
2377
|
const pid = isRecord(info) ? info.shell_pid : void 0;
|
|
2271
|
-
if (typeof pid !== "number") throw new PaneNotFoundError(paneId);
|
|
2378
|
+
if (typeof pid !== "number") throw new PaneNotFoundError(paneId, herdrBackend);
|
|
2272
2379
|
const { readlink } = await import("fs/promises");
|
|
2273
2380
|
try {
|
|
2274
2381
|
return await readlink(`/proc/${pid}/fd/0`);
|
|
@@ -2426,7 +2533,7 @@ function appendEnvFlags2(args, env) {
|
|
|
2426
2533
|
}
|
|
2427
2534
|
function rejectCommand(method, command) {
|
|
2428
2535
|
if (command === void 0 || command.trim().length === 0) return;
|
|
2429
|
-
throw new HerdrNotImplementedError(`${method} with a command
|
|
2536
|
+
throw new HerdrNotImplementedError(`${method} with a command`);
|
|
2430
2537
|
}
|
|
2431
2538
|
function readCreatedPaneId(result, key, args) {
|
|
2432
2539
|
const pane = readPane(result[key]);
|
|
@@ -2451,14 +2558,32 @@ async function herdrAvailable() {
|
|
|
2451
2558
|
if (result === void 0 || result.exitCode !== 0) return false;
|
|
2452
2559
|
return isHerdrStatusUsable(result.stdout);
|
|
2453
2560
|
}
|
|
2454
|
-
function
|
|
2561
|
+
async function attachFocusArgs(workspaceId, target) {
|
|
2455
2562
|
if (target.window !== void 0) {
|
|
2456
|
-
|
|
2563
|
+
const tabs = readTabs(await herdr(["tab", "list", "--workspace", workspaceId]));
|
|
2564
|
+
const tab = tabs.find((t) => t.name === target.window);
|
|
2565
|
+
if (tab !== void 0) return ["tab", "focus", tab.id];
|
|
2566
|
+
}
|
|
2567
|
+
return ["workspace", "focus", workspaceId];
|
|
2568
|
+
}
|
|
2569
|
+
function insideHerdrClient() {
|
|
2570
|
+
return process.env.HERDR_ENV === "1";
|
|
2571
|
+
}
|
|
2572
|
+
async function attachHint2(target) {
|
|
2573
|
+
const base = ["herdr", ...herdrGlobalFlags()].join(" ");
|
|
2574
|
+
const focus = await resolveWorkspaceId(target.session).then((id) => id === void 0 ? void 0 : attachFocusArgs(id, target)).catch(() => void 0);
|
|
2575
|
+
if (focus === void 0) {
|
|
2576
|
+
return `${base} workspace list # then: ${base} workspace focus <id of ${target.session}>`;
|
|
2457
2577
|
}
|
|
2458
|
-
|
|
2578
|
+
const line = `${base} ${focus.join(" ")}`;
|
|
2579
|
+
return insideHerdrClient() ? line : `${line} && ${base}`;
|
|
2459
2580
|
}
|
|
2460
|
-
function attachCommands2(target) {
|
|
2461
|
-
|
|
2581
|
+
async function attachCommands2(target) {
|
|
2582
|
+
const flags = herdrGlobalFlags();
|
|
2583
|
+
const focus = await attachFocusArgs(await requireWorkspaceId(target.session), target);
|
|
2584
|
+
const steps = [{ command: "herdr", args: [...flags, ...focus] }];
|
|
2585
|
+
if (!insideHerdrClient()) steps.push({ command: "herdr", args: [...flags] });
|
|
2586
|
+
return steps;
|
|
2462
2587
|
}
|
|
2463
2588
|
function paneNotFoundNextSteps2(paneId) {
|
|
2464
2589
|
return [
|
|
@@ -2815,16 +2940,11 @@ var WorkspacePathNotEmptyError = class extends Error {
|
|
|
2815
2940
|
command: `mu workspace orphans -w ${this.workstream}`
|
|
2816
2941
|
},
|
|
2817
2942
|
{
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
command: `rm -rf ${this.workspacePath}`
|
|
2824
|
-
},
|
|
2825
|
-
{
|
|
2826
|
-
intent: "For git workspaces specifically: also prune the worktree registration",
|
|
2827
|
-
command: "cd <project-root> && git worktree prune"
|
|
2943
|
+
// No row points at the dir (createWorkspace throws
|
|
2944
|
+
// WorkspaceExistsError first when one does), so `mu workspace
|
|
2945
|
+
// free` has nothing to free. Same recipe as `mu workspace orphans`.
|
|
2946
|
+
intent: "Inspect it, then remove it by hand (git: also drops the worktree registration)",
|
|
2947
|
+
command: `(cd <project-root> && git worktree remove --force ${this.workspacePath}) || rm -rf ${this.workspacePath}`
|
|
2828
2948
|
}
|
|
2829
2949
|
];
|
|
2830
2950
|
}
|
|
@@ -2937,22 +3057,44 @@ var WorkspaceDirtyError = class extends Error {
|
|
|
2937
3057
|
}
|
|
2938
3058
|
};
|
|
2939
3059
|
var WorkspaceConflictError = class extends Error {
|
|
2940
|
-
constructor(workspacePath2, fromRef, conflicts) {
|
|
2941
|
-
super(
|
|
3060
|
+
constructor(workspacePath2, fromRef, conflicts, aborted = true) {
|
|
3061
|
+
super(
|
|
3062
|
+
`rebase onto ${fromRef} produced ${conflicts.length} conflict(s)${aborted ? "; rebase aborted, workspace unchanged" : "; conflicts left in place"}: ${workspacePath2}`
|
|
3063
|
+
);
|
|
2942
3064
|
this.workspacePath = workspacePath2;
|
|
2943
3065
|
this.fromRef = fromRef;
|
|
2944
3066
|
this.conflicts = conflicts;
|
|
3067
|
+
this.aborted = aborted;
|
|
2945
3068
|
}
|
|
2946
3069
|
workspacePath;
|
|
2947
3070
|
fromRef;
|
|
2948
3071
|
conflicts;
|
|
3072
|
+
aborted;
|
|
2949
3073
|
name = "WorkspaceConflictError";
|
|
2950
3074
|
errorNextSteps() {
|
|
3075
|
+
const resync = {
|
|
3076
|
+
intent: "After resolving, record the new base (a clean refresh updates the fork point)",
|
|
3077
|
+
command: `mu workspace refresh <agent> --from '${this.fromRef}'`
|
|
3078
|
+
};
|
|
3079
|
+
if (this.aborted) {
|
|
3080
|
+
return [
|
|
3081
|
+
{
|
|
3082
|
+
intent: "The rebase was aborted. Rebase by hand in the workspace and resolve",
|
|
3083
|
+
command: `cd ${this.workspacePath} # then: git rebase ${this.fromRef} (sl: sl rebase -d '${this.fromRef}')`
|
|
3084
|
+
},
|
|
3085
|
+
resync,
|
|
3086
|
+
{
|
|
3087
|
+
intent: "Or DISCARD the workspace entirely (the lossy escape)",
|
|
3088
|
+
command: "mu workspace free <agent>"
|
|
3089
|
+
}
|
|
3090
|
+
];
|
|
3091
|
+
}
|
|
2951
3092
|
return [
|
|
2952
3093
|
{
|
|
2953
|
-
intent: "
|
|
2954
|
-
command: `cd ${this.workspacePath} # then resolve
|
|
2955
|
-
}
|
|
3094
|
+
intent: "Resolve the conflicted commits in place",
|
|
3095
|
+
command: `cd ${this.workspacePath} # then: jj resolve; or undo the rebase: jj op undo`
|
|
3096
|
+
},
|
|
3097
|
+
resync
|
|
2956
3098
|
];
|
|
2957
3099
|
}
|
|
2958
3100
|
};
|
|
@@ -2992,26 +3134,38 @@ async function run(bin, args, cwd) {
|
|
|
2992
3134
|
}
|
|
2993
3135
|
}
|
|
2994
3136
|
async function runShow(bin, args, cwd) {
|
|
3137
|
+
let stdout;
|
|
3138
|
+
let overflowed = false;
|
|
2995
3139
|
try {
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
if (stdout.length > SHOW_COMMIT_MAX_CHARS) {
|
|
3140
|
+
({ stdout } = await exec(bin, [...args], { cwd, maxBuffer: SHOW_COMMIT_MAX_CHARS * 2 }));
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
const partial = stdoutOverflow(err);
|
|
3143
|
+
if (partial === void 0) {
|
|
3001
3144
|
return {
|
|
3002
|
-
text:
|
|
3003
|
-
|
|
3004
|
-
|
|
3145
|
+
text: "",
|
|
3146
|
+
truncated: false,
|
|
3147
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3005
3148
|
};
|
|
3006
3149
|
}
|
|
3007
|
-
|
|
3008
|
-
|
|
3150
|
+
stdout = partial;
|
|
3151
|
+
overflowed = true;
|
|
3152
|
+
}
|
|
3153
|
+
if (overflowed || stdout.length > SHOW_COMMIT_MAX_CHARS) {
|
|
3009
3154
|
return {
|
|
3010
|
-
text:
|
|
3011
|
-
|
|
3012
|
-
|
|
3155
|
+
text: `${stdout.slice(0, SHOW_COMMIT_MAX_CHARS)}
|
|
3156
|
+
\u2026(truncated at ${SHOW_COMMIT_MAX_CHARS} chars)`,
|
|
3157
|
+
truncated: true
|
|
3013
3158
|
};
|
|
3014
3159
|
}
|
|
3160
|
+
return { text: stdout, truncated: false };
|
|
3161
|
+
}
|
|
3162
|
+
function stdoutOverflow(err) {
|
|
3163
|
+
if (!(err instanceof Error)) return void 0;
|
|
3164
|
+
const e = err;
|
|
3165
|
+
if (e.code !== "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || typeof e.stdout !== "string") {
|
|
3166
|
+
return void 0;
|
|
3167
|
+
}
|
|
3168
|
+
return e.message.startsWith("stdout ") ? e.stdout : void 0;
|
|
3015
3169
|
}
|
|
3016
3170
|
function relTimeFromIso(iso) {
|
|
3017
3171
|
const t = Date.parse(iso);
|
|
@@ -3104,15 +3258,15 @@ var gitBackend = {
|
|
|
3104
3258
|
},
|
|
3105
3259
|
// Rebase the worktree onto `fromRef` (default = origin/HEAD via
|
|
3106
3260
|
// resolveGitMainRef). Refuses on a dirty WC; returns the replayed
|
|
3107
|
-
// commit subjects oldest-first
|
|
3108
|
-
//
|
|
3109
|
-
// half-rebased worktree from us.
|
|
3261
|
+
// commit subjects oldest-first and the new fork point (parentRef);
|
|
3262
|
+
// on conflict aborts the rebase and throws WorkspaceConflictError so
|
|
3263
|
+
// the operator never inherits a half-rebased worktree from us.
|
|
3110
3264
|
//
|
|
3111
3265
|
// We DO `git fetch` first — otherwise the rebase target would only
|
|
3112
3266
|
// be as fresh as the local refs cache, and the operator running
|
|
3113
3267
|
// `mu workspace refresh` is explicitly asking for the latest. This
|
|
3114
3268
|
// is the one-and-only place mu fetches; commitsBehind() stays pure.
|
|
3115
|
-
async rebaseTo(workspacePath2, fromRef) {
|
|
3269
|
+
async rebaseTo(workspacePath2, fromRef, previousParentRef) {
|
|
3116
3270
|
if (!existsSync2(workspacePath2)) {
|
|
3117
3271
|
throw new Error(`vcs git: workspace path missing: ${workspacePath2}`);
|
|
3118
3272
|
}
|
|
@@ -3134,7 +3288,7 @@ var gitBackend = {
|
|
|
3134
3288
|
}
|
|
3135
3289
|
resolvedRef = main;
|
|
3136
3290
|
}
|
|
3137
|
-
const
|
|
3291
|
+
const headBefore = await run("git", ["rev-parse", "HEAD"], workspacePath2);
|
|
3138
3292
|
try {
|
|
3139
3293
|
await run(
|
|
3140
3294
|
"git",
|
|
@@ -3150,16 +3304,21 @@ var gitBackend = {
|
|
|
3150
3304
|
}
|
|
3151
3305
|
throw err;
|
|
3152
3306
|
}
|
|
3153
|
-
const
|
|
3154
|
-
|
|
3307
|
+
const target = await run(
|
|
3308
|
+
"git",
|
|
3309
|
+
["rev-parse", "--verify", `${resolvedRef}^{commit}`],
|
|
3310
|
+
workspacePath2
|
|
3155
3311
|
);
|
|
3156
|
-
const
|
|
3312
|
+
const keepPrevious = previousParentRef !== void 0 && await gitIsAncestor(workspacePath2, target, previousParentRef) && await gitIsAncestor(workspacePath2, previousParentRef, "HEAD");
|
|
3313
|
+
const parentRef = keepPrevious ? previousParentRef : target;
|
|
3314
|
+
const headAfter = await run("git", ["rev-parse", "HEAD"], workspacePath2);
|
|
3315
|
+
const logOut = headAfter === headBefore ? "" : await run(
|
|
3157
3316
|
"git",
|
|
3158
|
-
["log", "--reverse", "--format=%s", `${
|
|
3317
|
+
["log", "--reverse", "--format=%s", `${parentRef}..HEAD`],
|
|
3159
3318
|
workspacePath2
|
|
3160
3319
|
);
|
|
3161
3320
|
const replayed = logOut.length === 0 ? [] : logOut.split("\n");
|
|
3162
|
-
return { fromRef: resolvedRef, replayed, conflicts: [] };
|
|
3321
|
+
return { fromRef: resolvedRef, parentRef, replayed, conflicts: [] };
|
|
3163
3322
|
},
|
|
3164
3323
|
// List commits in (baseRef..HEAD), oldest-first. The format string
|
|
3165
3324
|
// packs four NUL-delimited fields per record, then a record
|
|
@@ -3210,6 +3369,7 @@ var gitBackend = {
|
|
|
3210
3369
|
return { removed: false };
|
|
3211
3370
|
}
|
|
3212
3371
|
let committedRef;
|
|
3372
|
+
let branch;
|
|
3213
3373
|
if (opts.commit) {
|
|
3214
3374
|
const dirty = (await listGitDirtyFiles(opts.workspacePath)).length > 0;
|
|
3215
3375
|
if (dirty) {
|
|
@@ -3229,6 +3389,7 @@ var gitBackend = {
|
|
|
3229
3389
|
);
|
|
3230
3390
|
committedRef = await run("git", ["rev-parse", "HEAD"], opts.workspacePath);
|
|
3231
3391
|
}
|
|
3392
|
+
branch = await keepGitHeadReachable(opts.workspacePath);
|
|
3232
3393
|
}
|
|
3233
3394
|
const projectRoot = await resolveGitProjectRoot(opts.workspacePath);
|
|
3234
3395
|
if (projectRoot) {
|
|
@@ -3238,6 +3399,7 @@ var gitBackend = {
|
|
|
3238
3399
|
}
|
|
3239
3400
|
const result = { removed: true };
|
|
3240
3401
|
if (committedRef !== void 0) result.committedRef = committedRef;
|
|
3402
|
+
if (branch !== void 0) result.branch = branch;
|
|
3241
3403
|
return result;
|
|
3242
3404
|
},
|
|
3243
3405
|
async listDirtyFiles(workspacePath2) {
|
|
@@ -3245,6 +3407,14 @@ var gitBackend = {
|
|
|
3245
3407
|
return listGitDirtyFiles(workspacePath2);
|
|
3246
3408
|
}
|
|
3247
3409
|
};
|
|
3410
|
+
async function gitIsAncestor(cwd, ancestor, descendant) {
|
|
3411
|
+
try {
|
|
3412
|
+
await run("git", ["merge-base", "--is-ancestor", ancestor, descendant], cwd);
|
|
3413
|
+
return true;
|
|
3414
|
+
} catch {
|
|
3415
|
+
return false;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3248
3418
|
async function resolveGitMainRef(workspacePath2) {
|
|
3249
3419
|
for (const candidate of [
|
|
3250
3420
|
"refs/remotes/origin/HEAD",
|
|
@@ -3272,6 +3442,21 @@ async function listGitUnmergedPaths(workspacePath2) {
|
|
|
3272
3442
|
return [];
|
|
3273
3443
|
}
|
|
3274
3444
|
}
|
|
3445
|
+
async function keepGitHeadReachable(workspacePath2) {
|
|
3446
|
+
const containing = await run(
|
|
3447
|
+
"git",
|
|
3448
|
+
["for-each-ref", "--count=1", "--contains", "HEAD", "refs/heads", "refs/remotes", "refs/tags"],
|
|
3449
|
+
workspacePath2
|
|
3450
|
+
);
|
|
3451
|
+
if (containing.length > 0) return void 0;
|
|
3452
|
+
const sha = await run("git", ["rev-parse", "HEAD"], workspacePath2);
|
|
3453
|
+
const parts = workspacePath2.replace(/\/+$/, "").split("/");
|
|
3454
|
+
const agent = parts.at(-1) ?? "workspace";
|
|
3455
|
+
const workstream = parts.at(-2) ?? "mu";
|
|
3456
|
+
const branch = `mu/${workstream}/${agent}-${sha.slice(0, 12)}`;
|
|
3457
|
+
await run("git", ["branch", branch, sha], workspacePath2);
|
|
3458
|
+
return branch;
|
|
3459
|
+
}
|
|
3275
3460
|
async function resolveGitProjectRoot(workspacePath2) {
|
|
3276
3461
|
try {
|
|
3277
3462
|
const { stdout } = await exec(
|
|
@@ -3317,6 +3502,7 @@ var jjBackend = {
|
|
|
3317
3502
|
}
|
|
3318
3503
|
await ensureParent(opts.workspacePath);
|
|
3319
3504
|
const name = jjWorkspaceName(opts.workspacePath);
|
|
3505
|
+
await forgetIfRootMissing(name, opts.projectRoot);
|
|
3320
3506
|
const args = ["workspace", "add", "--name", name];
|
|
3321
3507
|
if (opts.parentRef) args.push("--revision", opts.parentRef);
|
|
3322
3508
|
args.push(opts.workspacePath);
|
|
@@ -3328,7 +3514,6 @@ var jjBackend = {
|
|
|
3328
3514
|
if (!existsSync3(opts.workspacePath)) {
|
|
3329
3515
|
return { removed: false };
|
|
3330
3516
|
}
|
|
3331
|
-
const name = jjWorkspaceName(opts.workspacePath);
|
|
3332
3517
|
let committedRef;
|
|
3333
3518
|
if (opts.commit) {
|
|
3334
3519
|
const desc = await run(
|
|
@@ -3351,7 +3536,7 @@ var jjBackend = {
|
|
|
3351
3536
|
}
|
|
3352
3537
|
committedRef = await jjCommitId(opts.workspacePath);
|
|
3353
3538
|
}
|
|
3354
|
-
await run("jj", ["workspace", "forget"
|
|
3539
|
+
await run("jj", ["workspace", "forget"], opts.workspacePath);
|
|
3355
3540
|
rmDirSync(opts.workspacePath);
|
|
3356
3541
|
const result = { removed: true };
|
|
3357
3542
|
if (committedRef !== void 0) result.committedRef = committedRef;
|
|
@@ -3411,18 +3596,15 @@ var jjBackend = {
|
|
|
3411
3596
|
// doesn't auto-abort on conflicts (they materialise as commits with
|
|
3412
3597
|
// conflict markers), so the workspace is left in a state the
|
|
3413
3598
|
// operator can resolve in-place.
|
|
3414
|
-
async rebaseTo(workspacePath2, fromRef) {
|
|
3599
|
+
async rebaseTo(workspacePath2, fromRef, previousParentRef) {
|
|
3415
3600
|
if (!existsSync3(workspacePath2)) {
|
|
3416
3601
|
throw new Error(`vcs jj: workspace path missing: ${workspacePath2}`);
|
|
3417
3602
|
}
|
|
3418
3603
|
const target = fromRef ?? "trunk()";
|
|
3419
|
-
const
|
|
3420
|
-
"jj",
|
|
3421
|
-
["log", "-r", "@", "--no-graph", "--no-pager", "--color", "never", "--template", "change_id"],
|
|
3422
|
-
workspacePath2
|
|
3423
|
-
);
|
|
3604
|
+
const headBefore = await jjCommitId(workspacePath2);
|
|
3424
3605
|
await run("jj", ["rebase", "-d", target], workspacePath2);
|
|
3425
|
-
const
|
|
3606
|
+
const moved = await jjCommitId(workspacePath2) !== headBefore;
|
|
3607
|
+
const replayedRaw = moved ? await run(
|
|
3426
3608
|
"jj",
|
|
3427
3609
|
[
|
|
3428
3610
|
"log",
|
|
@@ -3437,7 +3619,7 @@ var jjBackend = {
|
|
|
3437
3619
|
'description.first_line() ++ "\\n"'
|
|
3438
3620
|
],
|
|
3439
3621
|
workspacePath2
|
|
3440
|
-
).catch(() => "");
|
|
3622
|
+
).catch(() => "") : "";
|
|
3441
3623
|
const replayed = replayedRaw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3442
3624
|
const conflictRaw = await run(
|
|
3443
3625
|
"jj",
|
|
@@ -3456,12 +3638,17 @@ var jjBackend = {
|
|
|
3456
3638
|
).catch(() => "");
|
|
3457
3639
|
const conflicts = conflictRaw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3458
3640
|
if (conflicts.length > 0) {
|
|
3459
|
-
throw new WorkspaceConflictError(workspacePath2, target, conflicts);
|
|
3641
|
+
throw new WorkspaceConflictError(workspacePath2, target, conflicts, false);
|
|
3460
3642
|
}
|
|
3461
|
-
|
|
3462
|
-
return { fromRef: target, replayed, conflicts: [] };
|
|
3643
|
+
const targetId = await jjSingleCommitId(workspacePath2, target);
|
|
3644
|
+
if (targetId === void 0) return { fromRef: target, replayed, conflicts: [] };
|
|
3645
|
+
const keepPrevious = previousParentRef !== void 0 && await jjSingleCommitId(workspacePath2, `${previousParentRef} & ${targetId}:: & ::@`) !== void 0;
|
|
3646
|
+
const parentRef = keepPrevious ? previousParentRef : targetId;
|
|
3647
|
+
return { fromRef: target, parentRef, replayed, conflicts: [] };
|
|
3463
3648
|
},
|
|
3464
|
-
// List jj commits in (baseRef..@), oldest-first
|
|
3649
|
+
// List jj commits in (baseRef..@), oldest-first, minus @ itself when
|
|
3650
|
+
// it is the empty, undescribed working-copy commit jj snapshots on
|
|
3651
|
+
// every command: that is "no work yet", not a commit. jj's templating
|
|
3465
3652
|
// gives us per-field strings; we glue them with NUL field-separators
|
|
3466
3653
|
// and \x1e record-separators so multi-line descriptions/bodies
|
|
3467
3654
|
// round-trip cleanly. The author timestamp template is
|
|
@@ -3476,7 +3663,7 @@ var jjBackend = {
|
|
|
3476
3663
|
[
|
|
3477
3664
|
"log",
|
|
3478
3665
|
"-r",
|
|
3479
|
-
|
|
3666
|
+
`(${baseRef}..@) ~ (@ & empty() & description(exact:""))`,
|
|
3480
3667
|
"--no-graph",
|
|
3481
3668
|
"--no-pager",
|
|
3482
3669
|
"--color",
|
|
@@ -3527,7 +3714,46 @@ var jjBackend = {
|
|
|
3527
3714
|
};
|
|
3528
3715
|
var jjCommitSummaryTemplate = 'commit_id ++ "\\x00" ++ description.first_line() ++ "\\x00" ++ description ++ "\\x00" ++ author.timestamp().format("%Y-%m-%dT%H:%M:%S%:z") ++ "\\x00" ++ author.name() ++ "\\x1e"';
|
|
3529
3716
|
function jjWorkspaceName(workspacePath2) {
|
|
3530
|
-
|
|
3717
|
+
const parts = workspacePath2.split("/").filter((p) => p.length > 0);
|
|
3718
|
+
return parts.slice(-2).join("/") || workspacePath2;
|
|
3719
|
+
}
|
|
3720
|
+
async function forgetIfRootMissing(name, projectRoot) {
|
|
3721
|
+
try {
|
|
3722
|
+
const out = await run(
|
|
3723
|
+
"jj",
|
|
3724
|
+
[
|
|
3725
|
+
"workspace",
|
|
3726
|
+
"list",
|
|
3727
|
+
"--color",
|
|
3728
|
+
"never",
|
|
3729
|
+
"--template",
|
|
3730
|
+
'name ++ "\\x00" ++ self.root() ++ "\\x1e"'
|
|
3731
|
+
],
|
|
3732
|
+
projectRoot
|
|
3733
|
+
);
|
|
3734
|
+
const stale = out.split("").map((r) => r.trim().split("\0")).some(([n, root]) => n === name && root === "");
|
|
3735
|
+
if (stale) await run("jj", ["workspace", "forget", name], projectRoot);
|
|
3736
|
+
} catch {
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
async function jjSingleCommitId(workspacePath2, revset) {
|
|
3740
|
+
const out = await run(
|
|
3741
|
+
"jj",
|
|
3742
|
+
[
|
|
3743
|
+
"log",
|
|
3744
|
+
"-r",
|
|
3745
|
+
revset,
|
|
3746
|
+
"--no-graph",
|
|
3747
|
+
"--no-pager",
|
|
3748
|
+
"--color",
|
|
3749
|
+
"never",
|
|
3750
|
+
"--template",
|
|
3751
|
+
'commit_id ++ "\\n"'
|
|
3752
|
+
],
|
|
3753
|
+
workspacePath2
|
|
3754
|
+
).catch(() => "");
|
|
3755
|
+
const ids = out.split("\n").filter((l) => l.length > 0);
|
|
3756
|
+
return ids.length === 1 ? ids[0] : void 0;
|
|
3531
3757
|
}
|
|
3532
3758
|
async function jjCommitId(workspacePath2) {
|
|
3533
3759
|
return run(
|
|
@@ -3671,12 +3897,15 @@ var slBackend = {
|
|
|
3671
3897
|
}
|
|
3672
3898
|
},
|
|
3673
3899
|
// Rebase the active draft chain onto `fromRef` (default = `trunk()`).
|
|
3674
|
-
//
|
|
3675
|
-
//
|
|
3676
|
-
//
|
|
3677
|
-
//
|
|
3678
|
-
//
|
|
3679
|
-
|
|
3900
|
+
// We pre-check the WC and refuse with the typed WorkspaceDirtyError.
|
|
3901
|
+
// `sl rebase` exits 0 on success (also when there is nothing to
|
|
3902
|
+
// rebase) and 1 on unresolved conflicts. Conflict surface via
|
|
3903
|
+
// `sl resolve --list --tool=internal:dumpjson` is brittle across
|
|
3904
|
+
// versions, so on failure we read the textual `sl resolve --list`
|
|
3905
|
+
// and keep the U-prefixed (unresolved) lines. A failure with no
|
|
3906
|
+
// unresolved files (bad ref, unresolvable trunk()) is rethrown, as
|
|
3907
|
+
// the git impl does.
|
|
3908
|
+
async rebaseTo(workspacePath2, fromRef, previousParentRef) {
|
|
3680
3909
|
if (!existsSync5(workspacePath2)) {
|
|
3681
3910
|
throw new Error(`vcs sl: workspace path missing: ${workspacePath2}`);
|
|
3682
3911
|
}
|
|
@@ -3685,25 +3914,41 @@ var slBackend = {
|
|
|
3685
3914
|
if (dirtyFiles.length > 0) {
|
|
3686
3915
|
throw new WorkspaceDirtyError(workspacePath2, dirtyFiles);
|
|
3687
3916
|
}
|
|
3688
|
-
await
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3917
|
+
const headBefore = await slCommitId(workspacePath2);
|
|
3918
|
+
try {
|
|
3919
|
+
await run(
|
|
3920
|
+
"sl",
|
|
3921
|
+
["--config", "ui.username=mu <mu@local>", "rebase", "-d", target],
|
|
3922
|
+
workspacePath2
|
|
3923
|
+
);
|
|
3924
|
+
} catch (err) {
|
|
3925
|
+
const conflicts = await listSlUnresolved(workspacePath2);
|
|
3696
3926
|
await run("sl", ["rebase", "--abort"], workspacePath2).catch(() => {
|
|
3697
3927
|
});
|
|
3698
|
-
|
|
3928
|
+
if (conflicts.length > 0) {
|
|
3929
|
+
throw new WorkspaceConflictError(workspacePath2, target, conflicts);
|
|
3930
|
+
}
|
|
3931
|
+
throw err;
|
|
3699
3932
|
}
|
|
3700
|
-
const
|
|
3933
|
+
const targetNode = await run(
|
|
3701
3934
|
"sl",
|
|
3702
|
-
["log", "-r",
|
|
3935
|
+
["log", "-r", `last(${target})`, "--template", "{node}"],
|
|
3703
3936
|
workspacePath2
|
|
3704
3937
|
).catch(() => "");
|
|
3938
|
+
const keepPrevious = previousParentRef !== void 0 && targetNode.length > 0 && await run(
|
|
3939
|
+
"sl",
|
|
3940
|
+
["log", "-r", `${previousParentRef} & ${targetNode}:: & ::.`, "--template", "{node}"],
|
|
3941
|
+
workspacePath2
|
|
3942
|
+
).catch(() => "") !== "";
|
|
3943
|
+
const parentRef = keepPrevious ? previousParentRef : targetNode;
|
|
3944
|
+
const moved = await slCommitId(workspacePath2) !== headBefore;
|
|
3945
|
+
const replayedRaw = moved ? await run(
|
|
3946
|
+
"sl",
|
|
3947
|
+
["log", "-r", `${target}::. - ${target}`, "--template", "{desc|firstline}\\n"],
|
|
3948
|
+
workspacePath2
|
|
3949
|
+
).catch(() => "") : "";
|
|
3705
3950
|
const replayed = replayedRaw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3706
|
-
return { fromRef: target, replayed, conflicts: [] };
|
|
3951
|
+
return parentRef.length > 0 ? { fromRef: target, parentRef, replayed, conflicts: [] } : { fromRef: target, replayed, conflicts: [] };
|
|
3707
3952
|
},
|
|
3708
3953
|
// List sl commits in (baseRef..., minus baseRef itself), oldest-
|
|
3709
3954
|
// first. Same NUL-field / \x1e-record format as the jj impl;
|
|
@@ -3879,12 +4124,13 @@ async function freeWorkspace(db, agent, opts) {
|
|
|
3879
4124
|
db,
|
|
3880
4125
|
row.workstreamName,
|
|
3881
4126
|
"workspace.free",
|
|
3882
|
-
`workspace free ${agent} (backend=${row.backend}, path=${row.path}${result.committedRef ? `, committed=${result.committedRef.slice(0, 12)}` : ""})`
|
|
4127
|
+
`workspace free ${agent} (backend=${row.backend}, path=${row.path}${result.committedRef ? `, committed=${result.committedRef.slice(0, 12)}` : ""}${result.branch ? `, branch=${result.branch}` : ""})`
|
|
3883
4128
|
);
|
|
3884
4129
|
return {
|
|
3885
4130
|
removed: result.removed,
|
|
3886
4131
|
rowDeleted: del.changes > 0,
|
|
3887
|
-
...result.committedRef !== void 0 ? { committedRef: result.committedRef } : {}
|
|
4132
|
+
...result.committedRef !== void 0 ? { committedRef: result.committedRef } : {},
|
|
4133
|
+
...result.branch !== void 0 ? { branch: result.branch } : {}
|
|
3888
4134
|
};
|
|
3889
4135
|
}
|
|
3890
4136
|
async function isWorkspaceClean(row) {
|
|
@@ -4073,10 +4319,11 @@ function noteFromDb(row) {
|
|
|
4073
4319
|
createdAt: row.created_at
|
|
4074
4320
|
};
|
|
4075
4321
|
}
|
|
4076
|
-
function lookupTaskAnyWorkstream(db, localId) {
|
|
4322
|
+
function lookupTaskAnyWorkstream(db, localId, preferWorkstream) {
|
|
4077
4323
|
const row = db.prepare(
|
|
4078
|
-
`SELECT ${SELECT_TASK_COLS} ${TASK_FROM_JOIN} WHERE t.local_id = ?
|
|
4079
|
-
|
|
4324
|
+
`SELECT ${SELECT_TASK_COLS} ${TASK_FROM_JOIN} WHERE t.local_id = ?
|
|
4325
|
+
ORDER BY (ws.name = ?) DESC, ws.name LIMIT 1`
|
|
4326
|
+
).get(localId, preferWorkstream ?? null);
|
|
4080
4327
|
return row ? rowFromDb3(row) : void 0;
|
|
4081
4328
|
}
|
|
4082
4329
|
function taskIdFor(db, localId, workstream) {
|
|
@@ -4101,6 +4348,16 @@ function getTask(db, localId, workstream) {
|
|
|
4101
4348
|
).get(wsId, localId);
|
|
4102
4349
|
return row ? rowFromDb3(row) : void 0;
|
|
4103
4350
|
}
|
|
4351
|
+
function getTaskOwner(db, localId, workstream) {
|
|
4352
|
+
return db.prepare(
|
|
4353
|
+
`SELECT a.name AS name, aws.name AS workstreamName
|
|
4354
|
+
FROM tasks t
|
|
4355
|
+
JOIN workstreams tws ON tws.id = t.workstream_id
|
|
4356
|
+
JOIN agents a ON a.id = t.owner_id
|
|
4357
|
+
JOIN workstreams aws ON aws.id = a.workstream_id
|
|
4358
|
+
WHERE t.local_id = ? AND tws.name = ?`
|
|
4359
|
+
).get(localId, workstream);
|
|
4360
|
+
}
|
|
4104
4361
|
function asList(v) {
|
|
4105
4362
|
if (v === void 0) return void 0;
|
|
4106
4363
|
return typeof v === "string" ? [v] : v;
|
|
@@ -4202,6 +4459,10 @@ function listNotes(db, taskLocalId, workstream, opts = {}) {
|
|
|
4202
4459
|
const taskId = taskIdFor(db, taskLocalId, workstream);
|
|
4203
4460
|
if (taskId === null) return [];
|
|
4204
4461
|
let cutoff = opts.since;
|
|
4462
|
+
if (cutoff !== void 0) {
|
|
4463
|
+
const ms = Date.parse(cutoff);
|
|
4464
|
+
if (!Number.isNaN(ms)) cutoff = new Date(ms).toISOString();
|
|
4465
|
+
}
|
|
4205
4466
|
if (cutoff === void 0 && opts.sinceClaim === true) {
|
|
4206
4467
|
const at = lastClaimEventAt(db, workstream, taskLocalId);
|
|
4207
4468
|
if (at !== null) cutoff = at;
|
|
@@ -4230,6 +4491,24 @@ function listTasksByOwner(db, workstream, owner, opts = {}) {
|
|
|
4230
4491
|
}
|
|
4231
4492
|
|
|
4232
4493
|
// src/tasks/id.ts
|
|
4494
|
+
var TaskTitleSlugEmptyError = class extends Error {
|
|
4495
|
+
constructor(title) {
|
|
4496
|
+
super(
|
|
4497
|
+
`title yields empty slug: ${JSON.stringify(title)} (no ASCII letter or digit to derive an id from)`
|
|
4498
|
+
);
|
|
4499
|
+
this.title = title;
|
|
4500
|
+
}
|
|
4501
|
+
title;
|
|
4502
|
+
name = "TaskTitleSlugEmptyError";
|
|
4503
|
+
errorNextSteps() {
|
|
4504
|
+
return [
|
|
4505
|
+
{
|
|
4506
|
+
intent: "Pass the id explicitly as the positional",
|
|
4507
|
+
command: 'mu task add <id> --title "..." --impact <n> --effort-days <n>'
|
|
4508
|
+
}
|
|
4509
|
+
];
|
|
4510
|
+
}
|
|
4511
|
+
};
|
|
4233
4512
|
var TASK_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
4234
4513
|
function isValidTaskId(id) {
|
|
4235
4514
|
return TASK_ID_RE.test(id);
|
|
@@ -4242,15 +4521,14 @@ function slugifyTitle(title) {
|
|
|
4242
4521
|
function slugifyTitleVerbose(title) {
|
|
4243
4522
|
const stripped = title.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4244
4523
|
if (stripped.length === 0) {
|
|
4245
|
-
throw new
|
|
4524
|
+
throw new TaskTitleSlugEmptyError(title);
|
|
4246
4525
|
}
|
|
4247
4526
|
let trimmed;
|
|
4248
4527
|
if (stripped.length <= SLUG_SOFT_CAP) {
|
|
4249
4528
|
trimmed = stripped;
|
|
4250
4529
|
} else {
|
|
4251
|
-
const
|
|
4252
|
-
|
|
4253
|
-
trimmed = lastSep > 0 ? window.slice(0, lastSep) : window;
|
|
4530
|
+
const lastSep = stripped.lastIndexOf("_", SLUG_SOFT_CAP);
|
|
4531
|
+
trimmed = lastSep > 0 ? stripped.slice(0, lastSep) : stripped.slice(0, SLUG_SOFT_CAP);
|
|
4254
4532
|
}
|
|
4255
4533
|
const applyPrefix = (s) => /^[a-z]/.test(s) ? s.slice(0, SLUG_HARD_CAP) : `t_${s}`.slice(0, SLUG_HARD_CAP);
|
|
4256
4534
|
const slug = applyPrefix(trimmed);
|
|
@@ -4293,8 +4571,7 @@ var TaskNotFoundError = class extends Error {
|
|
|
4293
4571
|
const recipe = `mu sql "SELECT ws.name AS workstream, t.local_id, t.status, t.title FROM tasks t JOIN workstreams ws ON ws.id = t.workstream_id WHERE LOWER(t.local_id) LIKE '%${idLit}%' OR LOWER(t.title) LIKE '%${idLit}%'"`;
|
|
4294
4572
|
return [
|
|
4295
4573
|
{ intent: "List tasks in workstream", command: "mu task list -w <workstream>" },
|
|
4296
|
-
{ intent: "Search by substring (id + title)", command: recipe }
|
|
4297
|
-
{ intent: "Find which workstream owns it", command: recipe }
|
|
4574
|
+
{ intent: "Search every workstream by substring (id + title)", command: recipe }
|
|
4298
4575
|
];
|
|
4299
4576
|
}
|
|
4300
4577
|
};
|
|
@@ -4309,7 +4586,7 @@ var TaskIdInvalidError = class extends Error {
|
|
|
4309
4586
|
const sanitised = sanitiseTaskId(this.attempted);
|
|
4310
4587
|
return [
|
|
4311
4588
|
{
|
|
4312
|
-
intent: "Use the auto-derived id (drop
|
|
4589
|
+
intent: "Use the auto-derived id (drop the <id> positional; --title derives it)",
|
|
4313
4590
|
command: 'mu task add --title "..." --impact <n> --effort-days <n>'
|
|
4314
4591
|
},
|
|
4315
4592
|
{
|
|
@@ -4419,6 +4696,24 @@ var ClaimerNotRegisteredError = class extends Error {
|
|
|
4419
4696
|
return steps;
|
|
4420
4697
|
}
|
|
4421
4698
|
};
|
|
4699
|
+
var TaskClaimStaleWorkspaceError = class extends Error {
|
|
4700
|
+
constructor(staleness) {
|
|
4701
|
+
super(
|
|
4702
|
+
`${staleness.agentName} workspace is ${staleness.commitsBehindMain} commits behind main (\u2265${WORKSPACE_STALE_THRESHOLD} = stale); refresh before dispatch or rerun without --strict-staleness`
|
|
4703
|
+
);
|
|
4704
|
+
this.staleness = staleness;
|
|
4705
|
+
}
|
|
4706
|
+
staleness;
|
|
4707
|
+
name = "TaskClaimStaleWorkspaceError";
|
|
4708
|
+
errorNextSteps() {
|
|
4709
|
+
return [
|
|
4710
|
+
{
|
|
4711
|
+
intent: "Refresh first",
|
|
4712
|
+
command: `mu workspace refresh ${this.staleness.agentName} -w ${this.staleness.workstreamName}`
|
|
4713
|
+
}
|
|
4714
|
+
];
|
|
4715
|
+
}
|
|
4716
|
+
};
|
|
4422
4717
|
var CycleError = class extends Error {
|
|
4423
4718
|
constructor(from, to) {
|
|
4424
4719
|
super(`adding edge ${from} -> ${to} would create a cycle`);
|
|
@@ -4445,6 +4740,38 @@ var CycleError = class extends Error {
|
|
|
4445
4740
|
];
|
|
4446
4741
|
}
|
|
4447
4742
|
};
|
|
4743
|
+
var ReaperDetectedDuringWaitError = class extends Error {
|
|
4744
|
+
constructor(taskId, previousOwner, workstream) {
|
|
4745
|
+
const ownerBit = previousOwner !== null ? `owner=${previousOwner}` : "owner=<unknown>";
|
|
4746
|
+
super(
|
|
4747
|
+
`task ${taskId} was IN_PROGRESS ${ownerBit} until just now; reaper detected dead pane and flipped to OPEN. wait abandoned. Re-dispatch a worker and retry.`
|
|
4748
|
+
);
|
|
4749
|
+
this.taskId = taskId;
|
|
4750
|
+
this.previousOwner = previousOwner;
|
|
4751
|
+
this.workstream = workstream;
|
|
4752
|
+
}
|
|
4753
|
+
taskId;
|
|
4754
|
+
previousOwner;
|
|
4755
|
+
workstream;
|
|
4756
|
+
name = "ReaperDetectedDuringWaitError";
|
|
4757
|
+
errorNextSteps() {
|
|
4758
|
+
const ws = this.workstream;
|
|
4759
|
+
return [
|
|
4760
|
+
{
|
|
4761
|
+
intent: "Inspect the task's current state",
|
|
4762
|
+
command: `mu task show ${this.taskId} -w ${ws}`
|
|
4763
|
+
},
|
|
4764
|
+
{
|
|
4765
|
+
intent: "List live agents in the workstream (post-reap)",
|
|
4766
|
+
command: `mu agent list -w ${ws}`
|
|
4767
|
+
},
|
|
4768
|
+
{
|
|
4769
|
+
intent: "Re-dispatch a fresh worker, then re-run the wait",
|
|
4770
|
+
command: `mu agent spawn <name> -w ${ws} && mu task claim ${this.taskId} --for <name> -w ${ws}`
|
|
4771
|
+
}
|
|
4772
|
+
];
|
|
4773
|
+
}
|
|
4774
|
+
};
|
|
4448
4775
|
function stallWaitHint(owner, ws, runs) {
|
|
4449
4776
|
if (runs === void 0 || runs < 1) return [];
|
|
4450
4777
|
return [
|
|
@@ -4455,7 +4782,7 @@ function stallWaitHint(owner, ws, runs) {
|
|
|
4455
4782
|
];
|
|
4456
4783
|
}
|
|
4457
4784
|
var StallDetectedDuringWaitError = class extends Error {
|
|
4458
|
-
constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false, ownerRuns) {
|
|
4785
|
+
constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false, ownerRuns, ownerWorkstream = workstream) {
|
|
4459
4786
|
const ownerBit = owner !== null ? owner : "<unknown>";
|
|
4460
4787
|
super(
|
|
4461
4788
|
`task ${taskName} owned by ${ownerBit} has been needs_input for ${ageSecs}s; exiting per --on-stall exit. Read the pane to see which it is \u2014 finished without closing, waiting on an answer, or sitting at a prompt.`
|
|
@@ -4466,6 +4793,7 @@ var StallDetectedDuringWaitError = class extends Error {
|
|
|
4466
4793
|
this.ageSecs = ageSecs;
|
|
4467
4794
|
this.ownerExpectsCtl = ownerExpectsCtl;
|
|
4468
4795
|
this.ownerRuns = ownerRuns;
|
|
4796
|
+
this.ownerWorkstream = ownerWorkstream;
|
|
4469
4797
|
}
|
|
4470
4798
|
taskName;
|
|
4471
4799
|
owner;
|
|
@@ -4473,6 +4801,7 @@ var StallDetectedDuringWaitError = class extends Error {
|
|
|
4473
4801
|
ageSecs;
|
|
4474
4802
|
ownerExpectsCtl;
|
|
4475
4803
|
ownerRuns;
|
|
4804
|
+
ownerWorkstream;
|
|
4476
4805
|
name = "StallDetectedDuringWaitError";
|
|
4477
4806
|
/**
|
|
4478
4807
|
* Diagnose before acting. `mu agent read` comes FIRST because the
|
|
@@ -4485,21 +4814,22 @@ var StallDetectedDuringWaitError = class extends Error {
|
|
|
4485
4814
|
*/
|
|
4486
4815
|
errorNextSteps() {
|
|
4487
4816
|
const ws = this.workstream;
|
|
4817
|
+
const ownerWs = this.ownerWorkstream;
|
|
4488
4818
|
const ownerBit = this.owner !== null ? this.owner : "<owner>";
|
|
4489
4819
|
return [
|
|
4490
|
-
...stallWaitHint(ownerBit,
|
|
4820
|
+
...stallWaitHint(ownerBit, ownerWs, this.ownerRuns),
|
|
4491
4821
|
{
|
|
4492
4822
|
intent: "Read the worker's pane to see what it is waiting on",
|
|
4493
|
-
command: `mu agent read ${ownerBit} -w ${
|
|
4823
|
+
command: `mu agent read ${ownerBit} -w ${ownerWs} --lines 60`
|
|
4494
4824
|
},
|
|
4495
4825
|
{
|
|
4496
4826
|
intent: "Answer a question, or poke a worker that stalled mid-step",
|
|
4497
|
-
command: `mu agent send ${ownerBit} '<answer-or-retry-instruction>' -w ${
|
|
4827
|
+
command: `mu agent send ${ownerBit} '<answer-or-retry-instruction>' -w ${ownerWs}`
|
|
4498
4828
|
},
|
|
4499
4829
|
...this.ownerExpectsCtl ? [
|
|
4500
4830
|
{
|
|
4501
4831
|
intent: "Stuck mid-turn? Stop it through the control socket before kick or close",
|
|
4502
|
-
command: `mu agent abort ${ownerBit} -w ${
|
|
4832
|
+
command: `mu agent abort ${ownerBit} -w ${ownerWs}`
|
|
4503
4833
|
}
|
|
4504
4834
|
] : [],
|
|
4505
4835
|
{
|
|
@@ -4531,12 +4861,12 @@ var CrossWorkstreamEdgeError = class extends Error {
|
|
|
4531
4861
|
errorNextSteps() {
|
|
4532
4862
|
return [
|
|
4533
4863
|
{
|
|
4534
|
-
intent: "
|
|
4535
|
-
command: `mu
|
|
4864
|
+
intent: "Duplicate the blocker into the dependent's workstream",
|
|
4865
|
+
command: `mu task add <new-id> -w ${this.dependentWorkstream} --title "<copy of ${this.blocker}>" --impact <n> --effort-days <n>`
|
|
4536
4866
|
},
|
|
4537
4867
|
{
|
|
4538
|
-
intent: "Or
|
|
4539
|
-
command: `mu task
|
|
4868
|
+
intent: "Or wait on the blocker across workstreams instead of an edge",
|
|
4869
|
+
command: `mu task wait ${this.blockerWorkstream}/${this.blocker}`
|
|
4540
4870
|
}
|
|
4541
4871
|
];
|
|
4542
4872
|
}
|
|
@@ -4729,7 +5059,7 @@ function addBlockEdgeImpl(db, workstream, blocked, blocker) {
|
|
|
4729
5059
|
}
|
|
4730
5060
|
const blockedRow = getTask(db, blocked, workstream);
|
|
4731
5061
|
if (!blockedRow) throw new TaskNotFoundError(blocked);
|
|
4732
|
-
const blockerRow = lookupTaskAnyWorkstream(db, blocker);
|
|
5062
|
+
const blockerRow = lookupTaskAnyWorkstream(db, blocker, blockedRow.workstreamName);
|
|
4733
5063
|
if (!blockerRow) throw new TaskNotFoundError(blocker);
|
|
4734
5064
|
if (blockedRow.workstreamName !== blockerRow.workstreamName) {
|
|
4735
5065
|
throw new CrossWorkstreamEdgeError(
|
|
@@ -4755,7 +5085,7 @@ function addBlockEdgeImpl(db, workstream, blocked, blocker) {
|
|
|
4755
5085
|
return true;
|
|
4756
5086
|
}
|
|
4757
5087
|
return false;
|
|
4758
|
-
})();
|
|
5088
|
+
}).immediate();
|
|
4759
5089
|
return { added };
|
|
4760
5090
|
}
|
|
4761
5091
|
function removeBlockEdge(db, workstream, blocked, blocker) {
|
|
@@ -4780,7 +5110,7 @@ function removeBlockEdgeImpl(db, workstream, blocked, blocker) {
|
|
|
4780
5110
|
return true;
|
|
4781
5111
|
}
|
|
4782
5112
|
return false;
|
|
4783
|
-
})();
|
|
5113
|
+
}).immediate();
|
|
4784
5114
|
if (removed) {
|
|
4785
5115
|
}
|
|
4786
5116
|
return { removed };
|
|
@@ -4802,7 +5132,7 @@ function reparentTaskImpl(db, taskLocalId, blockers, scope) {
|
|
|
4802
5132
|
if (blockerLocalId === taskLocalId) {
|
|
4803
5133
|
throw new CycleError(blockerLocalId, taskLocalId);
|
|
4804
5134
|
}
|
|
4805
|
-
const blocker = lookupTaskAnyWorkstream(db, blockerLocalId);
|
|
5135
|
+
const blocker = lookupTaskAnyWorkstream(db, blockerLocalId, task.workstreamName);
|
|
4806
5136
|
if (!blocker) throw new TaskNotFoundError(blockerLocalId);
|
|
4807
5137
|
if (blocker.workstreamName !== task.workstreamName) {
|
|
4808
5138
|
throw new CrossWorkstreamEdgeError(
|
|
@@ -4836,7 +5166,7 @@ function reparentTaskImpl(db, taskLocalId, blockers, scope) {
|
|
|
4836
5166
|
}
|
|
4837
5167
|
touchTask(db, taskSurrogateId, now);
|
|
4838
5168
|
return { removedEdges: removed.changes, addedEdges: blockerIds.length };
|
|
4839
|
-
})();
|
|
5169
|
+
}).immediate();
|
|
4840
5170
|
}
|
|
4841
5171
|
|
|
4842
5172
|
// src/workstream.ts
|
|
@@ -4862,6 +5192,27 @@ async function sessionAlive(session) {
|
|
|
4862
5192
|
function isScratchWorkstream(name) {
|
|
4863
5193
|
return RESERVED_WORKSTREAM_NAMES.has(name);
|
|
4864
5194
|
}
|
|
5195
|
+
var MU_PLACEHOLDER_WINDOW = "_mu";
|
|
5196
|
+
async function ensureWorkstreamSession(session) {
|
|
5197
|
+
const mux = await activeMux();
|
|
5198
|
+
if (!await mux.sessionExists(session)) {
|
|
5199
|
+
await mux.newSession(session, { detached: true, windowName: MU_PLACEHOLDER_WINDOW });
|
|
5200
|
+
return { created: true, muWindowRepaired: false };
|
|
5201
|
+
}
|
|
5202
|
+
const windows = await mux.listWindows(session).catch(() => []);
|
|
5203
|
+
if (windows.some((w) => w.name === MU_PLACEHOLDER_WINDOW)) {
|
|
5204
|
+
return { created: false, muWindowRepaired: false };
|
|
5205
|
+
}
|
|
5206
|
+
await mux.newWindow({
|
|
5207
|
+
session,
|
|
5208
|
+
name: MU_PLACEHOLDER_WINDOW,
|
|
5209
|
+
// A backend with no create-and-run form rejects a command and
|
|
5210
|
+
// starts a shell anyway (see createOrReusePane in agents/spawn.ts).
|
|
5211
|
+
command: mux.startAgentInPane === void 0 ? process.env.SHELL ?? "/bin/sh" : "",
|
|
5212
|
+
detached: true
|
|
5213
|
+
});
|
|
5214
|
+
return { created: false, muWindowRepaired: true };
|
|
5215
|
+
}
|
|
4865
5216
|
function isValidWorkstreamName(name) {
|
|
4866
5217
|
if (!WORKSTREAM_NAME_RE.test(name)) return false;
|
|
4867
5218
|
if (name.startsWith(RESERVED_WORKSTREAM_PREFIX)) return false;
|
|
@@ -4893,11 +5244,12 @@ var WorkstreamNameInvalidError = class extends Error {
|
|
|
4893
5244
|
attempted;
|
|
4894
5245
|
name = "WorkstreamNameInvalidError";
|
|
4895
5246
|
errorNextSteps() {
|
|
4896
|
-
const
|
|
5247
|
+
const candidate = this.attempted.toLowerCase().replace(/^mu-/, "").replace(/[^a-z0-9_-]/g, "_").replace(/^[^a-z]+/, "").slice(0, 32);
|
|
5248
|
+
const sanitized = isValidWorkstreamName(candidate) ? candidate : "<name>";
|
|
4897
5249
|
const isPrefixCase = this.attempted.toLowerCase().startsWith(RESERVED_WORKSTREAM_PREFIX);
|
|
4898
5250
|
const intent = isPrefixCase ? "Retry without the 'mu-' prefix" : "Try a sanitized name (best guess)";
|
|
4899
5251
|
return [
|
|
4900
|
-
{ intent, command: `mu workstream init ${sanitized
|
|
5252
|
+
{ intent, command: `mu workstream init ${sanitized}` },
|
|
4901
5253
|
{ intent: "List existing workstreams", command: "mu workstream list" }
|
|
4902
5254
|
];
|
|
4903
5255
|
}
|
|
@@ -4905,6 +5257,25 @@ var WorkstreamNameInvalidError = class extends Error {
|
|
|
4905
5257
|
function assertValidWorkstreamName(name) {
|
|
4906
5258
|
if (!isValidWorkstreamName(name)) throw new WorkstreamNameInvalidError(name);
|
|
4907
5259
|
}
|
|
5260
|
+
var WorkstreamNameReservedError = class extends Error {
|
|
5261
|
+
constructor(attempted) {
|
|
5262
|
+
super(
|
|
5263
|
+
`workstream name ${JSON.stringify(attempted)} is reserved: it is the off-the-cuff bucket and auto-creates on first spawn. Don't 'init' it.`
|
|
5264
|
+
);
|
|
5265
|
+
this.attempted = attempted;
|
|
5266
|
+
}
|
|
5267
|
+
attempted;
|
|
5268
|
+
name = "WorkstreamNameReservedError";
|
|
5269
|
+
errorNextSteps() {
|
|
5270
|
+
return [
|
|
5271
|
+
{
|
|
5272
|
+
intent: "Just spawn into it (auto-creates)",
|
|
5273
|
+
command: `mu agent spawn <name> -w ${this.attempted}`
|
|
5274
|
+
},
|
|
5275
|
+
{ intent: "Use a durable workstream instead", command: "mu workstream init <name>" }
|
|
5276
|
+
];
|
|
5277
|
+
}
|
|
5278
|
+
};
|
|
4908
5279
|
function ensureWorkstream(db, name) {
|
|
4909
5280
|
assertValidWorkstreamName(name);
|
|
4910
5281
|
return withOpContext(
|
|
@@ -5132,7 +5503,7 @@ function addTaskImpl(db, opts) {
|
|
|
5132
5503
|
const row = getTask(db, opts.localId, opts.workstream);
|
|
5133
5504
|
if (!row) throw new Error(`addTask: row missing after insert: ${opts.localId}`);
|
|
5134
5505
|
return row;
|
|
5135
|
-
})();
|
|
5506
|
+
}).immediate();
|
|
5136
5507
|
}
|
|
5137
5508
|
function addNote(db, taskLocalId, content, opts) {
|
|
5138
5509
|
return withOpContext(
|
|
@@ -5153,7 +5524,7 @@ function insertNote(db, taskLocalId, content, opts) {
|
|
|
5153
5524
|
const r = db.prepare("INSERT INTO task_notes (task_id, author, content, created_at) VALUES (?, ?, ?, ?)").run(taskId, opts.author ?? null, content, now);
|
|
5154
5525
|
touchTask(db, taskId, now);
|
|
5155
5526
|
return r;
|
|
5156
|
-
})();
|
|
5527
|
+
}).immediate();
|
|
5157
5528
|
return {
|
|
5158
5529
|
author: opts.author ?? null,
|
|
5159
5530
|
content,
|
|
@@ -5171,11 +5542,25 @@ function deleteTaskImpl(db, localId, workstream, opts) {
|
|
|
5171
5542
|
const dryRun = opts.dryRun === true;
|
|
5172
5543
|
const before = getTask(db, localId, workstream);
|
|
5173
5544
|
if (!before) {
|
|
5174
|
-
return {
|
|
5545
|
+
return {
|
|
5546
|
+
deleted: false,
|
|
5547
|
+
deletedEdges: 0,
|
|
5548
|
+
deletedNotes: 0,
|
|
5549
|
+
dryRun,
|
|
5550
|
+
present: false,
|
|
5551
|
+
group: null
|
|
5552
|
+
};
|
|
5175
5553
|
}
|
|
5176
5554
|
const taskId = taskIdFor(db, localId, before.workstreamName);
|
|
5177
5555
|
if (taskId === null) {
|
|
5178
|
-
return {
|
|
5556
|
+
return {
|
|
5557
|
+
deleted: false,
|
|
5558
|
+
deletedEdges: 0,
|
|
5559
|
+
deletedNotes: 0,
|
|
5560
|
+
dryRun,
|
|
5561
|
+
present: false,
|
|
5562
|
+
group: null
|
|
5563
|
+
};
|
|
5179
5564
|
}
|
|
5180
5565
|
const edgesBefore = db.prepare("SELECT COUNT(*) AS n FROM task_edges WHERE from_task_id = ? OR to_task_id = ?").get(taskId, taskId).n;
|
|
5181
5566
|
const notesBefore = db.prepare("SELECT COUNT(*) AS n FROM task_notes WHERE task_id = ?").get(taskId).n;
|
|
@@ -5185,7 +5570,8 @@ function deleteTaskImpl(db, localId, workstream, opts) {
|
|
|
5185
5570
|
deletedEdges: edgesBefore,
|
|
5186
5571
|
deletedNotes: notesBefore,
|
|
5187
5572
|
dryRun: true,
|
|
5188
|
-
present: true
|
|
5573
|
+
present: true,
|
|
5574
|
+
group: null
|
|
5189
5575
|
};
|
|
5190
5576
|
}
|
|
5191
5577
|
const result = db.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
|
|
@@ -5195,7 +5581,9 @@ function deleteTaskImpl(db, localId, workstream, opts) {
|
|
|
5195
5581
|
deletedEdges: edgesBefore,
|
|
5196
5582
|
deletedNotes: notesBefore,
|
|
5197
5583
|
dryRun: false,
|
|
5198
|
-
present: true
|
|
5584
|
+
present: true,
|
|
5585
|
+
// deleteTask opened a fresh group around this call.
|
|
5586
|
+
group: deleted ? currentOpContext(db).groupId : null
|
|
5199
5587
|
};
|
|
5200
5588
|
}
|
|
5201
5589
|
function updateTask(db, localId, opts, scope) {
|
|
@@ -5286,8 +5674,8 @@ function closeTask(db, localId, opts) {
|
|
|
5286
5674
|
);
|
|
5287
5675
|
}
|
|
5288
5676
|
function closeTaskImpl(db, localId, opts) {
|
|
5289
|
-
const as = opts.as ?? "done";
|
|
5290
5677
|
const before = getTask(db, localId, opts.workstream);
|
|
5678
|
+
const as = opts.as ?? (before?.status === "CLOSED" ? before.substate : "done");
|
|
5291
5679
|
if (opts.ifReady && before) {
|
|
5292
5680
|
const edges = getTaskEdgesWithStatus(db, localId, before.workstreamName);
|
|
5293
5681
|
const blocking = edges.blockers.filter((e) => e.status !== "CLOSED").map((e) => e.name).sort();
|
|
@@ -5314,9 +5702,11 @@ function closeTaskImpl(db, localId, opts) {
|
|
|
5314
5702
|
if (!r.changed || !before) return { ...r, unblocked: [] };
|
|
5315
5703
|
recordReasonNote(db, localId, before.workstreamName, as, opts.why, opts.author);
|
|
5316
5704
|
recordEvidenceNote(db, localId, before.workstreamName, "CLOSE", opts);
|
|
5317
|
-
const unblocked = track ? readyDependents(db, localId, before.workstreamName).filter(
|
|
5705
|
+
const unblocked = track ? readyDependents(db, localId, before.workstreamName).filter(
|
|
5706
|
+
(n) => !readyBefore.includes(n)
|
|
5707
|
+
) : [];
|
|
5318
5708
|
return { ...r, unblocked };
|
|
5319
|
-
})();
|
|
5709
|
+
}).immediate();
|
|
5320
5710
|
}
|
|
5321
5711
|
function readyDependents(db, localId, workstream) {
|
|
5322
5712
|
const id = taskIdFor(db, localId, workstream);
|
|
@@ -5382,7 +5772,7 @@ function parkTask(db, localId, opts) {
|
|
|
5382
5772
|
recordEvidenceNote(db, localId, before.workstreamName, "PARK", opts);
|
|
5383
5773
|
}
|
|
5384
5774
|
return r;
|
|
5385
|
-
})()
|
|
5775
|
+
}).immediate()
|
|
5386
5776
|
);
|
|
5387
5777
|
}
|
|
5388
5778
|
function acceptTask(db, localId, opts) {
|
|
@@ -5404,7 +5794,7 @@ function acceptTask(db, localId, opts) {
|
|
|
5404
5794
|
const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
|
|
5405
5795
|
recordEvidenceNote(db, localId, before.workstreamName, "ACCEPT", opts);
|
|
5406
5796
|
return r;
|
|
5407
|
-
})()
|
|
5797
|
+
}).immediate()
|
|
5408
5798
|
);
|
|
5409
5799
|
}
|
|
5410
5800
|
function unparkTask(db, localId, opts) {
|
|
@@ -5426,7 +5816,7 @@ function unparkTask(db, localId, opts) {
|
|
|
5426
5816
|
const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
|
|
5427
5817
|
recordEvidenceNote(db, localId, before.workstreamName, "UNPARK", opts);
|
|
5428
5818
|
return r;
|
|
5429
|
-
})()
|
|
5819
|
+
}).immediate()
|
|
5430
5820
|
);
|
|
5431
5821
|
}
|
|
5432
5822
|
|
|
@@ -5533,7 +5923,7 @@ async function claimTaskImpl(db, localId, opts) {
|
|
|
5533
5923
|
previousStatus: before.status,
|
|
5534
5924
|
status: after.status
|
|
5535
5925
|
};
|
|
5536
|
-
})()
|
|
5926
|
+
}).immediate()
|
|
5537
5927
|
);
|
|
5538
5928
|
}
|
|
5539
5929
|
function assertNotParked(task, opts) {
|
|
@@ -5596,7 +5986,7 @@ async function claimSelf(db, localId, opts) {
|
|
|
5596
5986
|
previousStatus: before.status,
|
|
5597
5987
|
status: after.status
|
|
5598
5988
|
};
|
|
5599
|
-
})()
|
|
5989
|
+
}).immediate()
|
|
5600
5990
|
);
|
|
5601
5991
|
}
|
|
5602
5992
|
|
|
@@ -5664,11 +6054,11 @@ async function waitForTasks(db, input, opts) {
|
|
|
5664
6054
|
const stuckLabel = /* @__PURE__ */ new Map();
|
|
5665
6055
|
const ctlOwners = /* @__PURE__ */ new Set();
|
|
5666
6056
|
const ownerRuns = /* @__PURE__ */ new Map();
|
|
5667
|
-
const stuckAgeMs = async (status, owner
|
|
5668
|
-
if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner ===
|
|
6057
|
+
const stuckAgeMs = async (status, owner) => {
|
|
6058
|
+
if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner === void 0 || opts.readOwnerState === void 0)
|
|
5669
6059
|
return null;
|
|
5670
|
-
const reading = await opts.readOwnerState(
|
|
5671
|
-
const ownerKey = `${workstreamName}/${owner}`;
|
|
6060
|
+
const reading = await opts.readOwnerState(owner);
|
|
6061
|
+
const ownerKey = `${owner.workstreamName}/${owner.name}`;
|
|
5672
6062
|
if (reading?.ctl !== void 0) ctlOwners.add(ownerKey);
|
|
5673
6063
|
if (reading?.runs !== void 0) ownerRuns.set(ownerKey, reading.runs);
|
|
5674
6064
|
else ownerRuns.delete(ownerKey);
|
|
@@ -5693,21 +6083,23 @@ async function waitForTasks(db, input, opts) {
|
|
|
5693
6083
|
const row = getTask(db, ref.name, ref.workstreamName);
|
|
5694
6084
|
const status = row?.status ?? "OPEN";
|
|
5695
6085
|
const substate = row?.substate ?? DEFAULT_SUBSTATE[status];
|
|
5696
|
-
const
|
|
5697
|
-
const
|
|
6086
|
+
const ownerRef = row === void 0 ? void 0 : getTaskOwner(db, ref.name, ref.workstreamName);
|
|
6087
|
+
const owner = ownerRef?.name ?? row?.ownerName ?? null;
|
|
6088
|
+
const ownerWs = ownerRef?.workstreamName ?? ref.workstreamName;
|
|
6089
|
+
const ageMs = await stuckAgeMs(status, ownerRef);
|
|
5698
6090
|
const stuck = ageMs !== null;
|
|
5699
6091
|
const key = refKey(ref);
|
|
5700
6092
|
if (ageMs !== null && !stuckWarned.has(key)) {
|
|
5701
6093
|
stuckWarned.add(key);
|
|
5702
6094
|
const ageSecs = Math.round(ageMs / 1e3);
|
|
5703
6095
|
const ownerBit = owner ?? "<none>";
|
|
5704
|
-
const runs = ownerRuns.get(`${
|
|
5705
|
-
const stateBit = stuckLabel.get(`${
|
|
5706
|
-
const waitLine = stallWaitHint(ownerBit,
|
|
6096
|
+
const runs = ownerRuns.get(`${ownerWs}/${ownerBit}`);
|
|
6097
|
+
const stateBit = stuckLabel.get(`${ownerWs}/${ownerBit}`) ?? "needs_input";
|
|
6098
|
+
const waitLine = stallWaitHint(ownerBit, ownerWs, runs).map((s) => ` ${s.command}
|
|
5707
6099
|
`).join("");
|
|
5708
6100
|
currentStuckWarn(
|
|
5709
6101
|
`\x1B[33mmu task wait: ${key} needs attention \u2014 owner=${ownerBit} has been in ${stateBit} for ${formatStallAge(ageMs)}. It may have finished without closing, be waiting on an answer, or be sitting at a prompt.\x1B[0m
|
|
5710
|
-
` + waitLine + ` mu agent read ${ownerBit} -w ${
|
|
6102
|
+
` + waitLine + ` mu agent read ${ownerBit} -w ${ownerWs} --lines 60
|
|
5711
6103
|
`
|
|
5712
6104
|
);
|
|
5713
6105
|
emitEvent(
|
|
@@ -5723,8 +6115,9 @@ async function waitForTasks(db, input, opts) {
|
|
|
5723
6115
|
owner,
|
|
5724
6116
|
ref.workstreamName,
|
|
5725
6117
|
ageSecs,
|
|
5726
|
-
ctlOwners.has(`${
|
|
5727
|
-
runs
|
|
6118
|
+
ctlOwners.has(`${ownerWs}/${owner ?? ""}`),
|
|
6119
|
+
runs,
|
|
6120
|
+
ownerWs
|
|
5728
6121
|
);
|
|
5729
6122
|
}
|
|
5730
6123
|
}
|
|
@@ -5734,7 +6127,7 @@ async function waitForTasks(db, input, opts) {
|
|
|
5734
6127
|
status,
|
|
5735
6128
|
substate,
|
|
5736
6129
|
owner,
|
|
5737
|
-
reachedTarget: status === target,
|
|
6130
|
+
reachedTarget: row !== void 0 && status === target,
|
|
5738
6131
|
stuck
|
|
5739
6132
|
});
|
|
5740
6133
|
}
|
|
@@ -5810,7 +6203,7 @@ async function abortAgent(db, name, opts) {
|
|
|
5810
6203
|
}
|
|
5811
6204
|
async function interruptAgent(db, name, text, opts) {
|
|
5812
6205
|
const r = await interruptAgentImpl(db, name, text, opts);
|
|
5813
|
-
recordSend(db, { name, workstreamName: opts.workstream }, "interrupt", text
|
|
6206
|
+
recordSend(db, { name, workstreamName: opts.workstream }, "interrupt", Buffer.byteLength(text), {
|
|
5814
6207
|
transport: "ctl",
|
|
5815
6208
|
wasBusy: r.wasBusy
|
|
5816
6209
|
});
|
|
@@ -5866,11 +6259,7 @@ async function adoptAgent(db, opts) {
|
|
|
5866
6259
|
const panesInSession = await mux.listPanesInSession(expectedSession);
|
|
5867
6260
|
const matchingPane = panesInSession.find((p) => p.paneId === opts.paneId);
|
|
5868
6261
|
if (!matchingPane) {
|
|
5869
|
-
throw new
|
|
5870
|
-
`pane ${opts.paneId}`,
|
|
5871
|
-
opts.workstream,
|
|
5872
|
-
"a different tmux session"
|
|
5873
|
-
);
|
|
6262
|
+
throw new PaneNotInSessionError(opts.paneId, opts.workstream, expectedSession);
|
|
5874
6263
|
}
|
|
5875
6264
|
const previousTitle = matchingPane.title.length > 0 ? matchingPane.title : null;
|
|
5876
6265
|
const candidate = opts.name ?? (previousTitle !== null ? parseAgentNameFromTitle(previousTitle) : "");
|
|
@@ -6128,19 +6517,26 @@ function insertAgent(db, input) {
|
|
|
6128
6517
|
ensureWorkstream(db, input.workstream);
|
|
6129
6518
|
const workstreamId = resolveWorkstreamId(db, input.workstream);
|
|
6130
6519
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6520
|
+
try {
|
|
6521
|
+
db.prepare(
|
|
6522
|
+
`INSERT INTO agents (name, workstream_id, cli, pane_id, status, role, tab, created_at, updated_at)
|
|
6523
|
+
VALUES (@name, @workstreamId, COALESCE(@cli, 'pi'), @paneId, 'spawning',
|
|
6524
|
+
COALESCE(@role, 'full-access'), @tab, @now, @now)`
|
|
6525
|
+
).run({
|
|
6526
|
+
name: input.name,
|
|
6527
|
+
workstreamId,
|
|
6528
|
+
cli: input.cli ?? null,
|
|
6529
|
+
paneId: input.paneId,
|
|
6530
|
+
role: input.role ?? null,
|
|
6531
|
+
tab: input.tab ?? null,
|
|
6532
|
+
now
|
|
6533
|
+
});
|
|
6534
|
+
} catch (err) {
|
|
6535
|
+
if (err.code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
6536
|
+
throw new AgentExistsError(input.name);
|
|
6537
|
+
}
|
|
6538
|
+
throw err;
|
|
6539
|
+
}
|
|
6144
6540
|
const row = getAgent(db, input.name, input.workstream);
|
|
6145
6541
|
if (!row) throw new Error(`agents.insertAgent: row not found after insert: ${input.name}`);
|
|
6146
6542
|
return row;
|
|
@@ -6230,7 +6626,7 @@ function deleteAgentRow(db, name, workstream) {
|
|
|
6230
6626
|
);
|
|
6231
6627
|
}
|
|
6232
6628
|
return true;
|
|
6233
|
-
})()
|
|
6629
|
+
}).immediate()
|
|
6234
6630
|
);
|
|
6235
6631
|
}
|
|
6236
6632
|
var AGENT_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
@@ -6248,7 +6644,7 @@ async function sendToAgent(db, name, text, opts) {
|
|
|
6248
6644
|
db,
|
|
6249
6645
|
agent,
|
|
6250
6646
|
opts.fresh ? "fresh" : opts.mode === "steer" ? "steer" : "plain",
|
|
6251
|
-
text
|
|
6647
|
+
Buffer.byteLength(text),
|
|
6252
6648
|
{
|
|
6253
6649
|
transport: sent.transport,
|
|
6254
6650
|
command: sent.command,
|
|
@@ -6612,7 +7008,6 @@ async function spawnAgent(db, opts) {
|
|
|
6612
7008
|
const workspacePathStr = opts.workspace ? await prestageWorkspace(db, opts, cli) : void 0;
|
|
6613
7009
|
const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname6(db.name));
|
|
6614
7010
|
mkdirSync2(dirname6(ctlSocket), { recursive: true, mode: 448 });
|
|
6615
|
-
rmSync4(ctlSocket, { force: true });
|
|
6616
7011
|
const paneEnv = {
|
|
6617
7012
|
MU_MANAGED_AGENT: "1",
|
|
6618
7013
|
MU_AGENT_NAME: opts.name,
|
|
@@ -6622,12 +7017,18 @@ async function spawnAgent(db, opts) {
|
|
|
6622
7017
|
const hasWorkspace = workspacePathStr !== void 0;
|
|
6623
7018
|
let paneId;
|
|
6624
7019
|
let agent;
|
|
7020
|
+
let ownsRow = hasWorkspace;
|
|
6625
7021
|
const wantsCtl = opts.ctl !== false && speaksMuCtl(cli, command) && defaultSpawnCtlMs() !== 0;
|
|
6626
7022
|
let ctl;
|
|
6627
7023
|
try {
|
|
6628
7024
|
const created = await withSpawnLock(session, async () => {
|
|
7025
|
+
if (!hasWorkspace && getAgent(db, opts.name, opts.workstream) !== void 0) {
|
|
7026
|
+
throw new AgentExistsError(opts.name);
|
|
7027
|
+
}
|
|
7028
|
+
rmSync4(ctlSocket, { force: true });
|
|
6629
7029
|
const pid = await createOrReusePane({
|
|
6630
7030
|
session,
|
|
7031
|
+
keepSessionAlive: isScratchWorkstream(opts.workstream),
|
|
6631
7032
|
windowName,
|
|
6632
7033
|
command,
|
|
6633
7034
|
cwd: workspacePathStr ?? opts.cwd,
|
|
@@ -6638,6 +7039,7 @@ async function spawnAgent(db, opts) {
|
|
|
6638
7039
|
await mux.setPaneTitle(pid, opts.name);
|
|
6639
7040
|
await mux.enableMuPaneBordersForPane(pid);
|
|
6640
7041
|
const row = finalizeAgentRow(db, { opts, cli, paneId: pid, hasWorkspace });
|
|
7042
|
+
ownsRow = true;
|
|
6641
7043
|
return { pid, row };
|
|
6642
7044
|
});
|
|
6643
7045
|
paneId = created.pid;
|
|
@@ -6657,7 +7059,7 @@ async function spawnAgent(db, opts) {
|
|
|
6657
7059
|
throw new AgentDiedOnSpawnError(opts.name, pid, lastScrollback, command, when);
|
|
6658
7060
|
};
|
|
6659
7061
|
ctl = await awaitCtlHandshake(ctlSocket, check ? assertPaneAlive : void 0);
|
|
6660
|
-
if (check) await checkSpawnHealth(pid, opts.name, command, when);
|
|
7062
|
+
if (check) await checkSpawnHealth(pid, opts.name, command, when, ctl === "ok");
|
|
6661
7063
|
} else {
|
|
6662
7064
|
await awaitSpawnLiveness(paneId, opts.name, command);
|
|
6663
7065
|
}
|
|
@@ -6665,7 +7067,7 @@ async function spawnAgent(db, opts) {
|
|
|
6665
7067
|
await startAgent({ paneId, name: opts.name, cli, command, commandSource });
|
|
6666
7068
|
}
|
|
6667
7069
|
} catch (err) {
|
|
6668
|
-
await rollbackSpawn(db, opts.name, paneId, hasWorkspace, opts.workstream);
|
|
7070
|
+
await rollbackSpawn(db, opts.name, paneId, hasWorkspace, ownsRow, opts.workstream);
|
|
6669
7071
|
if (hasWorkspace) attachOrphanCleanupHint(err, opts.name, opts.workstream);
|
|
6670
7072
|
throw err;
|
|
6671
7073
|
}
|
|
@@ -6724,7 +7126,7 @@ function finalizeAgentRow(db, args) {
|
|
|
6724
7126
|
if (!row) throw new Error(`spawnAgent: agent vanished after workspace stage: ${opts.name}`);
|
|
6725
7127
|
return row;
|
|
6726
7128
|
}
|
|
6727
|
-
async function rollbackSpawn(db, name, paneId, hasWorkspace, workstream) {
|
|
7129
|
+
async function rollbackSpawn(db, name, paneId, hasWorkspace, ownsRow, workstream) {
|
|
6728
7130
|
if (paneId !== void 0) {
|
|
6729
7131
|
await activeMux().then((mux) => mux.killPane(paneId)).catch(() => {
|
|
6730
7132
|
});
|
|
@@ -6733,7 +7135,7 @@ async function rollbackSpawn(db, name, paneId, hasWorkspace, workstream) {
|
|
|
6733
7135
|
await freeWorkspace(db, name, { workstream }).catch(() => {
|
|
6734
7136
|
});
|
|
6735
7137
|
}
|
|
6736
|
-
deleteAgent(db, name, workstream);
|
|
7138
|
+
if (ownsRow) deleteAgent(db, name, workstream);
|
|
6737
7139
|
}
|
|
6738
7140
|
function attachOrphanCleanupHint(err, agent, workstream) {
|
|
6739
7141
|
if (typeof err !== "object" || err === null) return;
|
|
@@ -6758,42 +7160,24 @@ function defaultSpawnLivenessMs() {
|
|
|
6758
7160
|
if (Number.isNaN(parsed) || parsed < 0) return 1500;
|
|
6759
7161
|
return parsed;
|
|
6760
7162
|
}
|
|
6761
|
-
var
|
|
7163
|
+
var AUTH_ERROR_PATTERNS = [
|
|
6762
7164
|
/No API key found for [\w-]+/i,
|
|
6763
7165
|
/Error: invalid API key/i,
|
|
6764
7166
|
/Authentication failed/i,
|
|
6765
7167
|
/401 Unauthorized/i,
|
|
6766
|
-
/Could not authenticate/i
|
|
6767
|
-
// fb_agent_spawn_no_validation part B: post-spawn detection of the
|
|
6768
|
-
// "binary not found at exec time" failure mode. The pre-flight check
|
|
6769
|
-
// above catches the common typo BEFORE any side effect, but a few
|
|
6770
|
-
// edge cases still slip through and only surface in the pane:
|
|
6771
|
-
// - `--command "..."` skips the pre-flight (operator opt-out).
|
|
6772
|
-
// - PATH inside the spawned shell differs from PATH in mu's
|
|
6773
|
-
// process (login shell rc files, /etc/paths.d, etc.).
|
|
6774
|
-
// - Race: binary on PATH at spawn time, gone 1.5s later.
|
|
6775
|
-
//
|
|
6776
|
-
// Anchored to lines that LOOK like a shell error rather than prose
|
|
6777
|
-
// mentioning the marker (review_substrate_startup_err_patterns_too_broad):
|
|
6778
|
-
// require the marker at end-of-line, optionally followed by a single
|
|
6779
|
-
// `: <name>` token (the zsh form: `zsh: command not found: pi-meta`).
|
|
6780
|
-
// This still trips the canonical cases —
|
|
6781
|
-
// `bash: pi-meta: command not found`
|
|
6782
|
-
// `zsh: command not found: pi-meta`
|
|
6783
|
-
// `sh: pi-meta: No such file or directory`
|
|
6784
|
-
// — but rejects banner prose like
|
|
6785
|
-
// `I noticed earlier you saw 'command not found'`
|
|
6786
|
-
// `type \`mu\` — command not found? install with …`
|
|
6787
|
-
// that previously triggered a full spawn rollback. The pre-flight
|
|
6788
|
-
// PATH check remains the deterministic safety net for typo'd `--cli`.
|
|
6789
|
-
/^(?:.*: )?(?:command not found|No such file or directory)(?::\s*\S+)?$/i
|
|
7168
|
+
/Could not authenticate/i
|
|
6790
7169
|
];
|
|
7170
|
+
var EXEC_FAILURE_PATTERN = /^(?:.*: )?(?:command not found|No such file or directory)(?::\s*\S+)?$/i;
|
|
7171
|
+
function isExecFailureLine(line) {
|
|
7172
|
+
return EXEC_FAILURE_PATTERN.test(line);
|
|
7173
|
+
}
|
|
6791
7174
|
var STARTUP_ERROR_TAIL_LINES = 30;
|
|
6792
|
-
function detectSpawnStartupError(scrollback) {
|
|
7175
|
+
function detectSpawnStartupError(scrollback, cliStarted = false) {
|
|
7176
|
+
const patterns = cliStarted ? AUTH_ERROR_PATTERNS : [...AUTH_ERROR_PATTERNS, EXEC_FAILURE_PATTERN];
|
|
6793
7177
|
const lines = scrollback.split(/\r?\n/);
|
|
6794
7178
|
const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
|
|
6795
7179
|
for (const line of tail) {
|
|
6796
|
-
for (const pattern of
|
|
7180
|
+
for (const pattern of patterns) {
|
|
6797
7181
|
if (pattern.test(line)) return line;
|
|
6798
7182
|
}
|
|
6799
7183
|
}
|
|
@@ -6805,14 +7189,14 @@ async function awaitSpawnLiveness(paneId, agentName, command) {
|
|
|
6805
7189
|
await sleep(ms);
|
|
6806
7190
|
await checkSpawnHealth(paneId, agentName, command);
|
|
6807
7191
|
}
|
|
6808
|
-
async function checkSpawnHealth(paneId, agentName, command, when) {
|
|
7192
|
+
async function checkSpawnHealth(paneId, agentName, command, when, cliStarted = false) {
|
|
6809
7193
|
const mux = await activeMux();
|
|
6810
7194
|
const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
|
|
6811
7195
|
if (!await mux.paneExists(paneId)) {
|
|
6812
7196
|
throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when);
|
|
6813
7197
|
}
|
|
6814
7198
|
if (scrollback !== void 0) {
|
|
6815
|
-
const matchedLine = detectSpawnStartupError(scrollback);
|
|
7199
|
+
const matchedLine = detectSpawnStartupError(scrollback, cliStarted);
|
|
6816
7200
|
if (matchedLine !== void 0) {
|
|
6817
7201
|
throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
|
|
6818
7202
|
}
|
|
@@ -6821,7 +7205,8 @@ async function checkSpawnHealth(paneId, agentName, command, when) {
|
|
|
6821
7205
|
async function createOrReusePane(opts) {
|
|
6822
7206
|
const mux = await activeMux();
|
|
6823
7207
|
const command = mux.startAgentInPane === void 0 ? opts.command : "";
|
|
6824
|
-
if (
|
|
7208
|
+
if (opts.keepSessionAlive) await ensureWorkstreamSession(opts.session);
|
|
7209
|
+
else if (!await mux.sessionExists(opts.session)) {
|
|
6825
7210
|
return mux.newSessionWithPane(opts.session, {
|
|
6826
7211
|
windowName: opts.windowName,
|
|
6827
7212
|
command,
|
|
@@ -6833,12 +7218,17 @@ async function createOrReusePane(opts) {
|
|
|
6833
7218
|
const matching = windows.find((w) => w.name === opts.windowName);
|
|
6834
7219
|
if (matching) {
|
|
6835
7220
|
return mux.splitWindow({
|
|
6836
|
-
// tmux splits a WINDOW
|
|
7221
|
+
// tmux splits a WINDOW: target it as `=<session>:<window_id>`.
|
|
7222
|
+
// A bare `<session>:<name>` falls back to tmux's session prefix
|
|
7223
|
+
// match, so if `mu-auth` vanishes between listWindows and here,
|
|
7224
|
+
// `mu-auth:Backend` splits `mu-auth-refactor`'s Backend window.
|
|
7225
|
+
// `=` makes the session exact and the stable window id (from
|
|
7226
|
+
// the listing above) must live in that session, so a vanished
|
|
7227
|
+
// session or window fails instead of landing elsewhere.
|
|
6837
7228
|
// herdr splits a PANE and has no window-target form at all. Ask
|
|
6838
7229
|
// the backend for a concrete pane in that window when it needs
|
|
6839
|
-
// one — cheaper than a `target` union type for two backends
|
|
6840
|
-
|
|
6841
|
-
target: mux.startAgentInPane === void 0 ? `${opts.session}:${opts.windowName}` : await firstPaneInWindow(mux, opts.session, matching.id),
|
|
7230
|
+
// one — cheaper than a `target` union type for two backends.
|
|
7231
|
+
target: mux.startAgentInPane === void 0 ? `=${opts.session}:${matching.id}` : await firstPaneInWindow(mux, opts.session, matching.id),
|
|
6842
7232
|
command,
|
|
6843
7233
|
cwd: opts.cwd,
|
|
6844
7234
|
env: opts.env
|
|
@@ -6968,6 +7358,32 @@ var AgentNotInWorkstreamError = class extends Error {
|
|
|
6968
7358
|
];
|
|
6969
7359
|
}
|
|
6970
7360
|
};
|
|
7361
|
+
var PaneNotInSessionError = class extends Error {
|
|
7362
|
+
constructor(paneId, workstream, expectedSession) {
|
|
7363
|
+
super(
|
|
7364
|
+
`pane ${paneId} is not in session ${expectedSession}; adopt only takes panes from workstream ${workstream}'s own session`
|
|
7365
|
+
);
|
|
7366
|
+
this.paneId = paneId;
|
|
7367
|
+
this.workstream = workstream;
|
|
7368
|
+
this.expectedSession = expectedSession;
|
|
7369
|
+
}
|
|
7370
|
+
paneId;
|
|
7371
|
+
workstream;
|
|
7372
|
+
expectedSession;
|
|
7373
|
+
name = "PaneNotInSessionError";
|
|
7374
|
+
errorNextSteps() {
|
|
7375
|
+
return [
|
|
7376
|
+
{
|
|
7377
|
+
intent: "List the workstream's agents and orphan panes to pick one",
|
|
7378
|
+
command: `mu agent list -w ${this.workstream}`
|
|
7379
|
+
},
|
|
7380
|
+
{
|
|
7381
|
+
intent: "Or adopt it into the workstream whose session holds the pane",
|
|
7382
|
+
command: `mu agent adopt ${this.paneId} -w <workstream>`
|
|
7383
|
+
}
|
|
7384
|
+
];
|
|
7385
|
+
}
|
|
7386
|
+
};
|
|
6971
7387
|
var AgentDiedOnSpawnError = class extends Error {
|
|
6972
7388
|
constructor(agentName, paneId, scrollback, command, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
|
|
6973
7389
|
const tail = scrollback?.trim();
|
|
@@ -7058,11 +7474,26 @@ ${scrollback.trim()}
|
|
|
7058
7474
|
scrollback;
|
|
7059
7475
|
name = "AgentSpawnStartupError";
|
|
7060
7476
|
errorNextSteps() {
|
|
7477
|
+
const inspect = {
|
|
7478
|
+
intent: "Inspect the parked pane's scrollback for the full error",
|
|
7479
|
+
command: `mu agent read ${this.agentName} -n 100`
|
|
7480
|
+
};
|
|
7481
|
+
const disable = {
|
|
7482
|
+
intent: "Disable the startup-error scan if you actually wanted that prompt (CI / scripted recovery)",
|
|
7483
|
+
command: "export MU_SPAWN_LIVENESS_MS=0"
|
|
7484
|
+
};
|
|
7485
|
+
if (isExecFailureLine(this.matchedLine.trim())) {
|
|
7486
|
+
return [
|
|
7487
|
+
inspect,
|
|
7488
|
+
{
|
|
7489
|
+
intent: "Check the spawned command resolves in the pane's shell (PATH, typo)",
|
|
7490
|
+
command: `mu agent spawn ${this.agentName} --command "<full path to the CLI>"`
|
|
7491
|
+
},
|
|
7492
|
+
disable
|
|
7493
|
+
];
|
|
7494
|
+
}
|
|
7061
7495
|
return [
|
|
7062
|
-
|
|
7063
|
-
intent: "Inspect the parked pane's scrollback for the full error",
|
|
7064
|
-
command: `mu agent read ${this.agentName} -n 100`
|
|
7065
|
-
},
|
|
7496
|
+
inspect,
|
|
7066
7497
|
{
|
|
7067
7498
|
// Most common today: the operator picked a model whose
|
|
7068
7499
|
// provider has no credentials in this env. Default Anthropic
|
|
@@ -7074,10 +7505,7 @@ ${scrollback.trim()}
|
|
|
7074
7505
|
intent: "Or set the missing API key env var for the provider you wanted, then re-spawn",
|
|
7075
7506
|
command: "export ANTHROPIC_API_KEY=... # or AWS_BEARER_TOKEN_BEDROCK, OPENAI_API_KEY, ..."
|
|
7076
7507
|
},
|
|
7077
|
-
|
|
7078
|
-
intent: "Disable the startup-error scan if you actually wanted that prompt (CI / scripted recovery)",
|
|
7079
|
-
command: "export MU_SPAWN_LIVENESS_MS=0"
|
|
7080
|
-
}
|
|
7508
|
+
disable
|
|
7081
7509
|
];
|
|
7082
7510
|
}
|
|
7083
7511
|
};
|
|
@@ -7984,13 +8412,17 @@ function applyDel(db, op) {
|
|
|
7984
8412
|
const { taskKey: taskKey2 } = parseNoteKey(op.key);
|
|
7985
8413
|
const taskId = taskRowId(db, taskKey2);
|
|
7986
8414
|
if (taskId === null) return { changed: false, appliedFields: [], skipped: "absent" };
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
8415
|
+
let entries = decodePayload(op.payload);
|
|
8416
|
+
if (!entries.some(([f, v]) => f === "content" && v !== null)) {
|
|
8417
|
+
const src = db.prepare(
|
|
8418
|
+
`SELECT payload FROM ops
|
|
8419
|
+
WHERE entity = 'note' AND key = @key AND op = 'put'
|
|
8420
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
8421
|
+
ORDER BY hlc DESC LIMIT 1`
|
|
8422
|
+
).get({ key: op.key });
|
|
8423
|
+
if (!src) return { changed: false, appliedFields: [], skipped: "absent" };
|
|
8424
|
+
entries = decodePayload(src.payload);
|
|
8425
|
+
}
|
|
7994
8426
|
const content = entries.find(([f]) => f === "content")?.[1] ?? "";
|
|
7995
8427
|
const author = entries.find(([f]) => f === "author")?.[1] ?? null;
|
|
7996
8428
|
const createdAt = entries.find(([f]) => f === "created_at")?.[1] ?? null;
|
|
@@ -8038,7 +8470,7 @@ function applyOp(db, op) {
|
|
|
8038
8470
|
case "workstream":
|
|
8039
8471
|
return applyWorkstreamPut(db, op);
|
|
8040
8472
|
case "task":
|
|
8041
|
-
return db.transaction(() => applyTaskPut(db, op))();
|
|
8473
|
+
return db.transaction(() => applyTaskPut(db, op)).immediate();
|
|
8042
8474
|
case "note":
|
|
8043
8475
|
return applyNotePut(db, op);
|
|
8044
8476
|
case "edge":
|
|
@@ -8145,7 +8577,7 @@ function installOpKeyFunctions(db) {
|
|
|
8145
8577
|
function applyOps(db, ops) {
|
|
8146
8578
|
const ordered = [...ops].sort((a, b) => compareHlc(a.hlc, b.hlc));
|
|
8147
8579
|
const run2 = db.transaction(() => ordered.map((op) => applyOp(db, op)));
|
|
8148
|
-
return run2();
|
|
8580
|
+
return run2.immediate();
|
|
8149
8581
|
}
|
|
8150
8582
|
|
|
8151
8583
|
// src/dag.ts
|
|
@@ -8247,12 +8679,12 @@ import { delimiter as delimiter2, dirname as dirname10, join as join11, parse }
|
|
|
8247
8679
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8248
8680
|
|
|
8249
8681
|
// src/drift.ts
|
|
8250
|
-
import { mkdtempSync, rmSync as
|
|
8682
|
+
import { mkdtempSync, rmSync as rmSync6 } from "fs";
|
|
8251
8683
|
import { tmpdir } from "os";
|
|
8252
8684
|
import { join as join9 } from "path";
|
|
8253
8685
|
|
|
8254
8686
|
// src/rebuild.ts
|
|
8255
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync3, statSync } from "fs";
|
|
8687
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync } from "fs";
|
|
8256
8688
|
import { dirname as dirname8 } from "path";
|
|
8257
8689
|
var RebuildTargetExistsError = class extends Error {
|
|
8258
8690
|
constructor(path) {
|
|
@@ -8320,7 +8752,7 @@ function rebuildInto(source, opts) {
|
|
|
8320
8752
|
const identity = source.prepare(
|
|
8321
8753
|
"SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
|
|
8322
8754
|
).get();
|
|
8323
|
-
if (!identity) throw new Error("source DB has no machine_identity row; not a
|
|
8755
|
+
if (!identity) throw new Error("source DB has no machine_identity row; not a mu DB");
|
|
8324
8756
|
const opRows = source.prepare(
|
|
8325
8757
|
`SELECT hlc, machine_id, group_id, actor, intent, entity, key, op, payload, created_at
|
|
8326
8758
|
FROM ops`
|
|
@@ -8332,6 +8764,9 @@ function rebuildInto(source, opts) {
|
|
|
8332
8764
|
const row = source.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
8333
8765
|
if (row.n > 0) machineLocalLost.push({ table, rows: row.n });
|
|
8334
8766
|
}
|
|
8767
|
+
if (opts.force === true) {
|
|
8768
|
+
for (const suffix of ["", "-wal", "-shm"]) rmSync5(`${targetPath}${suffix}`, { force: true });
|
|
8769
|
+
}
|
|
8335
8770
|
const target = openDb({ path: targetPath });
|
|
8336
8771
|
try {
|
|
8337
8772
|
target.prepare(
|
|
@@ -8389,7 +8824,7 @@ function rebuildInto(source, opts) {
|
|
|
8389
8824
|
}
|
|
8390
8825
|
});
|
|
8391
8826
|
});
|
|
8392
|
-
replay();
|
|
8827
|
+
replay.immediate();
|
|
8393
8828
|
const rebuiltRows = {};
|
|
8394
8829
|
for (const table of ["workstreams", "tasks", "task_edges", "task_notes", "ops"]) {
|
|
8395
8830
|
const row = target.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
@@ -8602,7 +9037,7 @@ function checkDrift(db) {
|
|
|
8602
9037
|
rebuilt.close();
|
|
8603
9038
|
}
|
|
8604
9039
|
} finally {
|
|
8605
|
-
|
|
9040
|
+
rmSync6(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 });
|
|
8606
9041
|
}
|
|
8607
9042
|
}
|
|
8608
9043
|
function formatDriftRecord(record) {
|
|
@@ -8659,6 +9094,16 @@ var DriftDetectedError = class extends Error {
|
|
|
8659
9094
|
// src/fleet-hazards.ts
|
|
8660
9095
|
import { existsSync as existsSync11, statfsSync } from "fs";
|
|
8661
9096
|
import { resolve as resolve4, sep } from "path";
|
|
9097
|
+
var HazardFailError = class extends Error {
|
|
9098
|
+
constructor(failedRows) {
|
|
9099
|
+
super(
|
|
9100
|
+
`doctor: ${failedRows.length} row${failedRows.length === 1 ? "" : "s"} failed (${failedRows.join(", ")}); see the remediation above`
|
|
9101
|
+
);
|
|
9102
|
+
this.failedRows = failedRows;
|
|
9103
|
+
}
|
|
9104
|
+
failedRows;
|
|
9105
|
+
name = "HazardFailError";
|
|
9106
|
+
};
|
|
8662
9107
|
function isPathInside(child, parent) {
|
|
8663
9108
|
const c = resolve4(child);
|
|
8664
9109
|
const p = resolve4(parent);
|
|
@@ -8809,10 +9254,10 @@ function checkCaseCollisions(db) {
|
|
|
8809
9254
|
"",
|
|
8810
9255
|
"Fix: rename one side to a distinct name before adding a Mac to the fleet.",
|
|
8811
9256
|
"There is no in-place rename verb; back the DB up, re-init under the new",
|
|
8812
|
-
"name, and
|
|
9257
|
+
"name, and tear down the old one:",
|
|
8813
9258
|
" mu db backup <file>",
|
|
8814
9259
|
" mu workstream init <new-name>",
|
|
8815
|
-
" mu workstream teardown <old-name>"
|
|
9260
|
+
" mu workstream teardown <old-name> --yes"
|
|
8816
9261
|
]
|
|
8817
9262
|
};
|
|
8818
9263
|
}
|
|
@@ -8833,7 +9278,7 @@ import {
|
|
|
8833
9278
|
readFileSync,
|
|
8834
9279
|
readlinkSync,
|
|
8835
9280
|
realpathSync,
|
|
8836
|
-
rmSync as
|
|
9281
|
+
rmSync as rmSync7,
|
|
8837
9282
|
symlinkSync,
|
|
8838
9283
|
writeFileSync
|
|
8839
9284
|
} from "fs";
|
|
@@ -8899,6 +9344,13 @@ function linkPi(opts) {
|
|
|
8899
9344
|
if (!existsSync12(entry)) {
|
|
8900
9345
|
throw new Error(`mu pi extension not built: ${entry} is missing (run npm run build)`);
|
|
8901
9346
|
}
|
|
9347
|
+
if (lstatOrUndefined(path)?.isSymbolicLink() === true) {
|
|
9348
|
+
const current = resolve5(dirname9(path), readlinkSync(path));
|
|
9349
|
+
if (existsSync12(current) && opts?.force !== true) {
|
|
9350
|
+
throw new LinkConflictError(path, "foreign-symlink", current);
|
|
9351
|
+
}
|
|
9352
|
+
rmSync7(path);
|
|
9353
|
+
}
|
|
8902
9354
|
let replacedCopy = false;
|
|
8903
9355
|
try {
|
|
8904
9356
|
replacedCopy = !readFileSync(path, "utf8").includes(MU_SHIM_MARKER);
|
|
@@ -8921,7 +9373,7 @@ function linkSkill(opts) {
|
|
|
8921
9373
|
throw new LinkConflictError(path, "foreign-symlink", current);
|
|
8922
9374
|
}
|
|
8923
9375
|
previous = current;
|
|
8924
|
-
|
|
9376
|
+
rmSync7(path);
|
|
8925
9377
|
}
|
|
8926
9378
|
mkdirSync4(dirname9(path), { recursive: true });
|
|
8927
9379
|
symlinkSync(target, path, "dir");
|
|
@@ -9122,7 +9574,7 @@ function loadDoctorSummary(db, snapshot2, agentStateSource = "murmur") {
|
|
|
9122
9574
|
detail: `${cheapDrift.totalUnexplained} row(s) with no ops; run \`mu doctor --deep\``
|
|
9123
9575
|
}
|
|
9124
9576
|
);
|
|
9125
|
-
return { checks, problemCount: countProblems(checks) };
|
|
9577
|
+
return { checks, problemCount: countProblems(checks), agentStateSource };
|
|
9126
9578
|
}
|
|
9127
9579
|
function resolveExecutable(name) {
|
|
9128
9580
|
for (const dir of (process.env.PATH ?? "").split(delimiter2).filter(Boolean)) {
|
|
@@ -9243,8 +9695,8 @@ function countProblems(checks) {
|
|
|
9243
9695
|
for (const c of checks) if (c.status !== "ok") n++;
|
|
9244
9696
|
return n;
|
|
9245
9697
|
}
|
|
9246
|
-
function loadDoctorChecks(db, snapshot2) {
|
|
9247
|
-
return loadDoctorSummary(db, snapshot2).checks;
|
|
9698
|
+
function loadDoctorChecks(db, snapshot2, agentStateSource = "murmur") {
|
|
9699
|
+
return loadDoctorSummary(db, snapshot2, agentStateSource).checks;
|
|
9248
9700
|
}
|
|
9249
9701
|
function yankCommandForCheck(check) {
|
|
9250
9702
|
switch (check.name) {
|
|
@@ -9319,10 +9771,13 @@ function remediationParagraph(check) {
|
|
|
9319
9771
|
];
|
|
9320
9772
|
case "workspaces":
|
|
9321
9773
|
return [
|
|
9322
|
-
"An 'orphan workspace dir' is a per-agent VCS workspace under",
|
|
9323
|
-
"the workstream
|
|
9324
|
-
"
|
|
9325
|
-
"`mu workspace
|
|
9774
|
+
"An 'orphan workspace dir' is a per-agent VCS workspace dir under",
|
|
9775
|
+
"the workstream with no vcs_workspaces row, so `mu workspace free`",
|
|
9776
|
+
"cannot remove it. mu never deletes it for you: it may hold the only",
|
|
9777
|
+
"copy of uncommitted work. Run `mu workspace orphans -w <ws>` to list",
|
|
9778
|
+
"them with a cleanup command, inspect each, then remove it by hand",
|
|
9779
|
+
"(`git worktree remove --force <path>` from the project root for a",
|
|
9780
|
+
"git checkout, else `rm -rf <path>`)."
|
|
9326
9781
|
];
|
|
9327
9782
|
case "drift":
|
|
9328
9783
|
return [
|
|
@@ -9363,18 +9818,20 @@ function remediationParagraph(check) {
|
|
|
9363
9818
|
];
|
|
9364
9819
|
case "schema":
|
|
9365
9820
|
return [
|
|
9366
|
-
"Missing tables
|
|
9367
|
-
"
|
|
9368
|
-
"
|
|
9369
|
-
"
|
|
9821
|
+
"Missing tables mean something dropped them after mu created",
|
|
9822
|
+
"them: every mu process runs CREATE TABLE IF NOT EXISTS for the",
|
|
9823
|
+
"whole schema at open, so re-running any mu command restores an",
|
|
9824
|
+
"empty table. If a table that held data is gone, back up the DB",
|
|
9825
|
+
"and rebuild from the ops log: `mu rebuild <new-file>`."
|
|
9370
9826
|
];
|
|
9371
9827
|
case "schema_version":
|
|
9372
9828
|
return [
|
|
9373
|
-
"
|
|
9374
|
-
"
|
|
9375
|
-
"
|
|
9376
|
-
"
|
|
9377
|
-
"
|
|
9829
|
+
"mu opens only a DB at exactly its own schema version: older",
|
|
9830
|
+
"DBs are refused with SchemaTooOldError and newer ones with",
|
|
9831
|
+
"SchemaTooNewError (exit 4), and there is no in-place migration.",
|
|
9832
|
+
"A mismatch seen here means the row changed after open (an",
|
|
9833
|
+
"external SQLite client). If `<` the expected version, migrate a",
|
|
9834
|
+
"copy with scripts/migrate.ts; if `>`, use the newer mu binary."
|
|
9378
9835
|
];
|
|
9379
9836
|
case "journal_mode":
|
|
9380
9837
|
return [
|
|
@@ -9445,6 +9902,7 @@ var VERBS = {
|
|
|
9445
9902
|
"task.open": "task open",
|
|
9446
9903
|
"task.park": "task park",
|
|
9447
9904
|
"task.unpark": "task unpark",
|
|
9905
|
+
"task.accept": "task accept",
|
|
9448
9906
|
"task.reject": "task reject",
|
|
9449
9907
|
"task.defer": "task defer",
|
|
9450
9908
|
"task.claim": "task claim",
|
|
@@ -9456,6 +9914,7 @@ var VERBS = {
|
|
|
9456
9914
|
"workstream.init": "workstream init",
|
|
9457
9915
|
"workstream.teardown": "workstream teardown",
|
|
9458
9916
|
"workstream.destroy": "workstream teardown",
|
|
9917
|
+
"sql.write": "sql write",
|
|
9459
9918
|
"agent.spawn": "agent spawn",
|
|
9460
9919
|
"agent.close": "agent close",
|
|
9461
9920
|
"agent.adopt": "agent adopt",
|
|
@@ -9539,7 +9998,8 @@ function renderKnown(row, intent) {
|
|
|
9539
9998
|
};
|
|
9540
9999
|
}
|
|
9541
10000
|
case "task.park":
|
|
9542
|
-
case "task.unpark":
|
|
10001
|
+
case "task.unpark":
|
|
10002
|
+
case "task.accept": {
|
|
9543
10003
|
const detail = str(bag, "substate") === void 0 ? "" : `\u2192 ${pairText(bag, "OPEN")}`;
|
|
9544
10004
|
return { verb: VERBS[intent], subject, detail };
|
|
9545
10005
|
}
|
|
@@ -9575,6 +10035,19 @@ function renderKnown(row, intent) {
|
|
|
9575
10035
|
subject,
|
|
9576
10036
|
detail: key.local === void 0 ? "" : "(cascaded)"
|
|
9577
10037
|
};
|
|
10038
|
+
case "sql.write": {
|
|
10039
|
+
if (deleted) return { verb: VERBS[intent], subject, detail: `${row.kind} deleted` };
|
|
10040
|
+
const edge = key.to === void 0 ? "" : ` -> ${key.to}`;
|
|
10041
|
+
const changed = changedFields(bag).map((f) => {
|
|
10042
|
+
const v = bag[f];
|
|
10043
|
+
return typeof v === "object" && v !== null ? f : `${f}=${oneLine(String(v), 40)}`;
|
|
10044
|
+
}).join(" ");
|
|
10045
|
+
return {
|
|
10046
|
+
verb: VERBS[intent],
|
|
10047
|
+
subject,
|
|
10048
|
+
detail: `${row.kind}${edge} ${changed === "" ? "(touched)" : changed}`
|
|
10049
|
+
};
|
|
10050
|
+
}
|
|
9578
10051
|
// Local intents: no trigger can see these, so `emitEvent` wrote the
|
|
9579
10052
|
// payload as prose. It is already human-readable — show it as the
|
|
9580
10053
|
// detail rather than inventing a second phrasing. The leading verb
|
|
@@ -9663,7 +10136,7 @@ function refuseUserSyncDirDuringTests(dir) {
|
|
|
9663
10136
|
}
|
|
9664
10137
|
function localMachineId(db) {
|
|
9665
10138
|
const row = db.prepare("SELECT machine_id FROM machine_identity WHERE id = 1").get();
|
|
9666
|
-
if (row === void 0) throw new Error("machine_identity row missing; not a
|
|
10139
|
+
if (row === void 0) throw new Error("machine_identity row missing; not a mu DB");
|
|
9667
10140
|
return row.machine_id;
|
|
9668
10141
|
}
|
|
9669
10142
|
function segmentPath(dir, machineId2) {
|
|
@@ -9875,14 +10348,17 @@ function segmentLineCount(path) {
|
|
|
9875
10348
|
}
|
|
9876
10349
|
function readSegmentTail(path) {
|
|
9877
10350
|
if (!existsSync14(path)) return { count: 0, lastHlc: null, goodLines: [], defect: null };
|
|
9878
|
-
const
|
|
9879
|
-
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
10351
|
+
const lines = segmentLines(readFileSync3(path, "utf8"));
|
|
9880
10352
|
let lastHlc = null;
|
|
9881
10353
|
let count = 0;
|
|
9882
10354
|
let defect = null;
|
|
9883
10355
|
for (let index = 0; index < lines.length; index++) {
|
|
9884
10356
|
const line = lines[index];
|
|
9885
10357
|
if (line === void 0) break;
|
|
10358
|
+
if (line.trim() === "") {
|
|
10359
|
+
defect = { kind: "malformed-shape", line: index + 1, detail: "blank line" };
|
|
10360
|
+
break;
|
|
10361
|
+
}
|
|
9886
10362
|
const decoded = decodeLine(line);
|
|
9887
10363
|
if (!decoded.ok) {
|
|
9888
10364
|
defect = { kind: decoded.kind, line: index + 1, detail: decoded.detail };
|
|
@@ -9893,6 +10369,11 @@ function readSegmentTail(path) {
|
|
|
9893
10369
|
}
|
|
9894
10370
|
return { count, lastHlc, goodLines: lines.slice(0, count), defect };
|
|
9895
10371
|
}
|
|
10372
|
+
function segmentLines(raw) {
|
|
10373
|
+
const lines = raw.split("\n");
|
|
10374
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
10375
|
+
return lines;
|
|
10376
|
+
}
|
|
9896
10377
|
function writeManifest(path, machine, count, lastHlc) {
|
|
9897
10378
|
const bytes = existsSync14(path) ? readFileSync3(path) : Buffer.alloc(0);
|
|
9898
10379
|
const manifest = {
|
|
@@ -9946,28 +10427,32 @@ function discoverPeers(dir, selfMachineId) {
|
|
|
9946
10427
|
} catch {
|
|
9947
10428
|
continue;
|
|
9948
10429
|
}
|
|
9949
|
-
peers.push({ machineId: machineId2, path, conflictCopy });
|
|
10430
|
+
peers.push({ machineId: machineId2, watermarkKey: conflictCopy ? stem : machineId2, path, conflictCopy });
|
|
9950
10431
|
}
|
|
9951
10432
|
return peers.sort((a, b) => a.path.localeCompare(b.path));
|
|
9952
10433
|
}
|
|
9953
|
-
function
|
|
9954
|
-
|
|
10434
|
+
function peerWatermarkKey(peer) {
|
|
10435
|
+
return peer.watermarkKey ?? peer.machineId;
|
|
10436
|
+
}
|
|
10437
|
+
function getWatermark(db, watermarkKey) {
|
|
10438
|
+
const row = db.prepare("SELECT last_applied_seq AS n FROM sync_peers WHERE machine_id = ?").get(watermarkKey);
|
|
9955
10439
|
return row?.n ?? 0;
|
|
9956
10440
|
}
|
|
9957
|
-
function setWatermark(db,
|
|
10441
|
+
function setWatermark(db, watermarkKey, value) {
|
|
9958
10442
|
db.prepare(
|
|
9959
10443
|
`INSERT INTO sync_peers (machine_id, last_applied_seq, last_seen_at)
|
|
9960
10444
|
VALUES (@machineId, @value, @seenAt)
|
|
9961
10445
|
ON CONFLICT (machine_id) DO UPDATE
|
|
9962
10446
|
SET last_applied_seq = @value, last_seen_at = @seenAt`
|
|
9963
|
-
).run({ machineId:
|
|
10447
|
+
).run({ machineId: watermarkKey, value, seenAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9964
10448
|
}
|
|
9965
|
-
function resetWatermark(db,
|
|
9966
|
-
setWatermark(db,
|
|
10449
|
+
function resetWatermark(db, watermarkKey) {
|
|
10450
|
+
setWatermark(db, watermarkKey, 0);
|
|
9967
10451
|
}
|
|
9968
10452
|
function ingestSegment(db, peer) {
|
|
9969
10453
|
const defects = [];
|
|
9970
|
-
const
|
|
10454
|
+
const watermarkKey = peerWatermarkKey(peer);
|
|
10455
|
+
const start = getWatermark(db, watermarkKey);
|
|
9971
10456
|
if (!existsSync14(peer.path)) {
|
|
9972
10457
|
return {
|
|
9973
10458
|
machineId: peer.machineId,
|
|
@@ -9984,9 +10469,7 @@ function ingestSegment(db, peer) {
|
|
|
9984
10469
|
if (!verified.ok) {
|
|
9985
10470
|
defects.push({ kind: "manifest-mismatch", line: 0, detail: verified.reason });
|
|
9986
10471
|
}
|
|
9987
|
-
const
|
|
9988
|
-
const lines = raw.split("\n");
|
|
9989
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
10472
|
+
const lines = segmentLines(readFileSync3(peer.path, "utf8"));
|
|
9990
10473
|
let applied = 0;
|
|
9991
10474
|
let changed = 0;
|
|
9992
10475
|
let watermark = start;
|
|
@@ -10001,14 +10484,14 @@ function ingestSegment(db, peer) {
|
|
|
10001
10484
|
}
|
|
10002
10485
|
const run2 = db.transaction(() => {
|
|
10003
10486
|
for (let index = start; index < lines.length; index++) {
|
|
10004
|
-
const
|
|
10487
|
+
const raw = lines[index];
|
|
10005
10488
|
const lineNo = index + 1;
|
|
10006
|
-
if (
|
|
10489
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
10007
10490
|
defects.push({ kind: "malformed-shape", line: lineNo, detail: "blank line" });
|
|
10008
10491
|
truncatedAt = lineNo;
|
|
10009
10492
|
break;
|
|
10010
10493
|
}
|
|
10011
|
-
const decoded = decodeLine(
|
|
10494
|
+
const decoded = decodeLine(raw);
|
|
10012
10495
|
if (!decoded.ok) {
|
|
10013
10496
|
defects.push({ kind: decoded.kind, line: lineNo, detail: decoded.detail });
|
|
10014
10497
|
truncatedAt = lineNo;
|
|
@@ -10043,6 +10526,11 @@ function ingestSegment(db, peer) {
|
|
|
10043
10526
|
truncatedAt = lineNo;
|
|
10044
10527
|
break;
|
|
10045
10528
|
}
|
|
10529
|
+
if (isLegacyLogOnlyIntent(decoded.line.intent)) {
|
|
10530
|
+
previousHlc = op.hlc;
|
|
10531
|
+
watermark = lineNo;
|
|
10532
|
+
continue;
|
|
10533
|
+
}
|
|
10046
10534
|
try {
|
|
10047
10535
|
const result = applyIncomingOp(db, op);
|
|
10048
10536
|
if (result.changed) changed += 1;
|
|
@@ -10063,13 +10551,13 @@ function ingestSegment(db, peer) {
|
|
|
10063
10551
|
previousHlc = op.hlc;
|
|
10064
10552
|
watermark = lineNo;
|
|
10065
10553
|
}
|
|
10066
|
-
setWatermark(db,
|
|
10554
|
+
setWatermark(db, watermarkKey, watermark);
|
|
10067
10555
|
});
|
|
10068
|
-
run2();
|
|
10556
|
+
run2.immediate();
|
|
10069
10557
|
return {
|
|
10070
10558
|
machineId: peer.machineId,
|
|
10071
10559
|
path: peer.path,
|
|
10072
|
-
read:
|
|
10560
|
+
read: watermark - start,
|
|
10073
10561
|
applied,
|
|
10074
10562
|
changed,
|
|
10075
10563
|
watermark,
|
|
@@ -10282,7 +10770,7 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
|
|
|
10282
10770
|
}
|
|
10283
10771
|
async function loadWorkstreamSnapshotSlow(db, workstream, opts = {}, baseSnapshot) {
|
|
10284
10772
|
const view = await listLiveAgents(db, { workstream });
|
|
10285
|
-
let workspaces = listWorkspaces2(db, workstream);
|
|
10773
|
+
let workspaces = await decorateWithStaleness(listWorkspaces2(db, workstream));
|
|
10286
10774
|
if (opts.withDirty === true) workspaces = await decorateWithDirty(workspaces);
|
|
10287
10775
|
const commits = await loadRecentCommits(opts.withRecentCommits);
|
|
10288
10776
|
const slow = {
|
|
@@ -10315,12 +10803,8 @@ function mergeSnapshotFastSlow(fast, slow) {
|
|
|
10315
10803
|
}
|
|
10316
10804
|
async function loadWorkstreamSnapshot(db, workstream, opts = {}) {
|
|
10317
10805
|
const fast = await loadWorkstreamSnapshotFast(db, workstream, opts);
|
|
10318
|
-
const
|
|
10319
|
-
|
|
10320
|
-
workspaces: await decorateWithStaleness(fast.workspaces)
|
|
10321
|
-
};
|
|
10322
|
-
const slow = await loadWorkstreamSnapshotSlow(db, workstream, opts, fastWithStaleness);
|
|
10323
|
-
return mergeSnapshotFastSlow(fastWithStaleness, slow);
|
|
10806
|
+
const slow = await loadWorkstreamSnapshotSlow(db, workstream, opts, fast);
|
|
10807
|
+
return mergeSnapshotFastSlow(fast, slow);
|
|
10324
10808
|
}
|
|
10325
10809
|
function emptyLiveAgentsView() {
|
|
10326
10810
|
return {
|
|
@@ -10449,13 +10933,15 @@ function peerStatuses(db, dir) {
|
|
|
10449
10933
|
lastSeenMs = null;
|
|
10450
10934
|
}
|
|
10451
10935
|
const total = segmentLineCount(peer.path);
|
|
10452
|
-
const
|
|
10936
|
+
const watermarkKey = peerWatermarkKey(peer);
|
|
10937
|
+
const watermark = getWatermark(db, watermarkKey);
|
|
10453
10938
|
const ageMs = lastSeenMs === null ? null : Math.max(0, now - lastSeenMs);
|
|
10454
10939
|
return {
|
|
10455
10940
|
machineId: peer.machineId,
|
|
10456
10941
|
short: peer.machineId.slice(0, PEER_SHORT_LEN),
|
|
10457
10942
|
path: peer.path,
|
|
10458
10943
|
conflictCopy: peer.conflictCopy,
|
|
10944
|
+
watermarkKey,
|
|
10459
10945
|
watermark,
|
|
10460
10946
|
total,
|
|
10461
10947
|
behind: Math.max(0, total - watermark),
|
|
@@ -10467,27 +10953,31 @@ function peerStatuses(db, dir) {
|
|
|
10467
10953
|
return rows.sort((a, b) => (b.lastSeenMs ?? 0) - (a.lastSeenMs ?? 0));
|
|
10468
10954
|
}
|
|
10469
10955
|
function resolvePeerRef(peers, ref) {
|
|
10470
|
-
const exact = peers.find((p) => p.machineId === ref);
|
|
10471
|
-
if (exact !== void 0) return exact;
|
|
10472
10956
|
const matches = peers.filter((p) => p.machineId.startsWith(ref));
|
|
10473
|
-
const
|
|
10957
|
+
const exact = matches.filter((p) => p.machineId === ref);
|
|
10958
|
+
const candidates = exact.length > 0 ? exact : matches;
|
|
10959
|
+
const machines = [...new Set(candidates.map((p) => p.machineId))];
|
|
10960
|
+
const first = candidates.find((p) => !p.conflictCopy) ?? candidates[0];
|
|
10474
10961
|
if (first === void 0) {
|
|
10475
10962
|
throw new SyncPeerNotFoundError(
|
|
10476
10963
|
ref,
|
|
10477
10964
|
peers.map((p) => p.short)
|
|
10478
10965
|
);
|
|
10479
10966
|
}
|
|
10480
|
-
if (
|
|
10967
|
+
if (machines.length > 1) {
|
|
10481
10968
|
throw new SyncPeerRefAmbiguousError(
|
|
10482
10969
|
ref,
|
|
10483
|
-
|
|
10970
|
+
machines.map((m) => m.slice(0, PEER_SHORT_LEN))
|
|
10484
10971
|
);
|
|
10485
10972
|
}
|
|
10486
10973
|
return first;
|
|
10487
10974
|
}
|
|
10488
10975
|
function repairPeer(db, ref, dir) {
|
|
10489
|
-
const
|
|
10490
|
-
|
|
10976
|
+
const peers = peerStatuses(db, dir);
|
|
10977
|
+
const peer = resolvePeerRef(peers, ref);
|
|
10978
|
+
for (const p of peers) {
|
|
10979
|
+
if (p.machineId === peer.machineId) resetWatermark(db, peerWatermarkKey(p));
|
|
10980
|
+
}
|
|
10491
10981
|
return peer;
|
|
10492
10982
|
}
|
|
10493
10983
|
function syncEnabled() {
|
|
@@ -10503,7 +10993,15 @@ function describeDefects(peerShort, defects) {
|
|
|
10503
10993
|
const detail = first === void 0 ? "unknown defect" : `${first.kind} at line ${first.line}`;
|
|
10504
10994
|
const more = defects.length > 1 ? ` (+${defects.length - 1} more)` : "";
|
|
10505
10995
|
const skipped = /* @__PURE__ */ new Set(["entity-not-synced", "duplicate-op"]);
|
|
10506
|
-
const
|
|
10996
|
+
const fileWide = /* @__PURE__ */ new Set(["manifest-mismatch"]);
|
|
10997
|
+
let hint;
|
|
10998
|
+
if (defects.every((d) => skipped.has(d.kind))) {
|
|
10999
|
+
hint = " \u2014 skipped; the rest of the segment applied";
|
|
11000
|
+
} else if (defects.every((d) => skipped.has(d.kind) || fileWide.has(d.kind))) {
|
|
11001
|
+
hint = " \u2014 the file is shorter than its manifest; copy it from the peer again";
|
|
11002
|
+
} else {
|
|
11003
|
+
hint = ` \u2014 re-read with \`mu sync --repair ${peerShort}\``;
|
|
11004
|
+
}
|
|
10507
11005
|
return `peer ${peerShort}: ${detail}${more}${hint}`;
|
|
10508
11006
|
}
|
|
10509
11007
|
async function ambientIngest(db, opts) {
|
|
@@ -10597,7 +11095,7 @@ function ingestFromDb(db, path) {
|
|
|
10597
11095
|
if (result.changed) changed += 1;
|
|
10598
11096
|
}
|
|
10599
11097
|
});
|
|
10600
|
-
run2();
|
|
11098
|
+
run2.immediate();
|
|
10601
11099
|
changed += reprojectDeferredOps(db);
|
|
10602
11100
|
return { path, read, changed, skippedLocal };
|
|
10603
11101
|
} finally {
|
|
@@ -10680,7 +11178,7 @@ var ENTITY_RESTORE_ORDER = {
|
|
|
10680
11178
|
function restoreRank(entity) {
|
|
10681
11179
|
return ENTITY_RESTORE_ORDER[entity] ?? 3;
|
|
10682
11180
|
}
|
|
10683
|
-
function priorFieldValue(db, entity, key, hlc, field) {
|
|
11181
|
+
function priorFieldValue(db, entity, key, hlc, field, machineId2) {
|
|
10684
11182
|
const row = db.prepare(
|
|
10685
11183
|
`SELECT json_extract(payload, '$.' || @field) AS value
|
|
10686
11184
|
FROM ops
|
|
@@ -10688,15 +11186,16 @@ function priorFieldValue(db, entity, key, hlc, field) {
|
|
|
10688
11186
|
AND key = @key
|
|
10689
11187
|
AND op = 'put'
|
|
10690
11188
|
AND hlc < @hlc
|
|
11189
|
+
AND (@machineId IS NULL OR machine_id = @machineId)
|
|
10691
11190
|
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
10692
11191
|
AND json_type(payload, '$.' || @field) IS NOT NULL
|
|
10693
11192
|
ORDER BY hlc DESC
|
|
10694
11193
|
LIMIT 1`
|
|
10695
|
-
).get({ entity, key, hlc, field });
|
|
11194
|
+
).get({ entity, key, hlc, field, machineId: machineId2 ?? null });
|
|
10696
11195
|
if (row === void 0) return { found: false };
|
|
10697
11196
|
return { found: true, value: row.value };
|
|
10698
11197
|
}
|
|
10699
|
-
function laterWriters(db, entity, key, hlc, field, excludeGroup) {
|
|
11198
|
+
function laterWriters(db, entity, key, hlc, field, excludeGroup, machineId2) {
|
|
10700
11199
|
return db.prepare(
|
|
10701
11200
|
`SELECT DISTINCT group_id AS groupId, intent
|
|
10702
11201
|
FROM ops
|
|
@@ -10705,9 +11204,10 @@ function laterWriters(db, entity, key, hlc, field, excludeGroup) {
|
|
|
10705
11204
|
AND op = 'put'
|
|
10706
11205
|
AND hlc > @hlc
|
|
10707
11206
|
AND group_id <> @excludeGroup
|
|
11207
|
+
AND (@machineId IS NULL OR machine_id = @machineId)
|
|
10708
11208
|
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
10709
11209
|
AND json_type(payload, '$.' || @field) IS NOT NULL`
|
|
10710
|
-
).all({ entity, key, hlc, field, excludeGroup });
|
|
11210
|
+
).all({ entity, key, hlc, field, excludeGroup, machineId: machineId2 ?? null });
|
|
10711
11211
|
}
|
|
10712
11212
|
function laterRowWriters(db, entity, key, hlc, excludeGroup) {
|
|
10713
11213
|
return db.prepare(
|
|
@@ -10721,6 +11221,15 @@ function laterRowWriters(db, entity, key, hlc, excludeGroup) {
|
|
|
10721
11221
|
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}`
|
|
10722
11222
|
).all({ entity, key, hlc, excludeGroup });
|
|
10723
11223
|
}
|
|
11224
|
+
function isDeletedByLaterGroup(db, entity, key, groupId) {
|
|
11225
|
+
const row = db.prepare(
|
|
11226
|
+
`SELECT op, group_id AS groupId FROM ops
|
|
11227
|
+
WHERE entity = @entity AND key = @key
|
|
11228
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
11229
|
+
ORDER BY hlc DESC LIMIT 1`
|
|
11230
|
+
).get({ entity, key });
|
|
11231
|
+
return row?.op === "del" && row.groupId !== groupId;
|
|
11232
|
+
}
|
|
10724
11233
|
function laterDeleters(db, entity, key, hlc, excludeGroup) {
|
|
10725
11234
|
return db.prepare(
|
|
10726
11235
|
`SELECT DISTINCT group_id AS groupId, intent
|
|
@@ -10741,6 +11250,7 @@ function listRecentGroups(db, limit = 10) {
|
|
|
10741
11250
|
MAX(actor) AS actor
|
|
10742
11251
|
FROM ops
|
|
10743
11252
|
WHERE entity IN ('workstream','task','note','edge')
|
|
11253
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
10744
11254
|
GROUP BY group_id
|
|
10745
11255
|
ORDER BY MAX(hlc) DESC
|
|
10746
11256
|
LIMIT @limit`
|
|
@@ -10775,24 +11285,36 @@ var CAPTURED_TABLE_FOR = {
|
|
|
10775
11285
|
note: "task_notes",
|
|
10776
11286
|
edge: "task_edges"
|
|
10777
11287
|
};
|
|
10778
|
-
var NEVER_RESTORE = /* @__PURE__ */ new Set(["id", "local_id", "name", "workstream_id", "task_id"
|
|
11288
|
+
var NEVER_RESTORE = /* @__PURE__ */ new Set(["id", "local_id", "name", "workstream_id", "task_id"]);
|
|
11289
|
+
var OWNER_FIELD = "owner_id";
|
|
11290
|
+
var NEVER_RECONSTRUCT = /* @__PURE__ */ new Set([...NEVER_RESTORE, OWNER_FIELD]);
|
|
11291
|
+
var TOUCH_FIELD = "updated_at";
|
|
10779
11292
|
function planUndo(db, groupId) {
|
|
10780
11293
|
const rows = db.prepare(
|
|
10781
|
-
`SELECT o.seq, o.hlc, o.group_id, o.intent, o.actor, o.entity, o.key, o.op, o.payload,
|
|
11294
|
+
`SELECT o.seq, o.hlc, o.group_id, o.machine_id, o.intent, o.actor, o.entity, o.key, o.op, o.payload,
|
|
10782
11295
|
(SELECT COUNT(*) FROM ops p
|
|
10783
|
-
WHERE p.entity = o.entity AND p.key = o.key AND p.hlc < o.hlc
|
|
11296
|
+
WHERE p.entity = o.entity AND p.key = o.key AND p.hlc < o.hlc
|
|
11297
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}) AS prior,
|
|
10784
11298
|
(SELECT p.op FROM ops p
|
|
10785
11299
|
WHERE p.entity = o.entity AND p.key = o.key AND p.hlc < o.hlc
|
|
11300
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
10786
11301
|
ORDER BY p.hlc DESC LIMIT 1) AS priorKind
|
|
10787
11302
|
FROM ops o
|
|
10788
11303
|
WHERE o.group_id = @groupId
|
|
10789
11304
|
AND o.entity IN ('workstream','task','note','edge')
|
|
11305
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
10790
11306
|
ORDER BY o.hlc`
|
|
10791
11307
|
).all({ groupId });
|
|
10792
11308
|
if (rows.length === 0) throw new UndoGroupNotFoundError(groupId);
|
|
10793
11309
|
const inverses = [];
|
|
10794
11310
|
let skipped = 0;
|
|
10795
11311
|
const seenLegacyNoteTasks = /* @__PURE__ */ new Set();
|
|
11312
|
+
const restoredFields = /* @__PURE__ */ new Map();
|
|
11313
|
+
let machine;
|
|
11314
|
+
const localMachine = () => {
|
|
11315
|
+
machine ??= localMachineId(db);
|
|
11316
|
+
return machine;
|
|
11317
|
+
};
|
|
10796
11318
|
for (const row of rows) {
|
|
10797
11319
|
if (row.op === "del") {
|
|
10798
11320
|
let fields3 = reconstructRow(db, row.entity, row.key, row.hlc);
|
|
@@ -10834,6 +11356,10 @@ function planUndo(db, groupId) {
|
|
|
10834
11356
|
continue;
|
|
10835
11357
|
}
|
|
10836
11358
|
if (row.prior === 0 || row.priorKind === "del") {
|
|
11359
|
+
if (isDeletedByLaterGroup(db, row.entity, row.key, groupId)) {
|
|
11360
|
+
skipped += 1;
|
|
11361
|
+
continue;
|
|
11362
|
+
}
|
|
10837
11363
|
const later = [...laterRowWriters(db, row.entity, row.key, row.hlc, groupId)];
|
|
10838
11364
|
inverses.push({
|
|
10839
11365
|
entity: row.entity,
|
|
@@ -10848,14 +11374,26 @@ function planUndo(db, groupId) {
|
|
|
10848
11374
|
const payload = JSON.parse(row.payload);
|
|
10849
11375
|
const fields2 = {};
|
|
10850
11376
|
const supersededBy = [];
|
|
11377
|
+
const rowKey = `${row.entity}\0${row.key}`;
|
|
11378
|
+
const restored = restoredFields.get(rowKey) ?? /* @__PURE__ */ new Set();
|
|
11379
|
+
restoredFields.set(rowKey, restored);
|
|
10851
11380
|
for (const field of Object.keys(payload)) {
|
|
10852
|
-
if (NEVER_RESTORE.has(field)) continue;
|
|
10853
|
-
|
|
11381
|
+
if (NEVER_RESTORE.has(field) || restored.has(field)) continue;
|
|
11382
|
+
restored.add(field);
|
|
11383
|
+
let machineId2;
|
|
11384
|
+
if (field === OWNER_FIELD) {
|
|
11385
|
+
if (row.machine_id !== localMachine()) continue;
|
|
11386
|
+
machineId2 = localMachine();
|
|
11387
|
+
}
|
|
11388
|
+
const prior = field === OWNER_FIELD ? priorOwner(db, row.key, row.hlc, localMachine()) : priorFieldValue(db, row.entity, row.key, row.hlc, field, machineId2);
|
|
10854
11389
|
if (!prior.found) {
|
|
10855
11390
|
continue;
|
|
10856
11391
|
}
|
|
11392
|
+
if (field === OWNER_FIELD && prior.value !== null && !agentExists(db, prior.value)) continue;
|
|
11393
|
+
const writers = laterWriters(db, row.entity, row.key, row.hlc, field, groupId, machineId2);
|
|
11394
|
+
if (field === TOUCH_FIELD && writers.length > 0) continue;
|
|
10857
11395
|
fields2[field] = prior.value;
|
|
10858
|
-
for (const writer of
|
|
11396
|
+
for (const writer of writers) {
|
|
10859
11397
|
supersededBy.push({ field, groupId: writer.groupId, intent: writer.intent });
|
|
10860
11398
|
}
|
|
10861
11399
|
}
|
|
@@ -10890,6 +11428,27 @@ function planUndo(db, groupId) {
|
|
|
10890
11428
|
skipped
|
|
10891
11429
|
};
|
|
10892
11430
|
}
|
|
11431
|
+
function priorOwner(db, key, hlc, machineId2) {
|
|
11432
|
+
const local = priorFieldValue(db, "task", key, hlc, OWNER_FIELD, machineId2);
|
|
11433
|
+
if (!local.found) return { found: true, value: null };
|
|
11434
|
+
const tomb = db.prepare(
|
|
11435
|
+
`SELECT 1 FROM ops d
|
|
11436
|
+
WHERE d.entity = 'task' AND d.key = @key AND d.op = 'del' AND d.hlc < @hlc
|
|
11437
|
+
AND d.hlc > (SELECT MAX(hlc) FROM ops
|
|
11438
|
+
WHERE entity = 'task' AND key = @key AND op = 'put'
|
|
11439
|
+
AND hlc < @hlc AND machine_id = @machineId
|
|
11440
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
11441
|
+
AND json_type(payload, '$.owner_id') IS NOT NULL)
|
|
11442
|
+
AND NOT EXISTS (SELECT 1 FROM ops p
|
|
11443
|
+
WHERE p.entity = 'task' AND p.key = @key AND p.op = 'put'
|
|
11444
|
+
AND p.hlc > d.hlc AND p.seq < d.seq)
|
|
11445
|
+
LIMIT 1`
|
|
11446
|
+
).get({ key, hlc, machineId: machineId2 });
|
|
11447
|
+
return tomb === void 0 ? local : { found: true, value: null };
|
|
11448
|
+
}
|
|
11449
|
+
function agentExists(db, id) {
|
|
11450
|
+
return db.prepare("SELECT 1 FROM agents WHERE id = ?").get(id) !== void 0;
|
|
11451
|
+
}
|
|
10893
11452
|
function groupWhen(db, groupId) {
|
|
10894
11453
|
const row = db.prepare("SELECT MIN(created_at) AS when_ FROM ops WHERE group_id = ?").get(groupId);
|
|
10895
11454
|
return row?.when_ ?? "";
|
|
@@ -10924,7 +11483,7 @@ function payloadFields(payload) {
|
|
|
10924
11483
|
const fields2 = {};
|
|
10925
11484
|
const parsed = JSON.parse(payload);
|
|
10926
11485
|
for (const [field, value] of Object.entries(parsed)) {
|
|
10927
|
-
if (
|
|
11486
|
+
if (NEVER_RECONSTRUCT.has(field)) continue;
|
|
10928
11487
|
if (value === null || typeof value === "string" || typeof value === "number") {
|
|
10929
11488
|
fields2[field] = value;
|
|
10930
11489
|
}
|
|
@@ -10942,7 +11501,7 @@ function reconstructRow(db, entity, key, beforeHlc) {
|
|
|
10942
11501
|
for (const put of puts) {
|
|
10943
11502
|
const parsed = JSON.parse(put.payload);
|
|
10944
11503
|
for (const [field, value] of Object.entries(parsed)) {
|
|
10945
|
-
if (
|
|
11504
|
+
if (NEVER_RECONSTRUCT.has(field)) continue;
|
|
10946
11505
|
if (value === null || typeof value === "string" || typeof value === "number") {
|
|
10947
11506
|
fields2[field] = value;
|
|
10948
11507
|
}
|
|
@@ -10973,8 +11532,8 @@ function undoGroup(db, groupId, opts = {}) {
|
|
|
10973
11532
|
}
|
|
10974
11533
|
);
|
|
10975
11534
|
});
|
|
10976
|
-
run2();
|
|
10977
|
-
return { plan, undoGroupId, applied };
|
|
11535
|
+
run2.immediate();
|
|
11536
|
+
return { plan, undoGroupId: applied > 0 && undoGroupId !== "" ? undoGroupId : null, applied };
|
|
10978
11537
|
}
|
|
10979
11538
|
function applyInverse(db, inverse) {
|
|
10980
11539
|
const table = CAPTURED_TABLE_FOR[inverse.entity];
|
|
@@ -11183,6 +11742,7 @@ export {
|
|
|
11183
11742
|
AgentCtlUnreachableError,
|
|
11184
11743
|
AgentDiedOnSpawnError,
|
|
11185
11744
|
AgentExistsError,
|
|
11745
|
+
AgentExtensionOutdatedError,
|
|
11186
11746
|
AgentFreshNeedsCtlError,
|
|
11187
11747
|
AgentNotFoundError,
|
|
11188
11748
|
AgentNotInWorkstreamError,
|
|
@@ -11199,6 +11759,8 @@ export {
|
|
|
11199
11759
|
DriftDetectedError,
|
|
11200
11760
|
EXPECTED_TABLES,
|
|
11201
11761
|
GLYPH,
|
|
11762
|
+
GroupIdAmbiguousError,
|
|
11763
|
+
HazardFailError,
|
|
11202
11764
|
HlcOverflowError,
|
|
11203
11765
|
HlcParseError,
|
|
11204
11766
|
HomeDirAsProjectRootError,
|
|
@@ -11206,6 +11768,7 @@ export {
|
|
|
11206
11768
|
KNOWN_INTENTS,
|
|
11207
11769
|
KNOWN_VERBS,
|
|
11208
11770
|
LinkConflictError,
|
|
11771
|
+
LogKindReservedError,
|
|
11209
11772
|
MACHINE_LOCAL_ENTITIES,
|
|
11210
11773
|
MACHINE_LOCAL_TABLES,
|
|
11211
11774
|
MU_SHIM_MARKER,
|
|
@@ -11222,11 +11785,15 @@ export {
|
|
|
11222
11785
|
PEER_STALE_MS,
|
|
11223
11786
|
PORTABLE_TABLES,
|
|
11224
11787
|
PaneNotFoundError,
|
|
11788
|
+
PaneNotInSessionError,
|
|
11789
|
+
ReaperDetectedDuringWaitError,
|
|
11225
11790
|
RebuildTargetExistsError,
|
|
11226
11791
|
RebuildTargetIsSourceError,
|
|
11227
11792
|
SEGMENT_FORMAT_VERSION,
|
|
11228
11793
|
SYNCED_ENTITIES,
|
|
11794
|
+
SchemaTooNewError,
|
|
11229
11795
|
SchemaTooOldError,
|
|
11796
|
+
StallDetectedDuringWaitError,
|
|
11230
11797
|
SubstateReasonRequiredError,
|
|
11231
11798
|
SyncPeerNotFoundError,
|
|
11232
11799
|
SyncPeerRefAmbiguousError,
|
|
@@ -11236,23 +11803,31 @@ export {
|
|
|
11236
11803
|
TASK_SUBSTATES,
|
|
11237
11804
|
TASK_SUBSTATE_ROWS,
|
|
11238
11805
|
TaskAlreadyOwnedError,
|
|
11806
|
+
TaskClaimStaleWorkspaceError,
|
|
11239
11807
|
TaskExistsError,
|
|
11808
|
+
TaskIdInvalidError,
|
|
11240
11809
|
TaskInTriageError,
|
|
11241
11810
|
TaskNotFoundError,
|
|
11242
11811
|
TaskNotInWorkstreamError,
|
|
11243
11812
|
TaskParkStateError,
|
|
11244
11813
|
TaskParkedError,
|
|
11814
|
+
TaskTitleSlugEmptyError,
|
|
11245
11815
|
TmuxError,
|
|
11246
11816
|
UNKNOWN_REASON,
|
|
11247
11817
|
UndoGroupNotFoundError,
|
|
11248
11818
|
UndoSupersededError,
|
|
11249
11819
|
WORKSPACE_STALE_THRESHOLD,
|
|
11820
|
+
WorkspaceConflictError,
|
|
11821
|
+
WorkspaceDirtyError,
|
|
11250
11822
|
WorkspaceExistsError,
|
|
11251
11823
|
WorkspaceNotFoundError,
|
|
11252
11824
|
WorkspacePathNotEmptyError,
|
|
11253
11825
|
WorkspacePreservedError,
|
|
11826
|
+
WorkspaceVcsRequiredError,
|
|
11254
11827
|
WorkstreamExistsError,
|
|
11255
11828
|
WorkstreamNameInvalidError,
|
|
11829
|
+
WorkstreamNameReservedError,
|
|
11830
|
+
WorkstreamNotFoundError,
|
|
11256
11831
|
abortAgent,
|
|
11257
11832
|
acceptTask,
|
|
11258
11833
|
activeMux,
|