@martintrojer/mu 0.4.0 → 0.4.2
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 +19 -14
- package/README.md +28 -14
- package/dist/cli.js +1663 -724
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +264 -132
- package/dist/index.js +1045 -568
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +12 -9
- package/docs/ROADMAP.md +81 -2
- package/docs/USAGE_GUIDE.md +147 -89
- package/docs/VOCABULARY.md +21 -3
- package/package.json +3 -9
- package/skills/mu/SKILL.md +8 -3
package/dist/index.js
CHANGED
|
@@ -5,8 +5,9 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/db.ts
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
8
9
|
import { mkdirSync } from "fs";
|
|
9
|
-
import { homedir } from "os";
|
|
10
|
+
import { homedir, hostname } from "os";
|
|
10
11
|
import { dirname, join, resolve } from "path";
|
|
11
12
|
import Database from "better-sqlite3";
|
|
12
13
|
function defaultStateDir() {
|
|
@@ -35,6 +36,7 @@ function openDb(options = {}) {
|
|
|
35
36
|
throw new SchemaTooOldError(detectedVersion, MIN_ACCEPTED_SCHEMA_VERSION);
|
|
36
37
|
}
|
|
37
38
|
applySchema(db);
|
|
39
|
+
seedMachineIdentity(db);
|
|
38
40
|
} else {
|
|
39
41
|
db.pragma("foreign_keys = ON");
|
|
40
42
|
}
|
|
@@ -120,6 +122,14 @@ function detectExistingSchemaVersion(db) {
|
|
|
120
122
|
if (hasWorkstreams) return 1;
|
|
121
123
|
return null;
|
|
122
124
|
}
|
|
125
|
+
function seedMachineIdentity(db) {
|
|
126
|
+
const row = db.prepare("SELECT COUNT(*) AS count FROM machine_identity").get();
|
|
127
|
+
if (row.count !== 0) return;
|
|
128
|
+
db.prepare(
|
|
129
|
+
`INSERT OR IGNORE INTO machine_identity (id, machine_id, hostname, created_at)
|
|
130
|
+
VALUES (1, ?, ?, ?)`
|
|
131
|
+
).run(randomUUID(), hostname(), (/* @__PURE__ */ new Date()).toISOString());
|
|
132
|
+
}
|
|
123
133
|
function applySchema(db) {
|
|
124
134
|
const preBumpVersion = detectExistingSchemaVersion(db);
|
|
125
135
|
db.exec(CURRENT_SCHEMA);
|
|
@@ -136,7 +146,7 @@ function applySchema(db) {
|
|
|
136
146
|
CURRENT_SCHEMA_VERSION
|
|
137
147
|
);
|
|
138
148
|
}
|
|
139
|
-
var CURRENT_SCHEMA_VERSION =
|
|
149
|
+
var CURRENT_SCHEMA_VERSION = 8;
|
|
140
150
|
var MIN_ACCEPTED_SCHEMA_VERSION = 5;
|
|
141
151
|
var EXPECTED_TABLES = [
|
|
142
152
|
"agent_logs",
|
|
@@ -146,12 +156,14 @@ var EXPECTED_TABLES = [
|
|
|
146
156
|
"archived_notes",
|
|
147
157
|
"archived_tasks",
|
|
148
158
|
"archives",
|
|
159
|
+
"machine_identity",
|
|
149
160
|
"schema_version",
|
|
150
161
|
"snapshots",
|
|
151
162
|
"task_edges",
|
|
152
163
|
"task_notes",
|
|
153
164
|
"tasks",
|
|
154
165
|
"vcs_workspaces",
|
|
166
|
+
"workstream_sync",
|
|
155
167
|
"workstreams"
|
|
156
168
|
];
|
|
157
169
|
var READY_VIEW_SQL = `
|
|
@@ -203,6 +215,15 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
|
|
203
215
|
version INTEGER NOT NULL
|
|
204
216
|
);
|
|
205
217
|
|
|
218
|
+
-- machine_identity: one durable identity per DB/machine, seeded by
|
|
219
|
+
-- openDb after schema creation. hostname is advisory only.
|
|
220
|
+
CREATE TABLE IF NOT EXISTS machine_identity (
|
|
221
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
222
|
+
machine_id TEXT NOT NULL,
|
|
223
|
+
hostname TEXT,
|
|
224
|
+
created_at TEXT NOT NULL
|
|
225
|
+
);
|
|
226
|
+
|
|
206
227
|
-- \u2500\u2500\u2500 Tables \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
207
228
|
|
|
208
229
|
-- workstreams: top of the hierarchy. name stays globally unique
|
|
@@ -214,6 +235,13 @@ CREATE TABLE IF NOT EXISTS workstreams (
|
|
|
214
235
|
created_at TEXT NOT NULL -- ISO 8601
|
|
215
236
|
);
|
|
216
237
|
|
|
238
|
+
-- workstream_sync: per-workstream cross-machine drift state. Rows are
|
|
239
|
+
-- created on demand by db import/export code, not pre-seeded.
|
|
240
|
+
CREATE TABLE IF NOT EXISTS workstream_sync (
|
|
241
|
+
workstream_id INTEGER PRIMARY KEY REFERENCES workstreams (id) ON DELETE CASCADE,
|
|
242
|
+
last_known_peer_seqs TEXT NOT NULL DEFAULT '{}'
|
|
243
|
+
);
|
|
244
|
+
|
|
217
245
|
-- agents: one row per managed pane. Per-workstream unique on name.
|
|
218
246
|
CREATE TABLE IF NOT EXISTS agents (
|
|
219
247
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -503,8 +531,8 @@ function listLogs(db, opts = {}) {
|
|
|
503
531
|
const rows = db.prepare(sql).all(...params);
|
|
504
532
|
return rows.map(rowFromDb);
|
|
505
533
|
}
|
|
506
|
-
function latestSeq(db) {
|
|
507
|
-
const row = db.prepare("SELECT MAX(seq) AS s FROM agent_logs").get();
|
|
534
|
+
function latestSeq(db, workstreamId) {
|
|
535
|
+
const row = workstreamId === void 0 ? db.prepare("SELECT MAX(seq) AS s FROM agent_logs").get() : db.prepare("SELECT MAX(seq) AS s FROM agent_logs WHERE workstream_id = ?").get(workstreamId);
|
|
508
536
|
return row.s ?? 0;
|
|
509
537
|
}
|
|
510
538
|
function emitEvent(db, workstream, payload, source = "system") {
|
|
@@ -565,17 +593,22 @@ var EVENT_VERB_PREFIXES = [
|
|
|
565
593
|
"workstream init",
|
|
566
594
|
"workstream destroy",
|
|
567
595
|
"workstream export",
|
|
568
|
-
"workstream import",
|
|
569
596
|
// src/archives.ts — v6 archive verbs. Machine-wide events
|
|
570
597
|
// (workstream=null) because archives outlive workstreams.
|
|
571
598
|
"archive create",
|
|
572
599
|
"archive delete",
|
|
573
600
|
"archive add",
|
|
574
601
|
"archive remove",
|
|
602
|
+
"archive restore",
|
|
575
603
|
// src/exporting.ts — archive export emits the bucket-render summary
|
|
576
604
|
// as a machine-wide event (workstream=null; the export spans every
|
|
577
605
|
// source-ws in the archive).
|
|
578
|
-
"archive export"
|
|
606
|
+
"archive export",
|
|
607
|
+
// src/db-sync.ts — emitted per-workstream after a successful
|
|
608
|
+
// `mu db export`. Used as the marker for src/parked.ts (the
|
|
609
|
+
// "presumed parked on another machine" heuristic for `mu workstream
|
|
610
|
+
// list` / TUI tab strip).
|
|
611
|
+
"db export"
|
|
579
612
|
];
|
|
580
613
|
function classifyEventVerb(payload) {
|
|
581
614
|
for (const verb of EVENT_VERB_PREFIXES) {
|
|
@@ -1093,17 +1126,19 @@ async function reconcile(db, opts) {
|
|
|
1093
1126
|
let statusChanges = 0;
|
|
1094
1127
|
const orphans = [];
|
|
1095
1128
|
const survivors = [];
|
|
1129
|
+
const pendingSurvivors = [];
|
|
1096
1130
|
for (const agent of dbAgents) {
|
|
1097
|
-
if (
|
|
1131
|
+
if (isPendingPaneId(agent.paneId)) {
|
|
1132
|
+
pendingSurvivors.push(agent);
|
|
1133
|
+
} else if (tmuxByPaneId.has(agent.paneId)) {
|
|
1098
1134
|
survivors.push(agent);
|
|
1099
1135
|
} else {
|
|
1100
1136
|
if (mode === "full") deleteAgent(db, agent.name, agent.workstreamName);
|
|
1101
1137
|
prunedGhosts++;
|
|
1102
1138
|
}
|
|
1103
1139
|
}
|
|
1104
|
-
if (mode
|
|
1140
|
+
if (mode === "full") {
|
|
1105
1141
|
for (const agent of survivors) {
|
|
1106
|
-
if (isPendingPaneId(agent.paneId)) continue;
|
|
1107
1142
|
const scrollback = await capturePane(agent.paneId, { lines: 100 });
|
|
1108
1143
|
const detected = detectPiStatus(scrollback);
|
|
1109
1144
|
if (shouldOverwriteAgentStatus(agent.status, detected) && detected !== agent.status) {
|
|
@@ -2236,6 +2271,10 @@ var WorkspacePreservedError = class extends Error {
|
|
|
2236
2271
|
intent: "Or close + discard the workspace in one shot (lossy)",
|
|
2237
2272
|
command: `mu agent close ${this.agentName} --discard-workspace`
|
|
2238
2273
|
},
|
|
2274
|
+
{
|
|
2275
|
+
intent: "If the workstream was archived, restore task memory under a fresh name",
|
|
2276
|
+
command: "mu archive restore <label> --as <new-workstream> --source <workstream>"
|
|
2277
|
+
},
|
|
2239
2278
|
{
|
|
2240
2279
|
intent: "Or just inspect what's in the workspace",
|
|
2241
2280
|
command: `cd ${this.workspacePath}`
|
|
@@ -4568,6 +4607,106 @@ function searchArchives(db, opts) {
|
|
|
4568
4607
|
return hits.slice(0, limit);
|
|
4569
4608
|
}
|
|
4570
4609
|
|
|
4610
|
+
// src/archives/restore.ts
|
|
4611
|
+
var ArchiveSourceAmbiguousError = class extends Error {
|
|
4612
|
+
constructor(label, sources) {
|
|
4613
|
+
super(
|
|
4614
|
+
sources.length === 0 ? `archive ${label} contains no source workstreams` : `archive ${label} requires --source <orig-ws-name>. Available: ${sources.join(", ")}`
|
|
4615
|
+
);
|
|
4616
|
+
this.label = label;
|
|
4617
|
+
this.sources = sources;
|
|
4618
|
+
}
|
|
4619
|
+
label;
|
|
4620
|
+
sources;
|
|
4621
|
+
name = "ArchiveSourceAmbiguousError";
|
|
4622
|
+
errorNextSteps() {
|
|
4623
|
+
return [
|
|
4624
|
+
{ intent: "Inspect archive sources", command: `mu archive show ${this.label}` },
|
|
4625
|
+
...this.sources.map((source) => ({
|
|
4626
|
+
intent: `Restore source workstream ${source}`,
|
|
4627
|
+
command: `mu archive restore ${this.label} --source ${source} --as <new-workstream>`
|
|
4628
|
+
}))
|
|
4629
|
+
];
|
|
4630
|
+
}
|
|
4631
|
+
};
|
|
4632
|
+
function restoreArchive(db, label, asWorkstream, opts = {}) {
|
|
4633
|
+
const archiveId = resolveArchiveId(db, label);
|
|
4634
|
+
const sources = listSources(db, archiveId);
|
|
4635
|
+
if (!isValidWorkstreamName(asWorkstream)) throw new WorkstreamNameInvalidError(asWorkstream);
|
|
4636
|
+
const sourceWorkstream = opts.sourceWorkstream ?? sources[0];
|
|
4637
|
+
if (sourceWorkstream === void 0) throw new ArchiveSourceAmbiguousError(label, sources);
|
|
4638
|
+
if (opts.sourceWorkstream === void 0 && sources.length > 1) {
|
|
4639
|
+
throw new ArchiveSourceAmbiguousError(label, sources);
|
|
4640
|
+
}
|
|
4641
|
+
if (opts.sourceWorkstream !== void 0 && !sources.includes(opts.sourceWorkstream)) {
|
|
4642
|
+
throw new ArchiveSourceAmbiguousError(label, sources);
|
|
4643
|
+
}
|
|
4644
|
+
if (tryResolveWorkstreamId(db, asWorkstream) !== null) {
|
|
4645
|
+
throw new WorkstreamExistsError(asWorkstream);
|
|
4646
|
+
}
|
|
4647
|
+
captureSnapshot(db, `archive restore ${label} as ${asWorkstream}`, null);
|
|
4648
|
+
return db.transaction(() => {
|
|
4649
|
+
ensureWorkstream(db, asWorkstream);
|
|
4650
|
+
const wsId = resolveWorkstreamId(db, asWorkstream);
|
|
4651
|
+
const restoredTasks = db.prepare(
|
|
4652
|
+
`INSERT INTO tasks
|
|
4653
|
+
(workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
|
|
4654
|
+
SELECT ?, original_local_id, title, status, impact, effort_days, NULL,
|
|
4655
|
+
original_created_at, original_updated_at
|
|
4656
|
+
FROM archived_tasks
|
|
4657
|
+
WHERE archive_id = ? AND source_workstream = ?
|
|
4658
|
+
ORDER BY id`
|
|
4659
|
+
).run(wsId, archiveId, sourceWorkstream).changes;
|
|
4660
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4661
|
+
const restoredEdges = db.prepare(
|
|
4662
|
+
`INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
|
|
4663
|
+
SELECT live_from.id, live_to.id, ?
|
|
4664
|
+
FROM archived_edges e
|
|
4665
|
+
JOIN archived_tasks arch_from ON arch_from.id = e.from_archived_id
|
|
4666
|
+
JOIN archived_tasks arch_to ON arch_to.id = e.to_archived_id
|
|
4667
|
+
JOIN tasks live_from ON live_from.workstream_id = ?
|
|
4668
|
+
AND live_from.local_id = arch_from.original_local_id
|
|
4669
|
+
JOIN tasks live_to ON live_to.workstream_id = ?
|
|
4670
|
+
AND live_to.local_id = arch_to.original_local_id
|
|
4671
|
+
WHERE e.archive_id = ?
|
|
4672
|
+
AND arch_from.source_workstream = ?
|
|
4673
|
+
AND arch_to.source_workstream = ?`
|
|
4674
|
+
).run(now, wsId, wsId, archiveId, sourceWorkstream, sourceWorkstream).changes;
|
|
4675
|
+
const restoredNotes = db.prepare(
|
|
4676
|
+
`INSERT INTO task_notes (task_id, author, content, created_at)
|
|
4677
|
+
SELECT live.id, n.author, n.content, n.created_at
|
|
4678
|
+
FROM archived_notes n
|
|
4679
|
+
JOIN archived_tasks arch ON arch.id = n.archived_task_id
|
|
4680
|
+
JOIN tasks live ON live.workstream_id = ?
|
|
4681
|
+
AND live.local_id = arch.original_local_id
|
|
4682
|
+
WHERE n.archive_id = ? AND arch.source_workstream = ?
|
|
4683
|
+
ORDER BY n.id`
|
|
4684
|
+
).run(wsId, archiveId, sourceWorkstream).changes;
|
|
4685
|
+
emitEvent(
|
|
4686
|
+
db,
|
|
4687
|
+
asWorkstream,
|
|
4688
|
+
`archive restore ${label} source=${sourceWorkstream} as ${asWorkstream} (tasks=${restoredTasks}, edges=${restoredEdges}, notes=${restoredNotes})`
|
|
4689
|
+
);
|
|
4690
|
+
return {
|
|
4691
|
+
archiveLabel: label,
|
|
4692
|
+
sourceWorkstream,
|
|
4693
|
+
workstreamName: asWorkstream,
|
|
4694
|
+
restoredTasks,
|
|
4695
|
+
restoredEdges,
|
|
4696
|
+
restoredNotes
|
|
4697
|
+
};
|
|
4698
|
+
})();
|
|
4699
|
+
}
|
|
4700
|
+
function listSources(db, archiveId) {
|
|
4701
|
+
return db.prepare(
|
|
4702
|
+
`SELECT source_workstream AS name
|
|
4703
|
+
FROM archived_tasks
|
|
4704
|
+
WHERE archive_id = ?
|
|
4705
|
+
GROUP BY source_workstream
|
|
4706
|
+
ORDER BY source_workstream`
|
|
4707
|
+
).all(archiveId).map((row) => row.name);
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4571
4710
|
// src/tasks/status.ts
|
|
4572
4711
|
var TASK_STATUSES = [
|
|
4573
4712
|
"OPEN",
|
|
@@ -5137,6 +5276,31 @@ function exportArchive(db, opts) {
|
|
|
5137
5276
|
return { ...result, archiveLabel: opts.label, sourceCount: sources.length };
|
|
5138
5277
|
}
|
|
5139
5278
|
|
|
5279
|
+
// src/parked.ts
|
|
5280
|
+
var WORKSTREAM_PARKED_THRESHOLD_DAYS = 1;
|
|
5281
|
+
function parkedStatus(db, workstream, opts = {}) {
|
|
5282
|
+
const wsRow = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream);
|
|
5283
|
+
if (wsRow === void 0) return { parked: false };
|
|
5284
|
+
const latest = db.prepare(
|
|
5285
|
+
"SELECT kind, payload, created_at FROM agent_logs WHERE workstream_id = ? ORDER BY seq DESC LIMIT 1"
|
|
5286
|
+
).get(wsRow.id);
|
|
5287
|
+
if (latest === void 0) return { parked: false };
|
|
5288
|
+
if (latest.kind !== "event") return { parked: false };
|
|
5289
|
+
if (!latest.payload.startsWith("db export ")) return { parked: false };
|
|
5290
|
+
const aliveAgent = db.prepare("SELECT 1 AS x FROM agents WHERE workstream_id = ? AND status != 'closed' LIMIT 1").get(wsRow.id);
|
|
5291
|
+
if (aliveAgent !== void 0) return { parked: false };
|
|
5292
|
+
const inProgress = db.prepare("SELECT 1 AS x FROM tasks WHERE workstream_id = ? AND status = 'IN_PROGRESS' LIMIT 1").get(wsRow.id);
|
|
5293
|
+
if (inProgress !== void 0) return { parked: false };
|
|
5294
|
+
const threshold = Math.max(0, opts.thresholdDays ?? WORKSTREAM_PARKED_THRESHOLD_DAYS);
|
|
5295
|
+
const exportedAt = Date.parse(latest.created_at);
|
|
5296
|
+
if (Number.isNaN(exportedAt)) return { parked: false };
|
|
5297
|
+
const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
5298
|
+
const deltaMs = now - exportedAt;
|
|
5299
|
+
const deltaDays = Math.floor(deltaMs / (24 * 60 * 60 * 1e3));
|
|
5300
|
+
if (deltaDays < threshold) return { parked: false };
|
|
5301
|
+
return { parked: true, sinceDays: deltaDays };
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5140
5304
|
// src/workstream.ts
|
|
5141
5305
|
var WORKSTREAM_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
5142
5306
|
var RESERVED_WORKSTREAM_PREFIX = "mu-";
|
|
@@ -5145,6 +5309,27 @@ function isValidWorkstreamName(name) {
|
|
|
5145
5309
|
if (name.startsWith(RESERVED_WORKSTREAM_PREFIX)) return false;
|
|
5146
5310
|
return true;
|
|
5147
5311
|
}
|
|
5312
|
+
var WorkstreamExistsError = class extends Error {
|
|
5313
|
+
constructor(workstream) {
|
|
5314
|
+
super(`workstream already exists: ${workstream}`);
|
|
5315
|
+
this.workstream = workstream;
|
|
5316
|
+
}
|
|
5317
|
+
workstream;
|
|
5318
|
+
name = "WorkstreamExistsError";
|
|
5319
|
+
errorNextSteps() {
|
|
5320
|
+
return [
|
|
5321
|
+
{
|
|
5322
|
+
intent: "Pick a different workstream name",
|
|
5323
|
+
command: "mu archive restore <label> --as <new-name>"
|
|
5324
|
+
},
|
|
5325
|
+
{ intent: "List existing workstreams", command: "mu workstream list" },
|
|
5326
|
+
{
|
|
5327
|
+
intent: "Destroy the existing workstream first",
|
|
5328
|
+
command: `mu workstream destroy -w ${this.workstream} --yes`
|
|
5329
|
+
}
|
|
5330
|
+
];
|
|
5331
|
+
}
|
|
5332
|
+
};
|
|
5148
5333
|
var WorkstreamNameInvalidError = class extends Error {
|
|
5149
5334
|
constructor(attempted) {
|
|
5150
5335
|
const reason = attempted.startsWith(RESERVED_WORKSTREAM_PREFIX) ? `the 'mu-' prefix is reserved (mu auto-prepends 'mu-' to derive the tmux session name; '${attempted}' would produce session 'mu-${attempted}', which is double-prefixed and almost never what you want). Drop the 'mu-' from the workstream name.` : `must match /^[a-z][a-z0-9_-]{0,31}$/. tmux silently rewrites '.' to '_' and reserves ':' as a target separator, so workstream names containing those characters would create tmux sessions mu couldn't look up afterwards. Use letters, digits, '_', and '-' only.`;
|
|
@@ -5187,6 +5372,7 @@ async function listWorkstreams(db) {
|
|
|
5187
5372
|
}
|
|
5188
5373
|
async function summarizeWorkstream(db, opts) {
|
|
5189
5374
|
const tmuxSession = opts.tmuxSession ?? `mu-${opts.workstream}`;
|
|
5375
|
+
const parked = parkedStatus(db, opts.workstream);
|
|
5190
5376
|
return {
|
|
5191
5377
|
name: opts.workstream,
|
|
5192
5378
|
tmuxSession,
|
|
@@ -5196,7 +5382,8 @@ async function summarizeWorkstream(db, opts) {
|
|
|
5196
5382
|
noteCount: countNotes(db, opts.workstream),
|
|
5197
5383
|
edgeCount: countEdges(db, opts.workstream),
|
|
5198
5384
|
workspaceCount: listWorkspaces(db, opts.workstream).length,
|
|
5199
|
-
registered: isRegistered(db, opts.workstream)
|
|
5385
|
+
registered: isRegistered(db, opts.workstream),
|
|
5386
|
+
...parked.parked ? { parked: { sinceDays: parked.sinceDays ?? 0 } } : {}
|
|
5200
5387
|
};
|
|
5201
5388
|
}
|
|
5202
5389
|
function isRegistered(db, workstream) {
|
|
@@ -6244,6 +6431,9 @@ var STATUS_EMOJI = {
|
|
|
6244
6431
|
terminated: "\uF057"
|
|
6245
6432
|
// nf-fa-times_circle
|
|
6246
6433
|
};
|
|
6434
|
+
function agentStatusGlyph(status) {
|
|
6435
|
+
return STATUS_EMOJI[status] ?? "?";
|
|
6436
|
+
}
|
|
6247
6437
|
var MAX_TITLE_LEN = 64;
|
|
6248
6438
|
var PENDING_PANE_PREFIX = "%pending-";
|
|
6249
6439
|
function pendingPaneIdFor(agentName) {
|
|
@@ -6257,7 +6447,7 @@ function composeAgentTitle(db, agent) {
|
|
|
6257
6447
|
const tasks = listTasksByOwner(db, agent.workstreamName, agent.name);
|
|
6258
6448
|
let title = agent.name;
|
|
6259
6449
|
if (showStatus) {
|
|
6260
|
-
title += ` \xB7 ${
|
|
6450
|
+
title += ` \xB7 ${agentStatusGlyph(agent.status)}`;
|
|
6261
6451
|
}
|
|
6262
6452
|
if (tasks.length === 1) {
|
|
6263
6453
|
title += ` \xB7 ${tasks[0]?.name}`;
|
|
@@ -6389,6 +6579,8 @@ async function listLiveAgents(db, opts) {
|
|
|
6389
6579
|
}
|
|
6390
6580
|
|
|
6391
6581
|
// src/dag.ts
|
|
6582
|
+
import pc2 from "picocolors";
|
|
6583
|
+
var RECURRENCE_MARKER = ` ${pc2.dim("(\u21BB)")}`;
|
|
6392
6584
|
function loadFullDag(db, workstream, opts = {}) {
|
|
6393
6585
|
const tasks = listTasks(db, workstream).filter(
|
|
6394
6586
|
(t) => opts.statuses === void 0 || opts.statuses.has(t.status)
|
|
@@ -6427,7 +6619,7 @@ function renderForest(roots, edges, statusFn, tasksByName, opts = {}) {
|
|
|
6427
6619
|
if (!byName.has(root.name)) byName.set(root.name, root);
|
|
6428
6620
|
const lines = [formatTreeNodeLabel(root, statusFn, opts)];
|
|
6429
6621
|
if (seen.has(root.name)) {
|
|
6430
|
-
lines[0] = `${lines[0]}
|
|
6622
|
+
lines[0] = `${lines[0]}${RECURRENCE_MARKER}`;
|
|
6431
6623
|
} else {
|
|
6432
6624
|
seen.add(root.name);
|
|
6433
6625
|
renderForestChildren(root.name, "", edges, byName, statusFn, seen, lines, opts);
|
|
@@ -6486,7 +6678,7 @@ function renderForestChildren(taskName, prefix, edges, byName, statusFn, seen, l
|
|
|
6486
6678
|
}
|
|
6487
6679
|
if (seen.has(childName)) {
|
|
6488
6680
|
lines.push(
|
|
6489
|
-
`${prefix}${branch}${formatTreeNodeLabel(child, statusFn, opts)}
|
|
6681
|
+
`${prefix}${branch}${formatTreeNodeLabel(child, statusFn, opts)}${RECURRENCE_MARKER}`
|
|
6490
6682
|
);
|
|
6491
6683
|
continue;
|
|
6492
6684
|
}
|
|
@@ -6501,6 +6693,815 @@ function formatTreeNodeLabel(t, statusFn, opts = {}) {
|
|
|
6501
6693
|
return `${base} ${t.title}`;
|
|
6502
6694
|
}
|
|
6503
6695
|
|
|
6696
|
+
// src/db-sync.ts
|
|
6697
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
6698
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync2, unlinkSync as unlinkSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
6699
|
+
import { hostname as hostname2 } from "os";
|
|
6700
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
6701
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6702
|
+
|
|
6703
|
+
// src/db-sync-replay.ts
|
|
6704
|
+
import { createHash as createHash2 } from "crypto";
|
|
6705
|
+
var DbReplayWorkstreamMissingError = class extends Error {
|
|
6706
|
+
constructor(workstream) {
|
|
6707
|
+
super(
|
|
6708
|
+
`replay sidecar is for workstream "${workstream}", which does not exist locally; restore it first via mu db import or mu archive restore`
|
|
6709
|
+
);
|
|
6710
|
+
this.workstream = workstream;
|
|
6711
|
+
}
|
|
6712
|
+
workstream;
|
|
6713
|
+
name = "DbReplayWorkstreamMissingError";
|
|
6714
|
+
errorNextSteps() {
|
|
6715
|
+
return [
|
|
6716
|
+
{
|
|
6717
|
+
intent: "Restore this workstream from a DB export",
|
|
6718
|
+
command: "mu db import <file> --apply"
|
|
6719
|
+
},
|
|
6720
|
+
{
|
|
6721
|
+
intent: "Or restore it from an archive",
|
|
6722
|
+
command: `mu archive restore <label> --as ${this.workstream}`
|
|
6723
|
+
}
|
|
6724
|
+
];
|
|
6725
|
+
}
|
|
6726
|
+
};
|
|
6727
|
+
var DbReplayLocalIdConflictError = class extends Error {
|
|
6728
|
+
constructor(workstream, conflicts) {
|
|
6729
|
+
super(
|
|
6730
|
+
`sidecar task id collides with different local content in ${workstream}: ${conflicts.map(
|
|
6731
|
+
(c) => `${c.localId} (local: ${c.local.status} ${JSON.stringify(c.local.title)}; sidecar: ${c.sidecar.status} ${JSON.stringify(c.sidecar.title)})`
|
|
6732
|
+
).join(", ")}`
|
|
6733
|
+
);
|
|
6734
|
+
this.workstream = workstream;
|
|
6735
|
+
this.conflicts = conflicts;
|
|
6736
|
+
}
|
|
6737
|
+
workstream;
|
|
6738
|
+
conflicts;
|
|
6739
|
+
name = "DbReplayLocalIdConflictError";
|
|
6740
|
+
errorNextSteps() {
|
|
6741
|
+
const first = this.conflicts[0];
|
|
6742
|
+
return [
|
|
6743
|
+
{
|
|
6744
|
+
intent: "Create a renamed local task manually, then replay notes if desired",
|
|
6745
|
+
command: first ? `mu task add ${first.localId}-replay -w ${this.workstream} -t ${shellQuote2(first.sidecar.title)} -i <impact> -e <effort>` : `mu task add <renamed-id> -w ${this.workstream} -t <title> -i <impact> -e <effort>`
|
|
6746
|
+
},
|
|
6747
|
+
{
|
|
6748
|
+
intent: "Skip the colliding id and replay another task",
|
|
6749
|
+
command: "mu db replay <sidecar> --task <other-id> --apply"
|
|
6750
|
+
}
|
|
6751
|
+
];
|
|
6752
|
+
}
|
|
6753
|
+
};
|
|
6754
|
+
function replayDb(db, file, opts = {}) {
|
|
6755
|
+
const sidecarDb = openDb({ path: file, readonly: true });
|
|
6756
|
+
try {
|
|
6757
|
+
const plan = buildReplayPlan(db, sidecarDb, file);
|
|
6758
|
+
const taskFilter = new Set(opts.tasks ?? []);
|
|
6759
|
+
const noteFilter = new Set(opts.notes ?? []);
|
|
6760
|
+
const selectedTaskIds = opts.all === true ? new Set(plan.tasks.map((t) => t.localId)) : taskFilter;
|
|
6761
|
+
const selectedNoteIds = opts.all === true ? new Set(plan.notes.map((n) => n.taskLocalId)) : noteFilter;
|
|
6762
|
+
const hasSelectors = opts.all === true || selectedTaskIds.size > 0 || selectedNoteIds.size > 0;
|
|
6763
|
+
const noteTaskIds = /* @__PURE__ */ new Set([...selectedNoteIds, ...selectedTaskIds]);
|
|
6764
|
+
const hasWrites = plan.tasks.some((t) => selectedTaskIds.has(t.localId)) || plan.notes.some((n) => noteTaskIds.has(n.taskLocalId)) || plan.edges.some(
|
|
6765
|
+
(e) => opts.all === true || selectedTaskIds.has(e.fromLocalId) || selectedTaskIds.has(e.toLocalId)
|
|
6766
|
+
);
|
|
6767
|
+
const relevantConflicts = opts.all === true ? plan.conflicts : plan.conflicts.filter((c) => selectedTaskIds.has(c.localId));
|
|
6768
|
+
if (relevantConflicts.length > 0) {
|
|
6769
|
+
throw new DbReplayLocalIdConflictError(plan.workstream, relevantConflicts);
|
|
6770
|
+
}
|
|
6771
|
+
if (opts.apply !== true || !hasSelectors) return replayResult(plan, true, false);
|
|
6772
|
+
if (!hasWrites) return replayResult(plan, false, true);
|
|
6773
|
+
const snapshot = captureSnapshot(db, `db replay ${file}`, null);
|
|
6774
|
+
const applied = applyReplayPlan(db, plan, selectedTaskIds, selectedNoteIds, opts.all === true);
|
|
6775
|
+
return { ...replayResult(plan, false, true), snapshotId: snapshot.id, ...applied };
|
|
6776
|
+
} finally {
|
|
6777
|
+
sidecarDb.close();
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
6780
|
+
function buildReplayPlan(localDb, sidecarDb, sourceFile) {
|
|
6781
|
+
const sidecarWorkstreams = listLocalWorkstreams(sidecarDb);
|
|
6782
|
+
const sidecarWs = sidecarWorkstreams[0];
|
|
6783
|
+
if (sidecarWorkstreams.length !== 1 || !sidecarWs) {
|
|
6784
|
+
throw new Error(
|
|
6785
|
+
`replay sidecar must contain exactly one workstream; found ${sidecarWorkstreams.length}`
|
|
6786
|
+
);
|
|
6787
|
+
}
|
|
6788
|
+
const localWs = listLocalWorkstreams(localDb).find((w) => w.name === sidecarWs.name);
|
|
6789
|
+
if (!localWs) throw new DbReplayWorkstreamMissingError(sidecarWs.name);
|
|
6790
|
+
const localTasks = new Map(
|
|
6791
|
+
localDb.prepare("SELECT local_id, title, status FROM tasks WHERE workstream_id = ?").all(localWs.id).map((t) => [t.local_id, t])
|
|
6792
|
+
);
|
|
6793
|
+
const tasks = [];
|
|
6794
|
+
const conflicts = [];
|
|
6795
|
+
for (const task of listReplayTasks(sidecarDb, sidecarWs.id)) {
|
|
6796
|
+
const local = localTasks.get(task.localId);
|
|
6797
|
+
if (!local) tasks.push(task);
|
|
6798
|
+
else if (local.title !== task.title || local.status !== task.status) {
|
|
6799
|
+
conflicts.push({
|
|
6800
|
+
localId: task.localId,
|
|
6801
|
+
local: { title: local.title, status: local.status },
|
|
6802
|
+
sidecar: { title: task.title, status: task.status }
|
|
6803
|
+
});
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
const localNoteHashes = new Set(listReplayNotes(localDb, localWs.id).map((n) => n.hash));
|
|
6807
|
+
const localEdges = new Set(listReplayEdges(localDb, localWs.id).map(edgeKey));
|
|
6808
|
+
return {
|
|
6809
|
+
sourceFile,
|
|
6810
|
+
workstream: sidecarWs.name,
|
|
6811
|
+
tasks,
|
|
6812
|
+
notes: listReplayNotes(sidecarDb, sidecarWs.id).filter((n) => !localNoteHashes.has(n.hash)),
|
|
6813
|
+
edges: listReplayEdges(sidecarDb, sidecarWs.id).filter((e) => !localEdges.has(edgeKey(e))),
|
|
6814
|
+
conflicts
|
|
6815
|
+
};
|
|
6816
|
+
}
|
|
6817
|
+
function applyReplayPlan(db, plan, selectedTaskIds, selectedNoteIds, replayAllEdges) {
|
|
6818
|
+
const warnings = [];
|
|
6819
|
+
const added = db.transaction(() => {
|
|
6820
|
+
const wsId = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(plan.workstream)?.id;
|
|
6821
|
+
if (wsId === void 0) throw new DbReplayWorkstreamMissingError(plan.workstream);
|
|
6822
|
+
const taskIds = new Set(selectedTaskIds);
|
|
6823
|
+
const noteTaskIds = /* @__PURE__ */ new Set([...selectedNoteIds, ...taskIds]);
|
|
6824
|
+
let tasks = 0;
|
|
6825
|
+
let notes = 0;
|
|
6826
|
+
let edges = 0;
|
|
6827
|
+
const insertTask = db.prepare(
|
|
6828
|
+
`INSERT OR IGNORE INTO tasks (workstream_id, local_id, title, status, impact, effort_days, created_at, updated_at)
|
|
6829
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
6830
|
+
);
|
|
6831
|
+
for (const task of plan.tasks) {
|
|
6832
|
+
if (!taskIds.has(task.localId)) continue;
|
|
6833
|
+
const result = insertTask.run(
|
|
6834
|
+
wsId,
|
|
6835
|
+
task.localId,
|
|
6836
|
+
task.title,
|
|
6837
|
+
task.status,
|
|
6838
|
+
task.impact,
|
|
6839
|
+
task.effortDays,
|
|
6840
|
+
task.createdAt,
|
|
6841
|
+
task.updatedAt
|
|
6842
|
+
);
|
|
6843
|
+
if (result.changes > 0) tasks += 1;
|
|
6844
|
+
}
|
|
6845
|
+
const existingNoteHashes = new Set(listReplayNotes(db, wsId).map((n) => n.hash));
|
|
6846
|
+
const insertNote = db.prepare(
|
|
6847
|
+
`INSERT INTO task_notes (task_id, author, content, created_at)
|
|
6848
|
+
SELECT id, ?, ?, ? FROM tasks WHERE workstream_id = ? AND local_id = ?`
|
|
6849
|
+
);
|
|
6850
|
+
for (const note of plan.notes) {
|
|
6851
|
+
if (!noteTaskIds.has(note.taskLocalId) || existingNoteHashes.has(note.hash)) continue;
|
|
6852
|
+
const result = insertNote.run(
|
|
6853
|
+
note.author,
|
|
6854
|
+
note.content,
|
|
6855
|
+
note.createdAt,
|
|
6856
|
+
wsId,
|
|
6857
|
+
note.taskLocalId
|
|
6858
|
+
);
|
|
6859
|
+
if (result.changes > 0) {
|
|
6860
|
+
notes += 1;
|
|
6861
|
+
existingNoteHashes.add(note.hash);
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
const insertEdge = db.prepare(
|
|
6865
|
+
`INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
|
|
6866
|
+
SELECT f.id, t.id, ?
|
|
6867
|
+
FROM tasks f, tasks t
|
|
6868
|
+
WHERE f.workstream_id = ? AND f.local_id = ?
|
|
6869
|
+
AND t.workstream_id = ? AND t.local_id = ?`
|
|
6870
|
+
);
|
|
6871
|
+
for (const edge of plan.edges) {
|
|
6872
|
+
if (!replayAllEdges && !taskIds.has(edge.fromLocalId) && !taskIds.has(edge.toLocalId)) {
|
|
6873
|
+
continue;
|
|
6874
|
+
}
|
|
6875
|
+
if (!hasTask(db, wsId, edge.fromLocalId) || !hasTask(db, wsId, edge.toLocalId)) {
|
|
6876
|
+
warnings.push(
|
|
6877
|
+
`skipped edge ${edge.fromLocalId} -> ${edge.toLocalId}: one endpoint is missing locally`
|
|
6878
|
+
);
|
|
6879
|
+
continue;
|
|
6880
|
+
}
|
|
6881
|
+
const result = insertEdge.run(edge.createdAt, wsId, edge.fromLocalId, wsId, edge.toLocalId);
|
|
6882
|
+
if (result.changes > 0) edges += 1;
|
|
6883
|
+
}
|
|
6884
|
+
return { tasks, notes, edges };
|
|
6885
|
+
})();
|
|
6886
|
+
return { added, warnings };
|
|
6887
|
+
}
|
|
6888
|
+
function replayResult(plan, dryRun, applied) {
|
|
6889
|
+
return { ...plan, dryRun, applied, added: { tasks: 0, notes: 0, edges: 0 }, warnings: [] };
|
|
6890
|
+
}
|
|
6891
|
+
function listLocalWorkstreams(db) {
|
|
6892
|
+
return db.prepare("SELECT id, name FROM workstreams ORDER BY name").all();
|
|
6893
|
+
}
|
|
6894
|
+
function listReplayTasks(db, wsId) {
|
|
6895
|
+
return db.prepare(
|
|
6896
|
+
`SELECT local_id, title, status, impact, effort_days, created_at, updated_at
|
|
6897
|
+
FROM tasks
|
|
6898
|
+
WHERE workstream_id = ?
|
|
6899
|
+
ORDER BY local_id`
|
|
6900
|
+
).all(wsId).map((row) => ({
|
|
6901
|
+
localId: row.local_id,
|
|
6902
|
+
title: row.title,
|
|
6903
|
+
status: row.status,
|
|
6904
|
+
impact: row.impact,
|
|
6905
|
+
effortDays: row.effort_days,
|
|
6906
|
+
createdAt: row.created_at,
|
|
6907
|
+
updatedAt: row.updated_at
|
|
6908
|
+
}));
|
|
6909
|
+
}
|
|
6910
|
+
function listReplayNotes(db, wsId) {
|
|
6911
|
+
const rows = db.prepare(
|
|
6912
|
+
`SELECT t.local_id AS taskLocalId, n.author, n.content, n.created_at AS createdAt
|
|
6913
|
+
FROM task_notes n
|
|
6914
|
+
JOIN tasks t ON t.id = n.task_id
|
|
6915
|
+
WHERE t.workstream_id = ?
|
|
6916
|
+
ORDER BY n.created_at, n.id`
|
|
6917
|
+
).all(wsId);
|
|
6918
|
+
return rows.map((row) => ({ ...row, hash: noteHash(row) }));
|
|
6919
|
+
}
|
|
6920
|
+
function listReplayEdges(db, wsId) {
|
|
6921
|
+
return db.prepare(
|
|
6922
|
+
`SELECT f.local_id AS fromLocalId, t.local_id AS toLocalId, e.created_at AS createdAt
|
|
6923
|
+
FROM task_edges e
|
|
6924
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
6925
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
6926
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?
|
|
6927
|
+
ORDER BY f.local_id, t.local_id`
|
|
6928
|
+
).all(wsId, wsId);
|
|
6929
|
+
}
|
|
6930
|
+
function noteHash(note) {
|
|
6931
|
+
return createHash2("sha256").update(`${note.taskLocalId}\0${note.content}\0${note.createdAt}`).digest("hex");
|
|
6932
|
+
}
|
|
6933
|
+
function edgeKey(edge) {
|
|
6934
|
+
return `${edge.fromLocalId}\0${edge.toLocalId}`;
|
|
6935
|
+
}
|
|
6936
|
+
function hasTask(db, wsId, localId) {
|
|
6937
|
+
return db.prepare("SELECT 1 FROM tasks WHERE workstream_id = ? AND local_id = ?").get(wsId, localId) !== void 0;
|
|
6938
|
+
}
|
|
6939
|
+
function shellQuote2(s) {
|
|
6940
|
+
return `'${s.replace(/'/g, `'"'"'`)}'`;
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6943
|
+
// src/db-sync.ts
|
|
6944
|
+
var DbExportTargetExistsError = class extends Error {
|
|
6945
|
+
constructor(file) {
|
|
6946
|
+
super(`DB export target already exists: ${file}`);
|
|
6947
|
+
this.file = file;
|
|
6948
|
+
}
|
|
6949
|
+
file;
|
|
6950
|
+
name = "DbExportTargetExistsError";
|
|
6951
|
+
errorNextSteps() {
|
|
6952
|
+
return [
|
|
6953
|
+
{ intent: "Choose a different target", command: "mu db export <new-file>" },
|
|
6954
|
+
{ intent: "Overwrite this target", command: `mu db export ${shellQuote3(this.file)} --force` }
|
|
6955
|
+
];
|
|
6956
|
+
}
|
|
6957
|
+
};
|
|
6958
|
+
var DbImportManifestMissingError = class extends Error {
|
|
6959
|
+
constructor(manifestPath) {
|
|
6960
|
+
super(`DB import manifest not found: ${manifestPath}`);
|
|
6961
|
+
this.manifestPath = manifestPath;
|
|
6962
|
+
}
|
|
6963
|
+
manifestPath;
|
|
6964
|
+
name = "DbImportManifestMissingError";
|
|
6965
|
+
errorNextSteps() {
|
|
6966
|
+
return [
|
|
6967
|
+
{ intent: "Export the DB with its sidecar", command: "mu db export /tmp/mu.db --force" },
|
|
6968
|
+
{ intent: "Copy the sidecar too", command: `scp <host>:${shellQuote3(this.manifestPath)} .` }
|
|
6969
|
+
];
|
|
6970
|
+
}
|
|
6971
|
+
};
|
|
6972
|
+
var DbImportSchemaTooOldError = class extends Error {
|
|
6973
|
+
constructor(sourceVersion) {
|
|
6974
|
+
super(
|
|
6975
|
+
`source DB schema v${sourceVersion} is older than local mu requires (v${CURRENT_SCHEMA_VERSION})`
|
|
6976
|
+
);
|
|
6977
|
+
this.sourceVersion = sourceVersion;
|
|
6978
|
+
}
|
|
6979
|
+
sourceVersion;
|
|
6980
|
+
name = "DbImportSchemaTooOldError";
|
|
6981
|
+
errorNextSteps() {
|
|
6982
|
+
return [
|
|
6983
|
+
{
|
|
6984
|
+
intent: "Upgrade mu on the source machine",
|
|
6985
|
+
command: "npm run build && mu db export <file> --force"
|
|
6986
|
+
},
|
|
6987
|
+
{ intent: "Then retry this import", command: "mu db import <file> --apply" }
|
|
6988
|
+
];
|
|
6989
|
+
}
|
|
6990
|
+
};
|
|
6991
|
+
var DbImportSchemaTooNewError = class extends Error {
|
|
6992
|
+
constructor(sourceVersion) {
|
|
6993
|
+
super(
|
|
6994
|
+
`source DB schema v${sourceVersion} is newer than this mu supports (v${CURRENT_SCHEMA_VERSION}); upgrade local mu`
|
|
6995
|
+
);
|
|
6996
|
+
this.sourceVersion = sourceVersion;
|
|
6997
|
+
}
|
|
6998
|
+
sourceVersion;
|
|
6999
|
+
name = "DbImportSchemaTooNewError";
|
|
7000
|
+
errorNextSteps() {
|
|
7001
|
+
return [
|
|
7002
|
+
{ intent: "Upgrade local mu", command: "git pull && npm install && npm run build" },
|
|
7003
|
+
{ intent: "Then retry this import", command: "mu db import <file> --apply" }
|
|
7004
|
+
];
|
|
7005
|
+
}
|
|
7006
|
+
};
|
|
7007
|
+
var DbImportSourceStaleError = class extends Error {
|
|
7008
|
+
constructor(workstreams) {
|
|
7009
|
+
super(`source DB is stale for local-ahead workstream(s): ${workstreams.join(", ")}`);
|
|
7010
|
+
this.workstreams = workstreams;
|
|
7011
|
+
}
|
|
7012
|
+
workstreams;
|
|
7013
|
+
name = "DbImportSourceStaleError";
|
|
7014
|
+
errorNextSteps() {
|
|
7015
|
+
return [
|
|
7016
|
+
{ intent: "Re-export from this machine", command: "mu db export /tmp/mu-fresh.db --force" },
|
|
7017
|
+
{ intent: "Dry-run the incoming file first", command: "mu db import <file>" }
|
|
7018
|
+
];
|
|
7019
|
+
}
|
|
7020
|
+
};
|
|
7021
|
+
var DbImportConflictError = class extends Error {
|
|
7022
|
+
constructor(workstreams) {
|
|
7023
|
+
super(`source and local both advanced for workstream(s): ${workstreams.join(", ")}`);
|
|
7024
|
+
this.workstreams = workstreams;
|
|
7025
|
+
}
|
|
7026
|
+
workstreams;
|
|
7027
|
+
name = "DbImportConflictError";
|
|
7028
|
+
errorNextSteps() {
|
|
7029
|
+
return [
|
|
7030
|
+
{ intent: "Preview the conflicting workstreams", command: "mu db import <file> --json" },
|
|
7031
|
+
{
|
|
7032
|
+
intent: "Clobber from source after parking local divergence",
|
|
7033
|
+
command: "mu db import <file> --apply --force-source"
|
|
7034
|
+
}
|
|
7035
|
+
];
|
|
7036
|
+
}
|
|
7037
|
+
};
|
|
7038
|
+
function exportDb(db, file, opts = {}) {
|
|
7039
|
+
const target = file;
|
|
7040
|
+
const manifestPath = `${target}.manifest.json`;
|
|
7041
|
+
const targetExists = existsSync12(target);
|
|
7042
|
+
if (targetExists && opts.force !== true) throw new DbExportTargetExistsError(target);
|
|
7043
|
+
const preEventManifest = buildExportManifest(db);
|
|
7044
|
+
for (const ws of preEventManifest.workstreams) {
|
|
7045
|
+
emitEvent(db, ws.name, `db export ${ws.name} seq=${ws.latestSeq}`);
|
|
7046
|
+
}
|
|
7047
|
+
const manifest = buildExportManifest(db);
|
|
7048
|
+
mkdirSync4(dirname5(target), { recursive: true });
|
|
7049
|
+
try {
|
|
7050
|
+
if (targetExists) unlinkSync4(target);
|
|
7051
|
+
db.exec(`VACUUM INTO ${quoteSqlString2(target)}`);
|
|
7052
|
+
writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
7053
|
+
`, "utf8");
|
|
7054
|
+
} catch (err) {
|
|
7055
|
+
try {
|
|
7056
|
+
if (existsSync12(target)) unlinkSync4(target);
|
|
7057
|
+
} catch {
|
|
7058
|
+
}
|
|
7059
|
+
throw err;
|
|
7060
|
+
}
|
|
7061
|
+
return { file: target, manifestPath, manifest, overwritten: targetExists };
|
|
7062
|
+
}
|
|
7063
|
+
function importDb(db, file, opts = {}) {
|
|
7064
|
+
const manifest = readImportManifest(file);
|
|
7065
|
+
assertImportSchemaCompatible(manifest.schemaVersion);
|
|
7066
|
+
const sourceDb = openDb({ path: file, readonly: true });
|
|
7067
|
+
try {
|
|
7068
|
+
const summary = buildImportPlan(db, manifest, file, opts.onlyWorkstreams);
|
|
7069
|
+
if (opts.apply !== true) {
|
|
7070
|
+
return {
|
|
7071
|
+
machineId: manifest.machineId,
|
|
7072
|
+
sourceFile: file,
|
|
7073
|
+
dryRun: true,
|
|
7074
|
+
applied: false,
|
|
7075
|
+
summary
|
|
7076
|
+
};
|
|
7077
|
+
}
|
|
7078
|
+
const stale = summary.filter((s) => s.decision === "LOCAL_AHEAD").map((s) => s.workstream);
|
|
7079
|
+
if (stale.length > 0) throw new DbImportSourceStaleError(stale);
|
|
7080
|
+
const conflicts = summary.filter((s) => s.decision === "CONFLICT").map((s) => s.workstream);
|
|
7081
|
+
if (conflicts.length > 0 && opts.forceSource !== true)
|
|
7082
|
+
throw new DbImportConflictError(conflicts);
|
|
7083
|
+
const mutating = summary.some((s) => shouldReplace(s.decision, opts.forceSource === true));
|
|
7084
|
+
const snapshot = mutating ? captureSnapshot(db, `db import ${file}`, null) : void 0;
|
|
7085
|
+
for (const item of summary) {
|
|
7086
|
+
if (!shouldReplace(item.decision, opts.forceSource === true)) continue;
|
|
7087
|
+
if (item.decision === "CONFLICT") {
|
|
7088
|
+
item.parkPath = parkLocalWorkstream(db, item.workstream);
|
|
7089
|
+
}
|
|
7090
|
+
const sourceWs = manifest.workstreams.find((w) => w.name === item.workstream);
|
|
7091
|
+
const sourceSeq = sourceWs?.latestSeq ?? 0;
|
|
7092
|
+
replaceWorkstreamFromSource(db, sourceDb, item.workstream, manifest.machineId, sourceSeq);
|
|
7093
|
+
}
|
|
7094
|
+
return {
|
|
7095
|
+
machineId: manifest.machineId,
|
|
7096
|
+
sourceFile: file,
|
|
7097
|
+
dryRun: false,
|
|
7098
|
+
applied: true,
|
|
7099
|
+
...snapshot ? { snapshotId: snapshot.id } : {},
|
|
7100
|
+
summary
|
|
7101
|
+
};
|
|
7102
|
+
} finally {
|
|
7103
|
+
sourceDb.close();
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
function buildImportPlan(localDb, manifest, sourceFile, onlyWorkstreams) {
|
|
7107
|
+
const sourceByName = new Map(manifest.workstreams.map((w) => [w.name, w]));
|
|
7108
|
+
const localByName = new Map(listLocalWorkstreams2(localDb).map((w) => [w.name, w]));
|
|
7109
|
+
const localMachineId = getMachineIdentity(localDb)?.machine_id ?? "";
|
|
7110
|
+
const only = normaliseOnlyWorkstreams(onlyWorkstreams);
|
|
7111
|
+
const names = Array.from(/* @__PURE__ */ new Set([...sourceByName.keys(), ...localByName.keys()])).filter((name) => only.size === 0 || only.has(name)).sort();
|
|
7112
|
+
return names.map((name) => {
|
|
7113
|
+
const source = sourceByName.get(name);
|
|
7114
|
+
const local = localByName.get(name);
|
|
7115
|
+
const sourceSeq = source?.latestSeq ?? 0;
|
|
7116
|
+
const localSeq = local ? latestSeq(localDb, local.id) : 0;
|
|
7117
|
+
const synced = source !== void 0 && local !== void 0 && manifest.machineId === localMachineId ? { sourceSeq: Math.min(sourceSeq, localSeq), localSeq: Math.min(sourceSeq, localSeq) } : local ? lastKnownPeerSync(localDb, local.id, manifest.machineId) : { sourceSeq: 0, localSeq: 0 };
|
|
7118
|
+
const decision = classifyWorkstream({
|
|
7119
|
+
hasSource: source !== void 0,
|
|
7120
|
+
hasLocal: local !== void 0,
|
|
7121
|
+
sourceSeq,
|
|
7122
|
+
localSeq,
|
|
7123
|
+
syncedSourceSeq: synced.sourceSeq,
|
|
7124
|
+
syncedLocalSeq: synced.localSeq
|
|
7125
|
+
});
|
|
7126
|
+
return {
|
|
7127
|
+
workstream: name,
|
|
7128
|
+
decision,
|
|
7129
|
+
delta: {
|
|
7130
|
+
sourceFile,
|
|
7131
|
+
sourceSeq,
|
|
7132
|
+
localSeq,
|
|
7133
|
+
lastSynced: synced.sourceSeq,
|
|
7134
|
+
localSynced: synced.localSeq,
|
|
7135
|
+
source: source ? countsFromManifest(source) : null,
|
|
7136
|
+
local: local ? countWorkstream(localDb, local.id) : null
|
|
7137
|
+
},
|
|
7138
|
+
...decision === "LOCAL_AHEAD" ? { needs: "re-export from this machine" } : {},
|
|
7139
|
+
...decision === "CONFLICT" ? { needs: "--force-source" } : {}
|
|
7140
|
+
};
|
|
7141
|
+
});
|
|
7142
|
+
}
|
|
7143
|
+
function classifyWorkstream(opts) {
|
|
7144
|
+
if (opts.hasSource && !opts.hasLocal) return "IMPORT";
|
|
7145
|
+
if (!opts.hasSource && opts.hasLocal)
|
|
7146
|
+
return opts.syncedSourceSeq > 0 || opts.syncedLocalSeq > 0 ? "LOCAL_AHEAD" : "LEAVE_ALONE";
|
|
7147
|
+
if (!opts.hasSource && !opts.hasLocal) return "IDENTICAL";
|
|
7148
|
+
const sourceAdvanced = opts.sourceSeq > opts.syncedSourceSeq;
|
|
7149
|
+
const localAdvanced = opts.localSeq > opts.syncedLocalSeq;
|
|
7150
|
+
if (!sourceAdvanced && !localAdvanced) return "IDENTICAL";
|
|
7151
|
+
if (sourceAdvanced && !localAdvanced) return "FAST_FORWARD";
|
|
7152
|
+
if (!sourceAdvanced && localAdvanced) return "LOCAL_AHEAD";
|
|
7153
|
+
return "CONFLICT";
|
|
7154
|
+
}
|
|
7155
|
+
function shouldReplace(decision, forceSource) {
|
|
7156
|
+
return decision === "FAST_FORWARD" || decision === "IMPORT" || decision === "CONFLICT" && forceSource;
|
|
7157
|
+
}
|
|
7158
|
+
function replaceWorkstreamFromSource(localDb, sourceDb, workstream, sourceMachineId, sourceSeq) {
|
|
7159
|
+
localDb.transaction(() => {
|
|
7160
|
+
const existing = localDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream);
|
|
7161
|
+
if (existing) {
|
|
7162
|
+
localDb.prepare("DELETE FROM vcs_workspaces WHERE workstream_id = ?").run(existing.id);
|
|
7163
|
+
localDb.prepare("DELETE FROM agents WHERE workstream_id = ?").run(existing.id);
|
|
7164
|
+
localDb.prepare("DELETE FROM workstreams WHERE id = ?").run(existing.id);
|
|
7165
|
+
}
|
|
7166
|
+
copyWorkstreamRows(sourceDb, localDb, workstream, {
|
|
7167
|
+
includeMachineLocalRows: false,
|
|
7168
|
+
preserveLogSeq: false,
|
|
7169
|
+
includeSync: false
|
|
7170
|
+
});
|
|
7171
|
+
const wsId = localDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream)?.id;
|
|
7172
|
+
if (wsId === void 0) throw new Error(`importDb: failed to import workstream ${workstream}`);
|
|
7173
|
+
writeSyncState(localDb, wsId, sourceMachineId, sourceSeq);
|
|
7174
|
+
})();
|
|
7175
|
+
}
|
|
7176
|
+
function parkLocalWorkstream(db, workstream) {
|
|
7177
|
+
const dir = join8(defaultStateDir(), "divergence");
|
|
7178
|
+
mkdirSync4(dir, { recursive: true });
|
|
7179
|
+
const path = join8(
|
|
7180
|
+
dir,
|
|
7181
|
+
`${workstream}-${(/* @__PURE__ */ new Date()).toISOString()}-${randomUUID2().slice(0, 8)}.db`
|
|
7182
|
+
);
|
|
7183
|
+
const parkDb = openDb({ path });
|
|
7184
|
+
try {
|
|
7185
|
+
const identity = getMachineIdentity(db);
|
|
7186
|
+
if (identity) {
|
|
7187
|
+
parkDb.prepare(
|
|
7188
|
+
`UPDATE machine_identity
|
|
7189
|
+
SET machine_id = ?, hostname = ?, created_at = ?
|
|
7190
|
+
WHERE id = 1`
|
|
7191
|
+
).run(
|
|
7192
|
+
identity.machine_id,
|
|
7193
|
+
identity.hostname,
|
|
7194
|
+
identity.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
7195
|
+
);
|
|
7196
|
+
}
|
|
7197
|
+
copyWorkstreamRows(db, parkDb, workstream, {
|
|
7198
|
+
includeMachineLocalRows: true,
|
|
7199
|
+
preserveLogSeq: true,
|
|
7200
|
+
includeSync: true
|
|
7201
|
+
});
|
|
7202
|
+
} catch (err) {
|
|
7203
|
+
try {
|
|
7204
|
+
parkDb.close();
|
|
7205
|
+
} catch {
|
|
7206
|
+
}
|
|
7207
|
+
try {
|
|
7208
|
+
if (existsSync12(path)) unlinkSync4(path);
|
|
7209
|
+
} catch {
|
|
7210
|
+
}
|
|
7211
|
+
throw err;
|
|
7212
|
+
}
|
|
7213
|
+
parkDb.close();
|
|
7214
|
+
return path;
|
|
7215
|
+
}
|
|
7216
|
+
function copyWorkstreamRows(sourceDb, targetDb, workstream, opts) {
|
|
7217
|
+
const sourceWs = sourceDb.prepare("SELECT id, name, created_at FROM workstreams WHERE name = ?").get(workstream);
|
|
7218
|
+
if (!sourceWs) throw new Error(`copyWorkstreamRows: no such workstream ${workstream}`);
|
|
7219
|
+
targetDb.prepare("INSERT INTO workstreams (name, created_at) VALUES (?, ?)").run(sourceWs.name, sourceWs.created_at);
|
|
7220
|
+
const targetWsId = targetDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream).id;
|
|
7221
|
+
if (opts.includeMachineLocalRows) copyAgents(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
7222
|
+
copyTasks(sourceDb, targetDb, sourceWs.id, targetWsId, opts.includeMachineLocalRows);
|
|
7223
|
+
copyEdges(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
7224
|
+
copyNotes(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
7225
|
+
copyLogs(sourceDb, targetDb, sourceWs.id, targetWsId, opts.preserveLogSeq);
|
|
7226
|
+
if (opts.includeMachineLocalRows) copyWorkspaces(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
7227
|
+
if (opts.includeSync) copySync(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
7228
|
+
}
|
|
7229
|
+
function copyAgents(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
7230
|
+
const rows = sourceDb.prepare(
|
|
7231
|
+
`SELECT name, cli, pane_id, status, role, tab, created_at, updated_at
|
|
7232
|
+
FROM agents
|
|
7233
|
+
WHERE workstream_id = ?
|
|
7234
|
+
ORDER BY id`
|
|
7235
|
+
).all(sourceWsId);
|
|
7236
|
+
const insert = targetDb.prepare(
|
|
7237
|
+
`INSERT INTO agents (workstream_id, name, cli, pane_id, status, role, tab, created_at, updated_at)
|
|
7238
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7239
|
+
);
|
|
7240
|
+
for (const row of rows) {
|
|
7241
|
+
insert.run(
|
|
7242
|
+
targetWsId,
|
|
7243
|
+
row.name,
|
|
7244
|
+
row.cli,
|
|
7245
|
+
row.pane_id,
|
|
7246
|
+
row.status,
|
|
7247
|
+
row.role,
|
|
7248
|
+
row.tab,
|
|
7249
|
+
row.created_at,
|
|
7250
|
+
row.updated_at
|
|
7251
|
+
);
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
function copyTasks(sourceDb, targetDb, sourceWsId, targetWsId, includeOwners) {
|
|
7255
|
+
const rows = sourceDb.prepare(
|
|
7256
|
+
`SELECT t.local_id, t.title, t.status, t.impact, t.effort_days, a.name AS owner_name,
|
|
7257
|
+
t.created_at, t.updated_at
|
|
7258
|
+
FROM tasks t
|
|
7259
|
+
LEFT JOIN agents a ON a.id = t.owner_id
|
|
7260
|
+
WHERE t.workstream_id = ?
|
|
7261
|
+
ORDER BY t.id`
|
|
7262
|
+
).all(sourceWsId);
|
|
7263
|
+
const ownerLookup = targetDb.prepare(
|
|
7264
|
+
"SELECT id FROM agents WHERE workstream_id = ? AND name = ?"
|
|
7265
|
+
);
|
|
7266
|
+
const insert = targetDb.prepare(
|
|
7267
|
+
`INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
|
|
7268
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7269
|
+
);
|
|
7270
|
+
for (const row of rows) {
|
|
7271
|
+
const ownerId = includeOwners && row.owner_name !== null ? ownerLookup.get(targetWsId, row.owner_name)?.id ?? null : null;
|
|
7272
|
+
insert.run(
|
|
7273
|
+
targetWsId,
|
|
7274
|
+
row.local_id,
|
|
7275
|
+
row.title,
|
|
7276
|
+
row.status,
|
|
7277
|
+
row.impact,
|
|
7278
|
+
row.effort_days,
|
|
7279
|
+
ownerId,
|
|
7280
|
+
row.created_at,
|
|
7281
|
+
row.updated_at
|
|
7282
|
+
);
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
function copyEdges(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
7286
|
+
const rows = sourceDb.prepare(
|
|
7287
|
+
`SELECT f.local_id AS from_local_id, t.local_id AS to_local_id, e.created_at
|
|
7288
|
+
FROM task_edges e
|
|
7289
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
7290
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
7291
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?
|
|
7292
|
+
ORDER BY e.created_at, f.local_id, t.local_id`
|
|
7293
|
+
).all(sourceWsId, sourceWsId);
|
|
7294
|
+
const insert = targetDb.prepare(
|
|
7295
|
+
`INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
|
|
7296
|
+
SELECT f.id, t.id, ?
|
|
7297
|
+
FROM tasks f, tasks t
|
|
7298
|
+
WHERE f.workstream_id = ? AND f.local_id = ?
|
|
7299
|
+
AND t.workstream_id = ? AND t.local_id = ?`
|
|
7300
|
+
);
|
|
7301
|
+
for (const row of rows) {
|
|
7302
|
+
insert.run(row.created_at, targetWsId, row.from_local_id, targetWsId, row.to_local_id);
|
|
7303
|
+
}
|
|
7304
|
+
}
|
|
7305
|
+
function copyNotes(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
7306
|
+
const rows = sourceDb.prepare(
|
|
7307
|
+
`SELECT t.local_id AS task_local_id, n.author, n.content, n.created_at
|
|
7308
|
+
FROM task_notes n
|
|
7309
|
+
JOIN tasks t ON t.id = n.task_id
|
|
7310
|
+
WHERE t.workstream_id = ?
|
|
7311
|
+
ORDER BY n.id`
|
|
7312
|
+
).all(sourceWsId);
|
|
7313
|
+
const insert = targetDb.prepare(
|
|
7314
|
+
`INSERT INTO task_notes (task_id, author, content, created_at)
|
|
7315
|
+
SELECT id, ?, ?, ? FROM tasks WHERE workstream_id = ? AND local_id = ?`
|
|
7316
|
+
);
|
|
7317
|
+
for (const row of rows) {
|
|
7318
|
+
insert.run(row.author, row.content, row.created_at, targetWsId, row.task_local_id);
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
function copyLogs(sourceDb, targetDb, sourceWsId, targetWsId, preserveSeq) {
|
|
7322
|
+
const rows = sourceDb.prepare(
|
|
7323
|
+
`SELECT seq, source, kind, payload, created_at
|
|
7324
|
+
FROM agent_logs
|
|
7325
|
+
WHERE workstream_id = ?
|
|
7326
|
+
ORDER BY seq`
|
|
7327
|
+
).all(sourceWsId);
|
|
7328
|
+
const insertPreserve = targetDb.prepare(
|
|
7329
|
+
"INSERT INTO agent_logs (seq, workstream_id, source, kind, payload, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
7330
|
+
);
|
|
7331
|
+
const insertRenumber = targetDb.prepare(
|
|
7332
|
+
"INSERT INTO agent_logs (workstream_id, source, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
7333
|
+
);
|
|
7334
|
+
for (const row of rows) {
|
|
7335
|
+
if (preserveSeq) {
|
|
7336
|
+
insertPreserve.run(row.seq, targetWsId, row.source, row.kind, row.payload, row.created_at);
|
|
7337
|
+
} else {
|
|
7338
|
+
insertRenumber.run(targetWsId, row.source, row.kind, row.payload, row.created_at);
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
function copyWorkspaces(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
7343
|
+
const rows = sourceDb.prepare(
|
|
7344
|
+
`SELECT a.name AS agent_name, v.backend, v.path, v.parent_ref, v.created_at
|
|
7345
|
+
FROM vcs_workspaces v
|
|
7346
|
+
JOIN agents a ON a.id = v.agent_id
|
|
7347
|
+
WHERE v.workstream_id = ?
|
|
7348
|
+
ORDER BY v.id`
|
|
7349
|
+
).all(sourceWsId);
|
|
7350
|
+
const agentLookup = targetDb.prepare(
|
|
7351
|
+
"SELECT id FROM agents WHERE workstream_id = ? AND name = ?"
|
|
7352
|
+
);
|
|
7353
|
+
const insert = targetDb.prepare(
|
|
7354
|
+
`INSERT INTO vcs_workspaces (agent_id, workstream_id, backend, path, parent_ref, created_at)
|
|
7355
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
7356
|
+
);
|
|
7357
|
+
for (const row of rows) {
|
|
7358
|
+
const agentId = agentLookup.get(targetWsId, row.agent_name)?.id;
|
|
7359
|
+
if (agentId === void 0) continue;
|
|
7360
|
+
insert.run(agentId, targetWsId, row.backend, row.path, row.parent_ref, row.created_at);
|
|
7361
|
+
}
|
|
7362
|
+
}
|
|
7363
|
+
function copySync(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
7364
|
+
const row = sourceDb.prepare("SELECT last_known_peer_seqs FROM workstream_sync WHERE workstream_id = ?").get(sourceWsId);
|
|
7365
|
+
if (!row) return;
|
|
7366
|
+
targetDb.prepare("INSERT INTO workstream_sync (workstream_id, last_known_peer_seqs) VALUES (?, ?)").run(targetWsId, row.last_known_peer_seqs);
|
|
7367
|
+
}
|
|
7368
|
+
function writeSyncState(db, workstreamId, sourceMachineId, sourceSeq) {
|
|
7369
|
+
const localSeq = latestSeq(db, workstreamId);
|
|
7370
|
+
const peers = {
|
|
7371
|
+
[sourceMachineId]: sourceSeq,
|
|
7372
|
+
[localSeqKey(sourceMachineId)]: localSeq
|
|
7373
|
+
};
|
|
7374
|
+
db.prepare(
|
|
7375
|
+
`INSERT OR REPLACE INTO workstream_sync (workstream_id, last_known_peer_seqs)
|
|
7376
|
+
VALUES (?, ?)`
|
|
7377
|
+
).run(workstreamId, JSON.stringify(peers));
|
|
7378
|
+
}
|
|
7379
|
+
function lastKnownPeerSync(db, workstreamId, machineId) {
|
|
7380
|
+
const row = db.prepare("SELECT last_known_peer_seqs FROM workstream_sync WHERE workstream_id = ?").get(workstreamId);
|
|
7381
|
+
if (!row) return { sourceSeq: 0, localSeq: 0 };
|
|
7382
|
+
const parsed = parsePeerSeqs(row.last_known_peer_seqs);
|
|
7383
|
+
const sourceSeq = parsed[machineId] ?? 0;
|
|
7384
|
+
return { sourceSeq, localSeq: parsed[localSeqKey(machineId)] ?? sourceSeq };
|
|
7385
|
+
}
|
|
7386
|
+
function localSeqKey(machineId) {
|
|
7387
|
+
return `${machineId}:local`;
|
|
7388
|
+
}
|
|
7389
|
+
function parsePeerSeqs(raw) {
|
|
7390
|
+
try {
|
|
7391
|
+
const parsed = JSON.parse(raw);
|
|
7392
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
7393
|
+
const result = {};
|
|
7394
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
7395
|
+
if (typeof value === "number" && Number.isFinite(value)) result[key] = value;
|
|
7396
|
+
}
|
|
7397
|
+
return result;
|
|
7398
|
+
} catch {
|
|
7399
|
+
return {};
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
function readImportManifest(file) {
|
|
7403
|
+
const manifestPath = `${file}.manifest.json`;
|
|
7404
|
+
if (!existsSync12(manifestPath)) throw new DbImportManifestMissingError(manifestPath);
|
|
7405
|
+
return JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
7406
|
+
}
|
|
7407
|
+
function assertImportSchemaCompatible(sourceVersion) {
|
|
7408
|
+
if (sourceVersion < CURRENT_SCHEMA_VERSION) throw new DbImportSchemaTooOldError(sourceVersion);
|
|
7409
|
+
if (sourceVersion > CURRENT_SCHEMA_VERSION) throw new DbImportSchemaTooNewError(sourceVersion);
|
|
7410
|
+
}
|
|
7411
|
+
function buildExportManifest(db) {
|
|
7412
|
+
const identity = getMachineIdentity(db);
|
|
7413
|
+
const schemaRow = db.prepare("SELECT version FROM schema_version WHERE id = 1").get();
|
|
7414
|
+
const workstreams = listLocalWorkstreams2(db);
|
|
7415
|
+
return {
|
|
7416
|
+
muVersion: readPackageVersion(),
|
|
7417
|
+
schemaVersion: schemaRow?.version ?? CURRENT_SCHEMA_VERSION,
|
|
7418
|
+
machineId: identity?.machine_id ?? "",
|
|
7419
|
+
hostname: identity?.hostname ?? hostname2(),
|
|
7420
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7421
|
+
workstreams: workstreams.map((ws) => ({
|
|
7422
|
+
name: ws.name,
|
|
7423
|
+
tasks: count(db, "SELECT COUNT(*) AS n FROM tasks WHERE workstream_id = ?", ws.id),
|
|
7424
|
+
edges: count(
|
|
7425
|
+
db,
|
|
7426
|
+
`SELECT COUNT(*) AS n
|
|
7427
|
+
FROM task_edges e
|
|
7428
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
7429
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
7430
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?`,
|
|
7431
|
+
ws.id,
|
|
7432
|
+
ws.id
|
|
7433
|
+
),
|
|
7434
|
+
notes: count(
|
|
7435
|
+
db,
|
|
7436
|
+
`SELECT COUNT(*) AS n
|
|
7437
|
+
FROM task_notes n
|
|
7438
|
+
JOIN tasks t ON t.id = n.task_id
|
|
7439
|
+
WHERE t.workstream_id = ?`,
|
|
7440
|
+
ws.id
|
|
7441
|
+
),
|
|
7442
|
+
latestSeq: latestSeq(db, ws.id)
|
|
7443
|
+
}))
|
|
7444
|
+
};
|
|
7445
|
+
}
|
|
7446
|
+
function listLocalWorkstreams2(db) {
|
|
7447
|
+
return db.prepare("SELECT id, name FROM workstreams ORDER BY name").all();
|
|
7448
|
+
}
|
|
7449
|
+
function getMachineIdentity(db) {
|
|
7450
|
+
return db.prepare("SELECT machine_id, hostname, created_at FROM machine_identity WHERE id = 1").get();
|
|
7451
|
+
}
|
|
7452
|
+
function countWorkstream(db, wsId) {
|
|
7453
|
+
return {
|
|
7454
|
+
tasks: count(db, "SELECT COUNT(*) AS n FROM tasks WHERE workstream_id = ?", wsId),
|
|
7455
|
+
edges: count(
|
|
7456
|
+
db,
|
|
7457
|
+
`SELECT COUNT(*) AS n
|
|
7458
|
+
FROM task_edges e
|
|
7459
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
7460
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
7461
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?`,
|
|
7462
|
+
wsId,
|
|
7463
|
+
wsId
|
|
7464
|
+
),
|
|
7465
|
+
notes: count(
|
|
7466
|
+
db,
|
|
7467
|
+
`SELECT COUNT(*) AS n
|
|
7468
|
+
FROM task_notes n
|
|
7469
|
+
JOIN tasks t ON t.id = n.task_id
|
|
7470
|
+
WHERE t.workstream_id = ?`,
|
|
7471
|
+
wsId
|
|
7472
|
+
)
|
|
7473
|
+
};
|
|
7474
|
+
}
|
|
7475
|
+
function countsFromManifest(ws) {
|
|
7476
|
+
return { tasks: ws.tasks, edges: ws.edges, notes: ws.notes };
|
|
7477
|
+
}
|
|
7478
|
+
function normaliseOnlyWorkstreams(input) {
|
|
7479
|
+
if (!input || input.length === 0) return /* @__PURE__ */ new Set();
|
|
7480
|
+
return new Set(
|
|
7481
|
+
input.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0)
|
|
7482
|
+
);
|
|
7483
|
+
}
|
|
7484
|
+
function count(db, sql, ...params) {
|
|
7485
|
+
const row = db.prepare(sql).get(...params);
|
|
7486
|
+
return row?.n ?? 0;
|
|
7487
|
+
}
|
|
7488
|
+
function quoteSqlString2(s) {
|
|
7489
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
7490
|
+
}
|
|
7491
|
+
function readPackageVersion() {
|
|
7492
|
+
try {
|
|
7493
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
7494
|
+
const raw = readFileSync2(join8(here, "..", "package.json"), "utf8");
|
|
7495
|
+
const parsed = JSON.parse(raw);
|
|
7496
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
7497
|
+
} catch {
|
|
7498
|
+
return "unknown";
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
function shellQuote3(s) {
|
|
7502
|
+
return `'${s.replace(/'/g, `'"'"'`)}'`;
|
|
7503
|
+
}
|
|
7504
|
+
|
|
6504
7505
|
// src/tracks.ts
|
|
6505
7506
|
function getParallelTracks(db, workstream) {
|
|
6506
7507
|
const goals = listGoals(db, workstream).filter(
|
|
@@ -6603,544 +7604,6 @@ var UnionFind = class {
|
|
|
6603
7604
|
}
|
|
6604
7605
|
};
|
|
6605
7606
|
|
|
6606
|
-
// src/importing.ts
|
|
6607
|
-
import { existsSync as existsSync12, readFileSync as readFileSync2, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
6608
|
-
import { basename as basename2, dirname as dirname5, join as join8 } from "path";
|
|
6609
|
-
var ImportBucketInvalidError = class extends Error {
|
|
6610
|
-
constructor(bucketDir, reason) {
|
|
6611
|
-
super(`not a valid mu bucket export at ${bucketDir}: ${reason}`);
|
|
6612
|
-
this.bucketDir = bucketDir;
|
|
6613
|
-
this.reason = reason;
|
|
6614
|
-
}
|
|
6615
|
-
bucketDir;
|
|
6616
|
-
reason;
|
|
6617
|
-
name = "ImportBucketInvalidError";
|
|
6618
|
-
errorNextSteps() {
|
|
6619
|
-
return [
|
|
6620
|
-
{ intent: "List the directory's contents", command: `ls ${this.bucketDir}` },
|
|
6621
|
-
{
|
|
6622
|
-
intent: "Inspect the manifest (must be bucketVersion 2)",
|
|
6623
|
-
command: `cat ${this.bucketDir}/manifest.json`
|
|
6624
|
-
}
|
|
6625
|
-
];
|
|
6626
|
-
}
|
|
6627
|
-
};
|
|
6628
|
-
var ImportSourceNotInBucketError = class extends Error {
|
|
6629
|
-
constructor(bucketDir, badName, validNames) {
|
|
6630
|
-
super(
|
|
6631
|
-
`--source-ws "${badName}" is not a source-ws in bucket ${bucketDir}; valid: ${validNames.length === 0 ? "<none>" : validNames.join(", ")}`
|
|
6632
|
-
);
|
|
6633
|
-
this.bucketDir = bucketDir;
|
|
6634
|
-
this.badName = badName;
|
|
6635
|
-
this.validNames = validNames;
|
|
6636
|
-
}
|
|
6637
|
-
bucketDir;
|
|
6638
|
-
badName;
|
|
6639
|
-
validNames;
|
|
6640
|
-
name = "ImportSourceNotInBucketError";
|
|
6641
|
-
errorNextSteps() {
|
|
6642
|
-
return [
|
|
6643
|
-
{ intent: "List the bucket's source-ws subdirs", command: `ls ${this.bucketDir}` },
|
|
6644
|
-
{
|
|
6645
|
-
intent: "Inspect the bucket manifest's sources map",
|
|
6646
|
-
command: `cat ${this.bucketDir}/manifest.json | head -40`
|
|
6647
|
-
}
|
|
6648
|
-
];
|
|
6649
|
-
}
|
|
6650
|
-
};
|
|
6651
|
-
var WorkstreamAlreadyExistsError = class extends Error {
|
|
6652
|
-
constructor(workstream) {
|
|
6653
|
-
super(
|
|
6654
|
-
`workstream "${workstream}" already exists in the DB; mu workstream import refuses to merge silently. Pass --workstream <new-name> to import under a different name (single-source buckets only), or destroy the existing workstream first.`
|
|
6655
|
-
);
|
|
6656
|
-
this.workstream = workstream;
|
|
6657
|
-
}
|
|
6658
|
-
workstream;
|
|
6659
|
-
name = "WorkstreamAlreadyExistsError";
|
|
6660
|
-
errorNextSteps() {
|
|
6661
|
-
return [
|
|
6662
|
-
{
|
|
6663
|
-
intent: "Import under a new name (single-source bucket only)",
|
|
6664
|
-
command: "mu workstream import <bucket> --workstream <new-name>"
|
|
6665
|
-
},
|
|
6666
|
-
{
|
|
6667
|
-
intent: "Or destroy the existing workstream first",
|
|
6668
|
-
command: `mu workstream destroy -w ${this.workstream} --yes`
|
|
6669
|
-
}
|
|
6670
|
-
];
|
|
6671
|
-
}
|
|
6672
|
-
};
|
|
6673
|
-
var ImportFrontmatterParseError = class extends Error {
|
|
6674
|
-
constructor(path, line, raw) {
|
|
6675
|
-
super(`failed to parse frontmatter at ${path}:${line}: ${raw}`);
|
|
6676
|
-
this.path = path;
|
|
6677
|
-
this.line = line;
|
|
6678
|
-
this.raw = raw;
|
|
6679
|
-
}
|
|
6680
|
-
path;
|
|
6681
|
-
line;
|
|
6682
|
-
raw;
|
|
6683
|
-
name = "ImportFrontmatterParseError";
|
|
6684
|
-
errorNextSteps() {
|
|
6685
|
-
return [{ intent: "Inspect the offending file", command: `sed -n 1,30p ${this.path}` }];
|
|
6686
|
-
}
|
|
6687
|
-
};
|
|
6688
|
-
var ImportEdgeRefMissingError = class extends Error {
|
|
6689
|
-
constructor(fromTask, toTask, direction) {
|
|
6690
|
-
super(
|
|
6691
|
-
`task "${fromTask}" references "${toTask}" via ${direction}, but no task with that id was found in the import`
|
|
6692
|
-
);
|
|
6693
|
-
this.fromTask = fromTask;
|
|
6694
|
-
this.toTask = toTask;
|
|
6695
|
-
this.direction = direction;
|
|
6696
|
-
}
|
|
6697
|
-
fromTask;
|
|
6698
|
-
toTask;
|
|
6699
|
-
direction;
|
|
6700
|
-
name = "ImportEdgeRefMissingError";
|
|
6701
|
-
errorNextSteps() {
|
|
6702
|
-
return [
|
|
6703
|
-
{
|
|
6704
|
-
intent: "Inspect the offending task file in the bucket",
|
|
6705
|
-
command: `grep -l 'id: "${this.fromTask}"' <bucket>/*/tasks/`
|
|
6706
|
-
}
|
|
6707
|
-
];
|
|
6708
|
-
}
|
|
6709
|
-
};
|
|
6710
|
-
function unquote(raw) {
|
|
6711
|
-
const trimmed = raw.trim();
|
|
6712
|
-
if (trimmed === "null") return null;
|
|
6713
|
-
if (trimmed.length < 2 || trimmed[0] !== '"' || trimmed[trimmed.length - 1] !== '"') {
|
|
6714
|
-
return raw.trim();
|
|
6715
|
-
}
|
|
6716
|
-
const inner = trimmed.slice(1, -1);
|
|
6717
|
-
let out = "";
|
|
6718
|
-
for (let i = 0; i < inner.length; i++) {
|
|
6719
|
-
const ch = inner[i];
|
|
6720
|
-
if (ch === "\\" && i + 1 < inner.length) {
|
|
6721
|
-
const next = inner[i + 1];
|
|
6722
|
-
if (next === '"' || next === "\\") {
|
|
6723
|
-
out += next;
|
|
6724
|
-
i += 1;
|
|
6725
|
-
continue;
|
|
6726
|
-
}
|
|
6727
|
-
}
|
|
6728
|
-
if (ch !== void 0) out += ch;
|
|
6729
|
-
}
|
|
6730
|
-
return out;
|
|
6731
|
-
}
|
|
6732
|
-
function parseStringArray(raw) {
|
|
6733
|
-
const trimmed = raw.trim();
|
|
6734
|
-
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
|
|
6735
|
-
throw new Error(`expected [..] array, got ${JSON.stringify(raw)}`);
|
|
6736
|
-
}
|
|
6737
|
-
const inner = trimmed.slice(1, -1).trim();
|
|
6738
|
-
if (inner === "") return [];
|
|
6739
|
-
const out = [];
|
|
6740
|
-
let i = 0;
|
|
6741
|
-
while (i < inner.length) {
|
|
6742
|
-
while (i < inner.length && (inner[i] === " " || inner[i] === ",")) i += 1;
|
|
6743
|
-
if (i >= inner.length) break;
|
|
6744
|
-
if (inner[i] !== '"') {
|
|
6745
|
-
throw new Error(`expected quoted string in array, got ${JSON.stringify(inner.slice(i))}`);
|
|
6746
|
-
}
|
|
6747
|
-
let j = i + 1;
|
|
6748
|
-
while (j < inner.length) {
|
|
6749
|
-
if (inner[j] === "\\" && j + 1 < inner.length) {
|
|
6750
|
-
j += 2;
|
|
6751
|
-
continue;
|
|
6752
|
-
}
|
|
6753
|
-
if (inner[j] === '"') break;
|
|
6754
|
-
j += 1;
|
|
6755
|
-
}
|
|
6756
|
-
if (j >= inner.length) {
|
|
6757
|
-
throw new Error("unterminated quoted string in array");
|
|
6758
|
-
}
|
|
6759
|
-
const scalar = unquote(inner.slice(i, j + 1));
|
|
6760
|
-
if (scalar === null) {
|
|
6761
|
-
throw new Error("null is not a legal array element");
|
|
6762
|
-
}
|
|
6763
|
-
out.push(scalar);
|
|
6764
|
-
i = j + 1;
|
|
6765
|
-
}
|
|
6766
|
-
return out;
|
|
6767
|
-
}
|
|
6768
|
-
function parseTaskMarkdown(path) {
|
|
6769
|
-
const raw = readFileSync2(path, "utf8");
|
|
6770
|
-
const lines = raw.split("\n");
|
|
6771
|
-
if (lines[0] !== "---") {
|
|
6772
|
-
throw new ImportFrontmatterParseError(path, 1, lines[0] ?? "");
|
|
6773
|
-
}
|
|
6774
|
-
let end = -1;
|
|
6775
|
-
for (let i2 = 1; i2 < lines.length; i2++) {
|
|
6776
|
-
if (lines[i2] === "---") {
|
|
6777
|
-
end = i2;
|
|
6778
|
-
break;
|
|
6779
|
-
}
|
|
6780
|
-
}
|
|
6781
|
-
if (end === -1) {
|
|
6782
|
-
throw new ImportFrontmatterParseError(path, 1, "missing closing '---' for frontmatter");
|
|
6783
|
-
}
|
|
6784
|
-
const fields = {};
|
|
6785
|
-
for (let i2 = 1; i2 < end; i2++) {
|
|
6786
|
-
const line = lines[i2] ?? "";
|
|
6787
|
-
const colon = line.indexOf(":");
|
|
6788
|
-
if (colon === -1) {
|
|
6789
|
-
throw new ImportFrontmatterParseError(path, i2 + 1, line);
|
|
6790
|
-
}
|
|
6791
|
-
const key = line.slice(0, colon).trim();
|
|
6792
|
-
const value = line.slice(colon + 1).trim();
|
|
6793
|
-
fields[key] = value;
|
|
6794
|
-
}
|
|
6795
|
-
function require2(key) {
|
|
6796
|
-
const v = fields[key];
|
|
6797
|
-
if (v === void 0) {
|
|
6798
|
-
throw new ImportFrontmatterParseError(path, 1, `missing frontmatter key: ${key}`);
|
|
6799
|
-
}
|
|
6800
|
-
return v;
|
|
6801
|
-
}
|
|
6802
|
-
const id = unquote(require2("id"));
|
|
6803
|
-
if (id === null || id === "") {
|
|
6804
|
-
throw new ImportFrontmatterParseError(path, 1, "id must be a non-empty string");
|
|
6805
|
-
}
|
|
6806
|
-
const workstream = unquote(require2("workstream"));
|
|
6807
|
-
if (workstream === null || workstream === "") {
|
|
6808
|
-
throw new ImportFrontmatterParseError(path, 1, "workstream must be a non-empty string");
|
|
6809
|
-
}
|
|
6810
|
-
const statusRaw = require2("status");
|
|
6811
|
-
if (!isTaskStatus(statusRaw)) {
|
|
6812
|
-
throw new ImportFrontmatterParseError(path, 1, `unknown status: ${statusRaw}`);
|
|
6813
|
-
}
|
|
6814
|
-
const impact = Number(require2("impact"));
|
|
6815
|
-
const effortDays = Number(require2("effort_days"));
|
|
6816
|
-
if (!Number.isFinite(impact) || !Number.isFinite(effortDays)) {
|
|
6817
|
-
throw new ImportFrontmatterParseError(path, 1, "impact / effort_days must be numeric");
|
|
6818
|
-
}
|
|
6819
|
-
const ownerName = unquote(require2("owner"));
|
|
6820
|
-
const createdAt = unquote(require2("created_at"));
|
|
6821
|
-
const updatedAt = unquote(require2("updated_at"));
|
|
6822
|
-
if (createdAt === null || updatedAt === null) {
|
|
6823
|
-
throw new ImportFrontmatterParseError(path, 1, "created_at / updated_at cannot be null");
|
|
6824
|
-
}
|
|
6825
|
-
let blockedBy;
|
|
6826
|
-
let blocks;
|
|
6827
|
-
try {
|
|
6828
|
-
blockedBy = parseStringArray(require2("blocked_by"));
|
|
6829
|
-
blocks = parseStringArray(require2("blocks"));
|
|
6830
|
-
} catch (err) {
|
|
6831
|
-
throw new ImportFrontmatterParseError(
|
|
6832
|
-
path,
|
|
6833
|
-
1,
|
|
6834
|
-
err instanceof Error ? err.message : String(err)
|
|
6835
|
-
);
|
|
6836
|
-
}
|
|
6837
|
-
let bodyIdx = end + 1;
|
|
6838
|
-
while (bodyIdx < lines.length && lines[bodyIdx] === "") bodyIdx += 1;
|
|
6839
|
-
const titleLine = lines[bodyIdx] ?? "";
|
|
6840
|
-
if (!titleLine.startsWith("# ")) {
|
|
6841
|
-
throw new ImportFrontmatterParseError(
|
|
6842
|
-
path,
|
|
6843
|
-
bodyIdx + 1,
|
|
6844
|
-
"expected '# <title>' after frontmatter"
|
|
6845
|
-
);
|
|
6846
|
-
}
|
|
6847
|
-
const title = titleLine.slice(2).trim();
|
|
6848
|
-
const notes = [];
|
|
6849
|
-
let i = bodyIdx + 1;
|
|
6850
|
-
while (i < lines.length && !(lines[i] ?? "").startsWith("## Notes (")) i += 1;
|
|
6851
|
-
if (i < lines.length) {
|
|
6852
|
-
i += 1;
|
|
6853
|
-
while (i < lines.length) {
|
|
6854
|
-
const line = lines[i] ?? "";
|
|
6855
|
-
if (!line.startsWith("### #")) {
|
|
6856
|
-
i += 1;
|
|
6857
|
-
continue;
|
|
6858
|
-
}
|
|
6859
|
-
const headerRest = line.slice(line.indexOf(" by ") + 4);
|
|
6860
|
-
const lastComma = headerRest.lastIndexOf(", ");
|
|
6861
|
-
if (lastComma === -1) {
|
|
6862
|
-
throw new ImportFrontmatterParseError(path, i + 1, line);
|
|
6863
|
-
}
|
|
6864
|
-
const authorRaw = headerRest.slice(0, lastComma);
|
|
6865
|
-
const author = unquote(authorRaw);
|
|
6866
|
-
const createdAtNote = headerRest.slice(lastComma + 2);
|
|
6867
|
-
i += 1;
|
|
6868
|
-
while (i < lines.length && lines[i] === "") i += 1;
|
|
6869
|
-
const openFence = lines[i] ?? "";
|
|
6870
|
-
if (!/^`{3,}$/.test(openFence)) {
|
|
6871
|
-
throw new ImportFrontmatterParseError(path, i + 1, openFence);
|
|
6872
|
-
}
|
|
6873
|
-
const fence = openFence;
|
|
6874
|
-
i += 1;
|
|
6875
|
-
const contentLines = [];
|
|
6876
|
-
while (i < lines.length && lines[i] !== fence) {
|
|
6877
|
-
contentLines.push(lines[i] ?? "");
|
|
6878
|
-
i += 1;
|
|
6879
|
-
}
|
|
6880
|
-
if (i >= lines.length) {
|
|
6881
|
-
throw new ImportFrontmatterParseError(path, i + 1, `unterminated note fence ${fence}`);
|
|
6882
|
-
}
|
|
6883
|
-
i += 1;
|
|
6884
|
-
notes.push({ author, createdAt: createdAtNote, content: contentLines.join("\n") });
|
|
6885
|
-
}
|
|
6886
|
-
}
|
|
6887
|
-
return {
|
|
6888
|
-
id,
|
|
6889
|
-
workstream,
|
|
6890
|
-
status: statusRaw,
|
|
6891
|
-
impact,
|
|
6892
|
-
effortDays,
|
|
6893
|
-
ownerName,
|
|
6894
|
-
createdAt,
|
|
6895
|
-
updatedAt,
|
|
6896
|
-
blockedBy,
|
|
6897
|
-
blocks,
|
|
6898
|
-
title,
|
|
6899
|
-
notes
|
|
6900
|
-
};
|
|
6901
|
-
}
|
|
6902
|
-
function walkBucket(bucketDir) {
|
|
6903
|
-
if (!existsSync12(bucketDir)) {
|
|
6904
|
-
throw new ImportBucketInvalidError(bucketDir, "directory does not exist");
|
|
6905
|
-
}
|
|
6906
|
-
if (!statSync4(bucketDir).isDirectory()) {
|
|
6907
|
-
throw new ImportBucketInvalidError(bucketDir, "not a directory");
|
|
6908
|
-
}
|
|
6909
|
-
const manifestPath = join8(bucketDir, "manifest.json");
|
|
6910
|
-
const probe = readManifest(manifestPath);
|
|
6911
|
-
if (probe.kind === "v2") {
|
|
6912
|
-
const manifest = probe.manifest;
|
|
6913
|
-
const sources = [];
|
|
6914
|
-
for (const entry of readdirSync3(bucketDir, { withFileTypes: true })) {
|
|
6915
|
-
if (!entry.isDirectory()) continue;
|
|
6916
|
-
const sourceDir = join8(bucketDir, entry.name);
|
|
6917
|
-
const tasksDir2 = join8(sourceDir, "tasks");
|
|
6918
|
-
if (!existsSync12(tasksDir2) || !statSync4(tasksDir2).isDirectory()) continue;
|
|
6919
|
-
const taskFiles2 = [];
|
|
6920
|
-
for (const f of readdirSync3(tasksDir2, { withFileTypes: true })) {
|
|
6921
|
-
if (f.isFile() && f.name.endsWith(".md")) {
|
|
6922
|
-
taskFiles2.push(join8(tasksDir2, f.name));
|
|
6923
|
-
}
|
|
6924
|
-
}
|
|
6925
|
-
taskFiles2.sort();
|
|
6926
|
-
sources.push({ diskName: entry.name, taskFiles: taskFiles2 });
|
|
6927
|
-
}
|
|
6928
|
-
sources.sort((a, b) => a.diskName.localeCompare(b.diskName));
|
|
6929
|
-
return { manifest, sources, shape: "bucket" };
|
|
6930
|
-
}
|
|
6931
|
-
if (probe.kind === "corrupt") {
|
|
6932
|
-
throw new ImportBucketInvalidError(bucketDir, "manifest.json is unreadable / malformed");
|
|
6933
|
-
}
|
|
6934
|
-
const tasksDir = join8(bucketDir, "tasks");
|
|
6935
|
-
const looksLikeSourceWs = existsSync12(join8(bucketDir, "README.md")) && existsSync12(join8(bucketDir, "INDEX.md")) && existsSync12(tasksDir) && statSync4(tasksDir).isDirectory();
|
|
6936
|
-
if (!looksLikeSourceWs) {
|
|
6937
|
-
throw new ImportBucketInvalidError(bucketDir, "manifest.json missing");
|
|
6938
|
-
}
|
|
6939
|
-
const parentDir = dirname5(bucketDir);
|
|
6940
|
-
const baseName = basename2(bucketDir);
|
|
6941
|
-
const parentProbe = readManifest(join8(parentDir, "manifest.json"));
|
|
6942
|
-
if (parentProbe.kind !== "v2") {
|
|
6943
|
-
throw new ImportBucketInvalidError(
|
|
6944
|
-
bucketDir,
|
|
6945
|
-
`${bucketDir} looks like a per-source-ws subdir (README.md + INDEX.md + tasks/), but ${parentDir}/manifest.json is missing or not a bucketVersion 2 manifest. Pass the parent bucket directory instead, or re-export.`
|
|
6946
|
-
);
|
|
6947
|
-
}
|
|
6948
|
-
const parentManifest = parentProbe.manifest;
|
|
6949
|
-
if (!Object.prototype.hasOwnProperty.call(parentManifest.sources ?? {}, baseName)) {
|
|
6950
|
-
throw new ImportBucketInvalidError(
|
|
6951
|
-
bucketDir,
|
|
6952
|
-
`${bucketDir} looks like a per-source-ws subdir but "${baseName}" is not in the parent bucket manifest's sources (${parentDir}/manifest.json). Re-export to refresh the bucket manifest.`
|
|
6953
|
-
);
|
|
6954
|
-
}
|
|
6955
|
-
const taskFiles = [];
|
|
6956
|
-
for (const f of readdirSync3(tasksDir, { withFileTypes: true })) {
|
|
6957
|
-
if (f.isFile() && f.name.endsWith(".md")) {
|
|
6958
|
-
taskFiles.push(join8(tasksDir, f.name));
|
|
6959
|
-
}
|
|
6960
|
-
}
|
|
6961
|
-
taskFiles.sort();
|
|
6962
|
-
return {
|
|
6963
|
-
manifest: parentManifest,
|
|
6964
|
-
sources: [{ diskName: baseName, taskFiles }],
|
|
6965
|
-
shape: "sourceWsSubdir"
|
|
6966
|
-
};
|
|
6967
|
-
}
|
|
6968
|
-
function importOneSource(db, targetWorkstream, parsed, options) {
|
|
6969
|
-
const idSet = new Set(parsed.map((p) => p.id));
|
|
6970
|
-
for (const t of parsed) {
|
|
6971
|
-
for (const blocker of t.blockedBy) {
|
|
6972
|
-
if (!idSet.has(blocker)) {
|
|
6973
|
-
throw new ImportEdgeRefMissingError(t.id, blocker, "blocked_by");
|
|
6974
|
-
}
|
|
6975
|
-
}
|
|
6976
|
-
for (const dep of t.blocks) {
|
|
6977
|
-
if (!idSet.has(dep)) {
|
|
6978
|
-
throw new ImportEdgeRefMissingError(t.id, dep, "blocks");
|
|
6979
|
-
}
|
|
6980
|
-
}
|
|
6981
|
-
}
|
|
6982
|
-
if (options.dryRun) {
|
|
6983
|
-
const edgePairs = /* @__PURE__ */ new Set();
|
|
6984
|
-
for (const t of parsed) {
|
|
6985
|
-
for (const blocker of t.blockedBy) edgePairs.add(`${blocker}\0${t.id}`);
|
|
6986
|
-
for (const dep of t.blocks) edgePairs.add(`${t.id}\0${dep}`);
|
|
6987
|
-
}
|
|
6988
|
-
const noteCount = parsed.reduce((acc, p) => acc + p.notes.length, 0);
|
|
6989
|
-
return {
|
|
6990
|
-
tasksImported: parsed.length,
|
|
6991
|
-
edgesImported: edgePairs.size,
|
|
6992
|
-
notesImported: noteCount
|
|
6993
|
-
};
|
|
6994
|
-
}
|
|
6995
|
-
return db.transaction(() => {
|
|
6996
|
-
ensureWorkstream(db, targetWorkstream);
|
|
6997
|
-
const wsId = resolveWorkstreamId(db, targetWorkstream);
|
|
6998
|
-
const insertTask = db.prepare(
|
|
6999
|
-
`INSERT INTO tasks
|
|
7000
|
-
(workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
|
|
7001
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
7002
|
-
);
|
|
7003
|
-
const taskIdByLocalId = /* @__PURE__ */ new Map();
|
|
7004
|
-
for (const t of parsed) {
|
|
7005
|
-
const r = insertTask.run(
|
|
7006
|
-
wsId,
|
|
7007
|
-
t.id,
|
|
7008
|
-
t.title,
|
|
7009
|
-
t.status,
|
|
7010
|
-
t.impact,
|
|
7011
|
-
t.effortDays,
|
|
7012
|
-
t.createdAt,
|
|
7013
|
-
t.updatedAt
|
|
7014
|
-
);
|
|
7015
|
-
taskIdByLocalId.set(t.id, Number(r.lastInsertRowid));
|
|
7016
|
-
}
|
|
7017
|
-
const insertEdge = db.prepare(
|
|
7018
|
-
"INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
|
|
7019
|
-
);
|
|
7020
|
-
const seenEdges = /* @__PURE__ */ new Set();
|
|
7021
|
-
let edgesImported = 0;
|
|
7022
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7023
|
-
const recordEdge = (fromLocal, toLocal) => {
|
|
7024
|
-
const key = `${fromLocal}\0${toLocal}`;
|
|
7025
|
-
if (seenEdges.has(key)) return;
|
|
7026
|
-
seenEdges.add(key);
|
|
7027
|
-
const fromId = taskIdByLocalId.get(fromLocal);
|
|
7028
|
-
const toId = taskIdByLocalId.get(toLocal);
|
|
7029
|
-
if (fromId === void 0 || toId === void 0) return;
|
|
7030
|
-
const r = insertEdge.run(fromId, toId, now);
|
|
7031
|
-
if (r.changes > 0) edgesImported += 1;
|
|
7032
|
-
};
|
|
7033
|
-
for (const t of parsed) {
|
|
7034
|
-
for (const blocker of t.blockedBy) recordEdge(blocker, t.id);
|
|
7035
|
-
for (const dep of t.blocks) recordEdge(t.id, dep);
|
|
7036
|
-
}
|
|
7037
|
-
const insertNote = db.prepare(
|
|
7038
|
-
"INSERT INTO task_notes (task_id, author, content, created_at) VALUES (?, ?, ?, ?)"
|
|
7039
|
-
);
|
|
7040
|
-
let notesImported = 0;
|
|
7041
|
-
for (const t of parsed) {
|
|
7042
|
-
const taskId = taskIdByLocalId.get(t.id);
|
|
7043
|
-
if (taskId === void 0) continue;
|
|
7044
|
-
for (const note of t.notes) {
|
|
7045
|
-
insertNote.run(taskId, note.author, note.content, note.createdAt);
|
|
7046
|
-
notesImported += 1;
|
|
7047
|
-
}
|
|
7048
|
-
}
|
|
7049
|
-
emitEvent(
|
|
7050
|
-
db,
|
|
7051
|
-
targetWorkstream,
|
|
7052
|
-
`workstream import ${targetWorkstream} (tasks=${parsed.length}, edges=${edgesImported}, notes=${notesImported})`
|
|
7053
|
-
);
|
|
7054
|
-
return {
|
|
7055
|
-
tasksImported: parsed.length,
|
|
7056
|
-
edgesImported,
|
|
7057
|
-
notesImported
|
|
7058
|
-
};
|
|
7059
|
-
})();
|
|
7060
|
-
}
|
|
7061
|
-
function importBucket(db, opts) {
|
|
7062
|
-
const { manifest, sources, shape } = walkBucket(opts.bucketDir);
|
|
7063
|
-
if (shape === "sourceWsSubdir" && opts.sourceWs !== void 0 && opts.sourceWs.length > 0) {
|
|
7064
|
-
throw new ImportBucketInvalidError(
|
|
7065
|
-
opts.bucketDir,
|
|
7066
|
-
`cannot pass --source-ws when ${opts.bucketDir} is itself a source-ws subdir; drop the flag`
|
|
7067
|
-
);
|
|
7068
|
-
}
|
|
7069
|
-
let filteredSources = sources;
|
|
7070
|
-
if (opts.sourceWs !== void 0 && opts.sourceWs.length > 0) {
|
|
7071
|
-
const validNames = Object.keys(manifest.sources ?? {}).sort();
|
|
7072
|
-
const validSet = new Set(validNames);
|
|
7073
|
-
for (const wanted of opts.sourceWs) {
|
|
7074
|
-
if (!validSet.has(wanted)) {
|
|
7075
|
-
throw new ImportSourceNotInBucketError(opts.bucketDir, wanted, validNames);
|
|
7076
|
-
}
|
|
7077
|
-
}
|
|
7078
|
-
const wantedSet = new Set(opts.sourceWs);
|
|
7079
|
-
filteredSources = sources.filter((s) => wantedSet.has(s.diskName));
|
|
7080
|
-
}
|
|
7081
|
-
if (opts.workstreamOverride !== void 0 && filteredSources.length !== 1) {
|
|
7082
|
-
throw new ImportBucketInvalidError(
|
|
7083
|
-
opts.bucketDir,
|
|
7084
|
-
`--workstream override requires a single source-ws subdir; this bucket has ${filteredSources.length}`
|
|
7085
|
-
);
|
|
7086
|
-
}
|
|
7087
|
-
const dryRun = opts.dryRun === true;
|
|
7088
|
-
const results = [];
|
|
7089
|
-
for (const source of filteredSources) {
|
|
7090
|
-
const targetName = opts.workstreamOverride ?? source.diskName;
|
|
7091
|
-
const result = {
|
|
7092
|
-
workstreamName: targetName,
|
|
7093
|
-
tasksImported: 0,
|
|
7094
|
-
edgesImported: 0,
|
|
7095
|
-
notesImported: 0,
|
|
7096
|
-
tombstonesSkipped: 0,
|
|
7097
|
-
errors: []
|
|
7098
|
-
};
|
|
7099
|
-
const liveFiles = [];
|
|
7100
|
-
for (const file of source.taskFiles) {
|
|
7101
|
-
const head = readFileSync2(file, "utf8").slice(0, DELETED_BANNER_PREFIX.length);
|
|
7102
|
-
if (head.startsWith(DELETED_BANNER_PREFIX)) {
|
|
7103
|
-
result.tombstonesSkipped += 1;
|
|
7104
|
-
continue;
|
|
7105
|
-
}
|
|
7106
|
-
liveFiles.push(file);
|
|
7107
|
-
}
|
|
7108
|
-
if (!dryRun) {
|
|
7109
|
-
const existing = db.prepare("SELECT 1 AS x FROM workstreams WHERE name = ?").get(targetName);
|
|
7110
|
-
if (existing !== void 0) {
|
|
7111
|
-
const err = new WorkstreamAlreadyExistsError(targetName);
|
|
7112
|
-
result.errors.push(err.message);
|
|
7113
|
-
results.push(result);
|
|
7114
|
-
throw err;
|
|
7115
|
-
}
|
|
7116
|
-
}
|
|
7117
|
-
let parsed;
|
|
7118
|
-
try {
|
|
7119
|
-
parsed = liveFiles.map(parseTaskMarkdown);
|
|
7120
|
-
} catch (err) {
|
|
7121
|
-
result.errors.push(err instanceof Error ? err.message : String(err));
|
|
7122
|
-
results.push(result);
|
|
7123
|
-
throw err;
|
|
7124
|
-
}
|
|
7125
|
-
try {
|
|
7126
|
-
const counts = importOneSource(db, targetName, parsed, { dryRun });
|
|
7127
|
-
result.tasksImported = counts.tasksImported;
|
|
7128
|
-
result.edgesImported = counts.edgesImported;
|
|
7129
|
-
result.notesImported = counts.notesImported;
|
|
7130
|
-
} catch (err) {
|
|
7131
|
-
result.errors.push(err instanceof Error ? err.message : String(err));
|
|
7132
|
-
results.push(result);
|
|
7133
|
-
throw err;
|
|
7134
|
-
}
|
|
7135
|
-
results.push(result);
|
|
7136
|
-
}
|
|
7137
|
-
return {
|
|
7138
|
-
bucketLabel: manifest.bucketLabel,
|
|
7139
|
-
bucketVersion: manifest.bucketVersion,
|
|
7140
|
-
sources: results
|
|
7141
|
-
};
|
|
7142
|
-
}
|
|
7143
|
-
|
|
7144
7607
|
// src/doctor-summary.ts
|
|
7145
7608
|
function loadDoctorSummary(db, snapshot) {
|
|
7146
7609
|
const checks = [];
|
|
@@ -7244,7 +7707,7 @@ function loadDoctorSummary(db, snapshot) {
|
|
|
7244
7707
|
checks.push({
|
|
7245
7708
|
name: "agents",
|
|
7246
7709
|
status: "warn",
|
|
7247
|
-
detail: `${ghosts} ghost pane${ghosts === 1 ? "" : "s"}; run \`mu agent list
|
|
7710
|
+
detail: `${ghosts} ghost pane${ghosts === 1 ? "" : "s"}; run \`mu state\` or \`mu agent list\` to reap`
|
|
7248
7711
|
});
|
|
7249
7712
|
}
|
|
7250
7713
|
const orphanPanes = snapshot.view.orphans.length;
|
|
@@ -7281,7 +7744,7 @@ function loadDoctorChecks(db, snapshot) {
|
|
|
7281
7744
|
function yankCommandForCheck(check) {
|
|
7282
7745
|
switch (check.name) {
|
|
7283
7746
|
case "agents":
|
|
7284
|
-
return "mu
|
|
7747
|
+
return "mu state";
|
|
7285
7748
|
case "panes":
|
|
7286
7749
|
return "mu agent adopt";
|
|
7287
7750
|
case "workspaces":
|
|
@@ -7299,10 +7762,10 @@ function remediationParagraph(check) {
|
|
|
7299
7762
|
switch (check.name) {
|
|
7300
7763
|
case "agents":
|
|
7301
7764
|
return [
|
|
7302
|
-
"A 'ghost pane' is a
|
|
7303
|
-
"
|
|
7304
|
-
"
|
|
7305
|
-
"
|
|
7765
|
+
"A 'ghost pane' is a registered agent whose tmux pane is gone.",
|
|
7766
|
+
"Doctor only reports the count. Run `mu state` or `mu agent list`",
|
|
7767
|
+
"to reap ghost agents and return their IN_PROGRESS tasks to OPEN.",
|
|
7768
|
+
"The TUI is read-only, but its slow tick uses the same state reap."
|
|
7306
7769
|
];
|
|
7307
7770
|
case "panes":
|
|
7308
7771
|
return [
|
|
@@ -7371,7 +7834,7 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
|
|
|
7371
7834
|
};
|
|
7372
7835
|
}
|
|
7373
7836
|
async function loadWorkstreamSnapshotSlow(db, workstream, opts = {}, baseSnapshot) {
|
|
7374
|
-
const view = await listLiveAgents(db, { workstream
|
|
7837
|
+
const view = await listLiveAgents(db, { workstream });
|
|
7375
7838
|
let workspaces = listWorkspaces(db, workstream);
|
|
7376
7839
|
if (opts.withDirty === true) workspaces = await decorateWithDirty(workspaces);
|
|
7377
7840
|
const commits = await loadRecentCommits(opts.withRecentCommits);
|
|
@@ -7414,7 +7877,7 @@ function emptyLiveAgentsView() {
|
|
|
7414
7877
|
return {
|
|
7415
7878
|
agents: [],
|
|
7416
7879
|
orphans: [],
|
|
7417
|
-
report: { prunedGhosts: 0, statusChanges: 0, orphans: [], mode: "
|
|
7880
|
+
report: { prunedGhosts: 0, statusChanges: 0, orphans: [], mode: "full" }
|
|
7418
7881
|
};
|
|
7419
7882
|
}
|
|
7420
7883
|
function minimalSnapshot(workstream) {
|
|
@@ -7476,14 +7939,14 @@ function agentStatusHistogram(agents) {
|
|
|
7476
7939
|
return out;
|
|
7477
7940
|
}
|
|
7478
7941
|
function summarizeOwnedTasks(owned) {
|
|
7479
|
-
const
|
|
7480
|
-
if (
|
|
7481
|
-
if (
|
|
7942
|
+
const count2 = owned.length;
|
|
7943
|
+
if (count2 === 0) return { bit: "\u2014", count: 0 };
|
|
7944
|
+
if (count2 === 1) {
|
|
7482
7945
|
const only = owned[0];
|
|
7483
7946
|
if (!only) return { bit: "\u2014", count: 0 };
|
|
7484
7947
|
return { bit: only.name, count: 1, onlyTaskId: only.name };
|
|
7485
7948
|
}
|
|
7486
|
-
return { bit: `\u2295${
|
|
7949
|
+
return { bit: `\u2295${count2}`, count: count2 };
|
|
7487
7950
|
}
|
|
7488
7951
|
export {
|
|
7489
7952
|
AgentDiedOnSpawnError,
|
|
@@ -7495,16 +7958,22 @@ export {
|
|
|
7495
7958
|
ArchiveAlreadyExistsError,
|
|
7496
7959
|
ArchiveLabelInvalidError,
|
|
7497
7960
|
ArchiveNotFoundError,
|
|
7961
|
+
ArchiveSourceAmbiguousError,
|
|
7498
7962
|
CURRENT_SCHEMA_VERSION,
|
|
7499
7963
|
ClaimerNotRegisteredError,
|
|
7500
7964
|
CrossWorkstreamEdgeError,
|
|
7501
7965
|
CycleError,
|
|
7966
|
+
DbExportTargetExistsError,
|
|
7967
|
+
DbImportConflictError,
|
|
7968
|
+
DbImportManifestMissingError,
|
|
7969
|
+
DbImportSchemaTooNewError,
|
|
7970
|
+
DbImportSchemaTooOldError,
|
|
7971
|
+
DbImportSourceStaleError,
|
|
7972
|
+
DbReplayLocalIdConflictError,
|
|
7973
|
+
DbReplayWorkstreamMissingError,
|
|
7502
7974
|
EVENT_VERB_PREFIXES,
|
|
7503
7975
|
EXPECTED_TABLES,
|
|
7504
7976
|
HomeDirAsProjectRootError,
|
|
7505
|
-
ImportBucketInvalidError,
|
|
7506
|
-
ImportEdgeRefMissingError,
|
|
7507
|
-
ImportFrontmatterParseError,
|
|
7508
7977
|
NoForegroundProcessError,
|
|
7509
7978
|
PANE_ID_RE,
|
|
7510
7979
|
PaneNotFoundError,
|
|
@@ -7524,21 +7993,25 @@ export {
|
|
|
7524
7993
|
TaskNotInWorkstreamError,
|
|
7525
7994
|
TmuxError,
|
|
7526
7995
|
WORKSPACE_STALE_THRESHOLD,
|
|
7996
|
+
WORKSTREAM_PARKED_THRESHOLD_DAYS,
|
|
7527
7997
|
WorkspaceExistsError,
|
|
7528
7998
|
WorkspaceNotFoundError,
|
|
7529
7999
|
WorkspacePathNotEmptyError,
|
|
7530
8000
|
WorkspacePreservedError,
|
|
7531
|
-
|
|
8001
|
+
WorkstreamExistsError,
|
|
7532
8002
|
WorkstreamNameInvalidError,
|
|
7533
8003
|
addBlockEdge,
|
|
7534
8004
|
addNote,
|
|
7535
8005
|
addTask,
|
|
7536
8006
|
addToArchive,
|
|
7537
8007
|
adoptAgent,
|
|
8008
|
+
agentStatusGlyph,
|
|
7538
8009
|
agentStatusHistogram,
|
|
7539
8010
|
appendLog,
|
|
7540
8011
|
assertValidPaneId,
|
|
7541
8012
|
backendByName,
|
|
8013
|
+
buildImportPlan,
|
|
8014
|
+
buildReplayPlan,
|
|
7542
8015
|
capturePane,
|
|
7543
8016
|
captureSnapshot,
|
|
7544
8017
|
checkCommandResolvable,
|
|
@@ -7570,6 +8043,7 @@ export {
|
|
|
7570
8043
|
ensureWorkstream,
|
|
7571
8044
|
envVarNameForCli,
|
|
7572
8045
|
exportArchive,
|
|
8046
|
+
exportDb,
|
|
7573
8047
|
exportSourceForWorkstream,
|
|
7574
8048
|
exportSourcesForArchive,
|
|
7575
8049
|
exportWorkstream,
|
|
@@ -7594,7 +8068,7 @@ export {
|
|
|
7594
8068
|
gitBackend,
|
|
7595
8069
|
idFromTitle,
|
|
7596
8070
|
idFromTitleVerbose,
|
|
7597
|
-
|
|
8071
|
+
importDb,
|
|
7598
8072
|
insertAgent,
|
|
7599
8073
|
isKickSignal,
|
|
7600
8074
|
isStaleVersion,
|
|
@@ -7647,6 +8121,7 @@ export {
|
|
|
7647
8121
|
openTask,
|
|
7648
8122
|
paneExists,
|
|
7649
8123
|
paneTTY,
|
|
8124
|
+
parkedStatus,
|
|
7650
8125
|
parseAgentNameFromTitle,
|
|
7651
8126
|
parsePsTtyOutput,
|
|
7652
8127
|
pruneSnapshots,
|
|
@@ -7663,6 +8138,7 @@ export {
|
|
|
7663
8138
|
renderTaskTree,
|
|
7664
8139
|
renderToBucket,
|
|
7665
8140
|
reparentTask,
|
|
8141
|
+
replayDb,
|
|
7666
8142
|
resetCommandResolverForTests,
|
|
7667
8143
|
resetKickProcessExecutor,
|
|
7668
8144
|
resetSleep,
|
|
@@ -7671,6 +8147,7 @@ export {
|
|
|
7671
8147
|
resolveActorIdentity,
|
|
7672
8148
|
resolveCliCommand,
|
|
7673
8149
|
resolveCliCommandWithSource,
|
|
8150
|
+
restoreArchive,
|
|
7674
8151
|
restoreSnapshot,
|
|
7675
8152
|
roiBucket,
|
|
7676
8153
|
searchArchives,
|