@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/cli.js
CHANGED
|
@@ -10,8 +10,9 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/db.ts
|
|
13
|
+
import { randomUUID } from "crypto";
|
|
13
14
|
import { mkdirSync } from "fs";
|
|
14
|
-
import { homedir } from "os";
|
|
15
|
+
import { homedir, hostname } from "os";
|
|
15
16
|
import { dirname, join, resolve } from "path";
|
|
16
17
|
import Database from "better-sqlite3";
|
|
17
18
|
function defaultStateDir() {
|
|
@@ -40,6 +41,7 @@ function openDb(options = {}) {
|
|
|
40
41
|
throw new SchemaTooOldError(detectedVersion, MIN_ACCEPTED_SCHEMA_VERSION);
|
|
41
42
|
}
|
|
42
43
|
applySchema(db);
|
|
44
|
+
seedMachineIdentity(db);
|
|
43
45
|
} else {
|
|
44
46
|
db.pragma("foreign_keys = ON");
|
|
45
47
|
}
|
|
@@ -76,6 +78,14 @@ function detectExistingSchemaVersion(db) {
|
|
|
76
78
|
if (hasWorkstreams) return 1;
|
|
77
79
|
return null;
|
|
78
80
|
}
|
|
81
|
+
function seedMachineIdentity(db) {
|
|
82
|
+
const row2 = db.prepare("SELECT COUNT(*) AS count FROM machine_identity").get();
|
|
83
|
+
if (row2.count !== 0) return;
|
|
84
|
+
db.prepare(
|
|
85
|
+
`INSERT OR IGNORE INTO machine_identity (id, machine_id, hostname, created_at)
|
|
86
|
+
VALUES (1, ?, ?, ?)`
|
|
87
|
+
).run(randomUUID(), hostname(), (/* @__PURE__ */ new Date()).toISOString());
|
|
88
|
+
}
|
|
79
89
|
function applySchema(db) {
|
|
80
90
|
const preBumpVersion = detectExistingSchemaVersion(db);
|
|
81
91
|
db.exec(CURRENT_SCHEMA);
|
|
@@ -145,7 +155,7 @@ var init_db = __esm({
|
|
|
145
155
|
];
|
|
146
156
|
}
|
|
147
157
|
};
|
|
148
|
-
CURRENT_SCHEMA_VERSION =
|
|
158
|
+
CURRENT_SCHEMA_VERSION = 8;
|
|
149
159
|
MIN_ACCEPTED_SCHEMA_VERSION = 5;
|
|
150
160
|
EXPECTED_TABLES = [
|
|
151
161
|
"agent_logs",
|
|
@@ -155,12 +165,14 @@ var init_db = __esm({
|
|
|
155
165
|
"archived_notes",
|
|
156
166
|
"archived_tasks",
|
|
157
167
|
"archives",
|
|
168
|
+
"machine_identity",
|
|
158
169
|
"schema_version",
|
|
159
170
|
"snapshots",
|
|
160
171
|
"task_edges",
|
|
161
172
|
"task_notes",
|
|
162
173
|
"tasks",
|
|
163
174
|
"vcs_workspaces",
|
|
175
|
+
"workstream_sync",
|
|
164
176
|
"workstreams"
|
|
165
177
|
];
|
|
166
178
|
READY_VIEW_SQL = `
|
|
@@ -212,6 +224,15 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
|
|
212
224
|
version INTEGER NOT NULL
|
|
213
225
|
);
|
|
214
226
|
|
|
227
|
+
-- machine_identity: one durable identity per DB/machine, seeded by
|
|
228
|
+
-- openDb after schema creation. hostname is advisory only.
|
|
229
|
+
CREATE TABLE IF NOT EXISTS machine_identity (
|
|
230
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
231
|
+
machine_id TEXT NOT NULL,
|
|
232
|
+
hostname TEXT,
|
|
233
|
+
created_at TEXT NOT NULL
|
|
234
|
+
);
|
|
235
|
+
|
|
215
236
|
-- \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
|
|
216
237
|
|
|
217
238
|
-- workstreams: top of the hierarchy. name stays globally unique
|
|
@@ -223,6 +244,13 @@ CREATE TABLE IF NOT EXISTS workstreams (
|
|
|
223
244
|
created_at TEXT NOT NULL -- ISO 8601
|
|
224
245
|
);
|
|
225
246
|
|
|
247
|
+
-- workstream_sync: per-workstream cross-machine drift state. Rows are
|
|
248
|
+
-- created on demand by db import/export code, not pre-seeded.
|
|
249
|
+
CREATE TABLE IF NOT EXISTS workstream_sync (
|
|
250
|
+
workstream_id INTEGER PRIMARY KEY REFERENCES workstreams (id) ON DELETE CASCADE,
|
|
251
|
+
last_known_peer_seqs TEXT NOT NULL DEFAULT '{}'
|
|
252
|
+
);
|
|
253
|
+
|
|
226
254
|
-- agents: one row per managed pane. Per-workstream unique on name.
|
|
227
255
|
CREATE TABLE IF NOT EXISTS agents (
|
|
228
256
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -505,8 +533,8 @@ function listLogs(db, opts = {}) {
|
|
|
505
533
|
const rows = db.prepare(sql).all(...params);
|
|
506
534
|
return rows.map(rowFromDb);
|
|
507
535
|
}
|
|
508
|
-
function latestSeq(db) {
|
|
509
|
-
const row2 = db.prepare("SELECT MAX(seq) AS s FROM agent_logs").get();
|
|
536
|
+
function latestSeq(db, workstreamId) {
|
|
537
|
+
const row2 = 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);
|
|
510
538
|
return row2.s ?? 0;
|
|
511
539
|
}
|
|
512
540
|
function emitEvent(db, workstream, payload, source = "system") {
|
|
@@ -607,17 +635,22 @@ var init_logs = __esm({
|
|
|
607
635
|
"workstream init",
|
|
608
636
|
"workstream destroy",
|
|
609
637
|
"workstream export",
|
|
610
|
-
"workstream import",
|
|
611
638
|
// src/archives.ts — v6 archive verbs. Machine-wide events
|
|
612
639
|
// (workstream=null) because archives outlive workstreams.
|
|
613
640
|
"archive create",
|
|
614
641
|
"archive delete",
|
|
615
642
|
"archive add",
|
|
616
643
|
"archive remove",
|
|
644
|
+
"archive restore",
|
|
617
645
|
// src/exporting.ts — archive export emits the bucket-render summary
|
|
618
646
|
// as a machine-wide event (workstream=null; the export spans every
|
|
619
647
|
// source-ws in the archive).
|
|
620
|
-
"archive export"
|
|
648
|
+
"archive export",
|
|
649
|
+
// src/db-sync.ts — emitted per-workstream after a successful
|
|
650
|
+
// `mu db export`. Used as the marker for src/parked.ts (the
|
|
651
|
+
// "presumed parked on another machine" heuristic for `mu workstream
|
|
652
|
+
// list` / TUI tab strip).
|
|
653
|
+
"db export"
|
|
621
654
|
];
|
|
622
655
|
}
|
|
623
656
|
});
|
|
@@ -1050,17 +1083,19 @@ async function reconcile(db, opts) {
|
|
|
1050
1083
|
let statusChanges = 0;
|
|
1051
1084
|
const orphans = [];
|
|
1052
1085
|
const survivors = [];
|
|
1086
|
+
const pendingSurvivors = [];
|
|
1053
1087
|
for (const agent of dbAgents) {
|
|
1054
|
-
if (
|
|
1088
|
+
if (isPendingPaneId(agent.paneId)) {
|
|
1089
|
+
pendingSurvivors.push(agent);
|
|
1090
|
+
} else if (tmuxByPaneId.has(agent.paneId)) {
|
|
1055
1091
|
survivors.push(agent);
|
|
1056
1092
|
} else {
|
|
1057
1093
|
if (mode === "full") deleteAgent(db, agent.name, agent.workstreamName);
|
|
1058
1094
|
prunedGhosts++;
|
|
1059
1095
|
}
|
|
1060
1096
|
}
|
|
1061
|
-
if (mode
|
|
1097
|
+
if (mode === "full") {
|
|
1062
1098
|
for (const agent of survivors) {
|
|
1063
|
-
if (isPendingPaneId(agent.paneId)) continue;
|
|
1064
1099
|
const scrollback = await capturePane(agent.paneId, { lines: 100 });
|
|
1065
1100
|
const detected = detectPiStatus(scrollback);
|
|
1066
1101
|
if (shouldOverwriteAgentStatus(agent.status, detected) && detected !== agent.status) {
|
|
@@ -2329,6 +2364,10 @@ ${scrollback.trim()}
|
|
|
2329
2364
|
intent: "Or close + discard the workspace in one shot (lossy)",
|
|
2330
2365
|
command: `mu agent close ${this.agentName} --discard-workspace`
|
|
2331
2366
|
},
|
|
2367
|
+
{
|
|
2368
|
+
intent: "If the workstream was archived, restore task memory under a fresh name",
|
|
2369
|
+
command: "mu archive restore <label> --as <new-workstream> --source <workstream>"
|
|
2370
|
+
},
|
|
2332
2371
|
{
|
|
2333
2372
|
intent: "Or just inspect what's in the workspace",
|
|
2334
2373
|
command: `cd ${this.workspacePath}`
|
|
@@ -4889,6 +4928,117 @@ var init_query = __esm({
|
|
|
4889
4928
|
}
|
|
4890
4929
|
});
|
|
4891
4930
|
|
|
4931
|
+
// src/archives/restore.ts
|
|
4932
|
+
function restoreArchive(db, label2, asWorkstream, opts = {}) {
|
|
4933
|
+
const archiveId = resolveArchiveId(db, label2);
|
|
4934
|
+
const sources = listSources(db, archiveId);
|
|
4935
|
+
if (!isValidWorkstreamName(asWorkstream)) throw new WorkstreamNameInvalidError(asWorkstream);
|
|
4936
|
+
const sourceWorkstream = opts.sourceWorkstream ?? sources[0];
|
|
4937
|
+
if (sourceWorkstream === void 0) throw new ArchiveSourceAmbiguousError(label2, sources);
|
|
4938
|
+
if (opts.sourceWorkstream === void 0 && sources.length > 1) {
|
|
4939
|
+
throw new ArchiveSourceAmbiguousError(label2, sources);
|
|
4940
|
+
}
|
|
4941
|
+
if (opts.sourceWorkstream !== void 0 && !sources.includes(opts.sourceWorkstream)) {
|
|
4942
|
+
throw new ArchiveSourceAmbiguousError(label2, sources);
|
|
4943
|
+
}
|
|
4944
|
+
if (tryResolveWorkstreamId(db, asWorkstream) !== null) {
|
|
4945
|
+
throw new WorkstreamExistsError(asWorkstream);
|
|
4946
|
+
}
|
|
4947
|
+
captureSnapshot(db, `archive restore ${label2} as ${asWorkstream}`, null);
|
|
4948
|
+
return db.transaction(() => {
|
|
4949
|
+
ensureWorkstream(db, asWorkstream);
|
|
4950
|
+
const wsId = resolveWorkstreamId(db, asWorkstream);
|
|
4951
|
+
const restoredTasks = db.prepare(
|
|
4952
|
+
`INSERT INTO tasks
|
|
4953
|
+
(workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
|
|
4954
|
+
SELECT ?, original_local_id, title, status, impact, effort_days, NULL,
|
|
4955
|
+
original_created_at, original_updated_at
|
|
4956
|
+
FROM archived_tasks
|
|
4957
|
+
WHERE archive_id = ? AND source_workstream = ?
|
|
4958
|
+
ORDER BY id`
|
|
4959
|
+
).run(wsId, archiveId, sourceWorkstream).changes;
|
|
4960
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4961
|
+
const restoredEdges = db.prepare(
|
|
4962
|
+
`INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
|
|
4963
|
+
SELECT live_from.id, live_to.id, ?
|
|
4964
|
+
FROM archived_edges e
|
|
4965
|
+
JOIN archived_tasks arch_from ON arch_from.id = e.from_archived_id
|
|
4966
|
+
JOIN archived_tasks arch_to ON arch_to.id = e.to_archived_id
|
|
4967
|
+
JOIN tasks live_from ON live_from.workstream_id = ?
|
|
4968
|
+
AND live_from.local_id = arch_from.original_local_id
|
|
4969
|
+
JOIN tasks live_to ON live_to.workstream_id = ?
|
|
4970
|
+
AND live_to.local_id = arch_to.original_local_id
|
|
4971
|
+
WHERE e.archive_id = ?
|
|
4972
|
+
AND arch_from.source_workstream = ?
|
|
4973
|
+
AND arch_to.source_workstream = ?`
|
|
4974
|
+
).run(now, wsId, wsId, archiveId, sourceWorkstream, sourceWorkstream).changes;
|
|
4975
|
+
const restoredNotes = db.prepare(
|
|
4976
|
+
`INSERT INTO task_notes (task_id, author, content, created_at)
|
|
4977
|
+
SELECT live.id, n.author, n.content, n.created_at
|
|
4978
|
+
FROM archived_notes n
|
|
4979
|
+
JOIN archived_tasks arch ON arch.id = n.archived_task_id
|
|
4980
|
+
JOIN tasks live ON live.workstream_id = ?
|
|
4981
|
+
AND live.local_id = arch.original_local_id
|
|
4982
|
+
WHERE n.archive_id = ? AND arch.source_workstream = ?
|
|
4983
|
+
ORDER BY n.id`
|
|
4984
|
+
).run(wsId, archiveId, sourceWorkstream).changes;
|
|
4985
|
+
emitEvent(
|
|
4986
|
+
db,
|
|
4987
|
+
asWorkstream,
|
|
4988
|
+
`archive restore ${label2} source=${sourceWorkstream} as ${asWorkstream} (tasks=${restoredTasks}, edges=${restoredEdges}, notes=${restoredNotes})`
|
|
4989
|
+
);
|
|
4990
|
+
return {
|
|
4991
|
+
archiveLabel: label2,
|
|
4992
|
+
sourceWorkstream,
|
|
4993
|
+
workstreamName: asWorkstream,
|
|
4994
|
+
restoredTasks,
|
|
4995
|
+
restoredEdges,
|
|
4996
|
+
restoredNotes
|
|
4997
|
+
};
|
|
4998
|
+
})();
|
|
4999
|
+
}
|
|
5000
|
+
function listSources(db, archiveId) {
|
|
5001
|
+
return db.prepare(
|
|
5002
|
+
`SELECT source_workstream AS name
|
|
5003
|
+
FROM archived_tasks
|
|
5004
|
+
WHERE archive_id = ?
|
|
5005
|
+
GROUP BY source_workstream
|
|
5006
|
+
ORDER BY source_workstream`
|
|
5007
|
+
).all(archiveId).map((row2) => row2.name);
|
|
5008
|
+
}
|
|
5009
|
+
var ArchiveSourceAmbiguousError;
|
|
5010
|
+
var init_restore2 = __esm({
|
|
5011
|
+
"src/archives/restore.ts"() {
|
|
5012
|
+
"use strict";
|
|
5013
|
+
init_db();
|
|
5014
|
+
init_logs();
|
|
5015
|
+
init_snapshots();
|
|
5016
|
+
init_workstream();
|
|
5017
|
+
init_core4();
|
|
5018
|
+
ArchiveSourceAmbiguousError = class extends Error {
|
|
5019
|
+
constructor(label2, sources) {
|
|
5020
|
+
super(
|
|
5021
|
+
sources.length === 0 ? `archive ${label2} contains no source workstreams` : `archive ${label2} requires --source <orig-ws-name>. Available: ${sources.join(", ")}`
|
|
5022
|
+
);
|
|
5023
|
+
this.label = label2;
|
|
5024
|
+
this.sources = sources;
|
|
5025
|
+
}
|
|
5026
|
+
label;
|
|
5027
|
+
sources;
|
|
5028
|
+
name = "ArchiveSourceAmbiguousError";
|
|
5029
|
+
errorNextSteps() {
|
|
5030
|
+
return [
|
|
5031
|
+
{ intent: "Inspect archive sources", command: `mu archive show ${this.label}` },
|
|
5032
|
+
...this.sources.map((source) => ({
|
|
5033
|
+
intent: `Restore source workstream ${source}`,
|
|
5034
|
+
command: `mu archive restore ${this.label} --source ${source} --as <new-workstream>`
|
|
5035
|
+
}))
|
|
5036
|
+
];
|
|
5037
|
+
}
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
5040
|
+
});
|
|
5041
|
+
|
|
4892
5042
|
// src/archives.ts
|
|
4893
5043
|
var init_archives = __esm({
|
|
4894
5044
|
"src/archives.ts"() {
|
|
@@ -4897,6 +5047,7 @@ var init_archives = __esm({
|
|
|
4897
5047
|
init_core4();
|
|
4898
5048
|
init_delete();
|
|
4899
5049
|
init_query();
|
|
5050
|
+
init_restore2();
|
|
4900
5051
|
}
|
|
4901
5052
|
});
|
|
4902
5053
|
|
|
@@ -5489,6 +5640,37 @@ var init_exporting = __esm({
|
|
|
5489
5640
|
}
|
|
5490
5641
|
});
|
|
5491
5642
|
|
|
5643
|
+
// src/parked.ts
|
|
5644
|
+
function parkedStatus(db, workstream, opts = {}) {
|
|
5645
|
+
const wsRow = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream);
|
|
5646
|
+
if (wsRow === void 0) return { parked: false };
|
|
5647
|
+
const latest = db.prepare(
|
|
5648
|
+
"SELECT kind, payload, created_at FROM agent_logs WHERE workstream_id = ? ORDER BY seq DESC LIMIT 1"
|
|
5649
|
+
).get(wsRow.id);
|
|
5650
|
+
if (latest === void 0) return { parked: false };
|
|
5651
|
+
if (latest.kind !== "event") return { parked: false };
|
|
5652
|
+
if (!latest.payload.startsWith("db export ")) return { parked: false };
|
|
5653
|
+
const aliveAgent = db.prepare("SELECT 1 AS x FROM agents WHERE workstream_id = ? AND status != 'closed' LIMIT 1").get(wsRow.id);
|
|
5654
|
+
if (aliveAgent !== void 0) return { parked: false };
|
|
5655
|
+
const inProgress = db.prepare("SELECT 1 AS x FROM tasks WHERE workstream_id = ? AND status = 'IN_PROGRESS' LIMIT 1").get(wsRow.id);
|
|
5656
|
+
if (inProgress !== void 0) return { parked: false };
|
|
5657
|
+
const threshold = Math.max(0, opts.thresholdDays ?? WORKSTREAM_PARKED_THRESHOLD_DAYS);
|
|
5658
|
+
const exportedAt = Date.parse(latest.created_at);
|
|
5659
|
+
if (Number.isNaN(exportedAt)) return { parked: false };
|
|
5660
|
+
const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
5661
|
+
const deltaMs = now - exportedAt;
|
|
5662
|
+
const deltaDays = Math.floor(deltaMs / (24 * 60 * 60 * 1e3));
|
|
5663
|
+
if (deltaDays < threshold) return { parked: false };
|
|
5664
|
+
return { parked: true, sinceDays: deltaDays };
|
|
5665
|
+
}
|
|
5666
|
+
var WORKSTREAM_PARKED_THRESHOLD_DAYS;
|
|
5667
|
+
var init_parked = __esm({
|
|
5668
|
+
"src/parked.ts"() {
|
|
5669
|
+
"use strict";
|
|
5670
|
+
WORKSTREAM_PARKED_THRESHOLD_DAYS = 1;
|
|
5671
|
+
}
|
|
5672
|
+
});
|
|
5673
|
+
|
|
5492
5674
|
// src/workstream.ts
|
|
5493
5675
|
import { existsSync as existsSync11, readdirSync as readdirSync2, rmdirSync } from "fs";
|
|
5494
5676
|
import { join as join7, resolve as resolve4 } from "path";
|
|
@@ -5571,6 +5753,7 @@ async function listEmptyWorkstreams(db) {
|
|
|
5571
5753
|
}
|
|
5572
5754
|
async function summarizeWorkstream(db, opts) {
|
|
5573
5755
|
const tmuxSession = opts.tmuxSession ?? `mu-${opts.workstream}`;
|
|
5756
|
+
const parked = parkedStatus(db, opts.workstream);
|
|
5574
5757
|
return {
|
|
5575
5758
|
name: opts.workstream,
|
|
5576
5759
|
tmuxSession,
|
|
@@ -5580,7 +5763,8 @@ async function summarizeWorkstream(db, opts) {
|
|
|
5580
5763
|
noteCount: countNotes(db, opts.workstream),
|
|
5581
5764
|
edgeCount: countEdges(db, opts.workstream),
|
|
5582
5765
|
workspaceCount: listWorkspaces(db, opts.workstream).length,
|
|
5583
|
-
registered: isRegistered(db, opts.workstream)
|
|
5766
|
+
registered: isRegistered(db, opts.workstream),
|
|
5767
|
+
...parked.parked ? { parked: { sinceDays: parked.sinceDays ?? 0 } } : {}
|
|
5584
5768
|
};
|
|
5585
5769
|
}
|
|
5586
5770
|
function isRegistered(db, workstream) {
|
|
@@ -5717,19 +5901,41 @@ function exportWorkstream(db, opts) {
|
|
|
5717
5901
|
source: sourceManifest
|
|
5718
5902
|
};
|
|
5719
5903
|
}
|
|
5720
|
-
var WORKSTREAM_NAME_RE, RESERVED_WORKSTREAM_PREFIX, WorkstreamNameInvalidError;
|
|
5904
|
+
var WORKSTREAM_NAME_RE, RESERVED_WORKSTREAM_PREFIX, WorkstreamExistsError, WorkstreamNameInvalidError;
|
|
5721
5905
|
var init_workstream = __esm({
|
|
5722
5906
|
"src/workstream.ts"() {
|
|
5723
5907
|
"use strict";
|
|
5724
5908
|
init_db();
|
|
5725
5909
|
init_exporting();
|
|
5726
5910
|
init_logs();
|
|
5911
|
+
init_parked();
|
|
5727
5912
|
init_snapshots();
|
|
5728
5913
|
init_tmux();
|
|
5729
5914
|
init_vcs2();
|
|
5730
5915
|
init_workspace();
|
|
5731
5916
|
WORKSTREAM_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
5732
5917
|
RESERVED_WORKSTREAM_PREFIX = "mu-";
|
|
5918
|
+
WorkstreamExistsError = class extends Error {
|
|
5919
|
+
constructor(workstream) {
|
|
5920
|
+
super(`workstream already exists: ${workstream}`);
|
|
5921
|
+
this.workstream = workstream;
|
|
5922
|
+
}
|
|
5923
|
+
workstream;
|
|
5924
|
+
name = "WorkstreamExistsError";
|
|
5925
|
+
errorNextSteps() {
|
|
5926
|
+
return [
|
|
5927
|
+
{
|
|
5928
|
+
intent: "Pick a different workstream name",
|
|
5929
|
+
command: "mu archive restore <label> --as <new-name>"
|
|
5930
|
+
},
|
|
5931
|
+
{ intent: "List existing workstreams", command: "mu workstream list" },
|
|
5932
|
+
{
|
|
5933
|
+
intent: "Destroy the existing workstream first",
|
|
5934
|
+
command: `mu workstream destroy -w ${this.workstream} --yes`
|
|
5935
|
+
}
|
|
5936
|
+
];
|
|
5937
|
+
}
|
|
5938
|
+
};
|
|
5733
5939
|
WorkstreamNameInvalidError = class extends Error {
|
|
5734
5940
|
constructor(attempted) {
|
|
5735
5941
|
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.`;
|
|
@@ -6689,6 +6895,9 @@ function shouldOverwriteAgentStatus(current, detected) {
|
|
|
6689
6895
|
}
|
|
6690
6896
|
return true;
|
|
6691
6897
|
}
|
|
6898
|
+
function agentStatusGlyph(status) {
|
|
6899
|
+
return STATUS_EMOJI[status] ?? "?";
|
|
6900
|
+
}
|
|
6692
6901
|
function pendingPaneIdFor(agentName) {
|
|
6693
6902
|
return `${PENDING_PANE_PREFIX}${agentName}`;
|
|
6694
6903
|
}
|
|
@@ -6700,7 +6909,7 @@ function composeAgentTitle(db, agent) {
|
|
|
6700
6909
|
const tasks = listTasksByOwner(db, agent.workstreamName, agent.name);
|
|
6701
6910
|
let title = agent.name;
|
|
6702
6911
|
if (showStatus) {
|
|
6703
|
-
title += ` \xB7 ${
|
|
6912
|
+
title += ` \xB7 ${agentStatusGlyph(agent.status)}`;
|
|
6704
6913
|
}
|
|
6705
6914
|
if (tasks.length === 1) {
|
|
6706
6915
|
title += ` \xB7 ${tasks[0]?.name}`;
|
|
@@ -6882,6 +7091,7 @@ var init_agents = __esm({
|
|
|
6882
7091
|
});
|
|
6883
7092
|
|
|
6884
7093
|
// src/dag.ts
|
|
7094
|
+
import pc2 from "picocolors";
|
|
6885
7095
|
function loadFullDag(db, workstream, opts = {}) {
|
|
6886
7096
|
const tasks = listTasks(db, workstream).filter(
|
|
6887
7097
|
(t) => opts.statuses === void 0 || opts.statuses.has(t.status)
|
|
@@ -6920,7 +7130,7 @@ function renderForest(roots, edges, statusFn, tasksByName, opts = {}) {
|
|
|
6920
7130
|
if (!byName.has(root.name)) byName.set(root.name, root);
|
|
6921
7131
|
const lines = [formatTreeNodeLabel(root, statusFn, opts)];
|
|
6922
7132
|
if (seen.has(root.name)) {
|
|
6923
|
-
lines[0] = `${lines[0]}
|
|
7133
|
+
lines[0] = `${lines[0]}${RECURRENCE_MARKER}`;
|
|
6924
7134
|
} else {
|
|
6925
7135
|
seen.add(root.name);
|
|
6926
7136
|
renderForestChildren(root.name, "", edges, byName, statusFn, seen, lines, opts);
|
|
@@ -6979,7 +7189,7 @@ function renderForestChildren(taskName, prefix, edges, byName, statusFn, seen, l
|
|
|
6979
7189
|
}
|
|
6980
7190
|
if (seen.has(childName)) {
|
|
6981
7191
|
lines.push(
|
|
6982
|
-
`${prefix}${branch}${formatTreeNodeLabel(child, statusFn, opts)}
|
|
7192
|
+
`${prefix}${branch}${formatTreeNodeLabel(child, statusFn, opts)}${RECURRENCE_MARKER}`
|
|
6983
7193
|
);
|
|
6984
7194
|
continue;
|
|
6985
7195
|
}
|
|
@@ -6993,10 +7203,12 @@ function formatTreeNodeLabel(t, statusFn, opts = {}) {
|
|
|
6993
7203
|
if (opts.includeTitle === false) return base;
|
|
6994
7204
|
return `${base} ${t.title}`;
|
|
6995
7205
|
}
|
|
7206
|
+
var RECURRENCE_MARKER;
|
|
6996
7207
|
var init_dag = __esm({
|
|
6997
7208
|
"src/dag.ts"() {
|
|
6998
7209
|
"use strict";
|
|
6999
7210
|
init_tasks();
|
|
7211
|
+
RECURRENCE_MARKER = ` ${pc2.dim("(\u21BB)")}`;
|
|
7000
7212
|
}
|
|
7001
7213
|
});
|
|
7002
7214
|
|
|
@@ -7103,7 +7315,7 @@ function loadDoctorSummary(db, snapshot) {
|
|
|
7103
7315
|
checks.push({
|
|
7104
7316
|
name: "agents",
|
|
7105
7317
|
status: "warn",
|
|
7106
|
-
detail: `${ghosts} ghost pane${ghosts === 1 ? "" : "s"}; run \`mu agent list
|
|
7318
|
+
detail: `${ghosts} ghost pane${ghosts === 1 ? "" : "s"}; run \`mu state\` or \`mu agent list\` to reap`
|
|
7107
7319
|
});
|
|
7108
7320
|
}
|
|
7109
7321
|
const orphanPanes = snapshot.view.orphans.length;
|
|
@@ -7140,7 +7352,7 @@ function loadDoctorChecks(db, snapshot) {
|
|
|
7140
7352
|
function yankCommandForCheck(check) {
|
|
7141
7353
|
switch (check.name) {
|
|
7142
7354
|
case "agents":
|
|
7143
|
-
return "mu
|
|
7355
|
+
return "mu state";
|
|
7144
7356
|
case "panes":
|
|
7145
7357
|
return "mu agent adopt";
|
|
7146
7358
|
case "workspaces":
|
|
@@ -7158,10 +7370,10 @@ function remediationParagraph(check) {
|
|
|
7158
7370
|
switch (check.name) {
|
|
7159
7371
|
case "agents":
|
|
7160
7372
|
return [
|
|
7161
|
-
"A 'ghost pane' is a
|
|
7162
|
-
"
|
|
7163
|
-
"
|
|
7164
|
-
"
|
|
7373
|
+
"A 'ghost pane' is a registered agent whose tmux pane is gone.",
|
|
7374
|
+
"Doctor only reports the count. Run `mu state` or `mu agent list`",
|
|
7375
|
+
"to reap ghost agents and return their IN_PROGRESS tasks to OPEN.",
|
|
7376
|
+
"The TUI is read-only, but its slow tick uses the same state reap."
|
|
7165
7377
|
];
|
|
7166
7378
|
case "panes":
|
|
7167
7379
|
return [
|
|
@@ -7345,7 +7557,7 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
|
|
|
7345
7557
|
};
|
|
7346
7558
|
}
|
|
7347
7559
|
async function loadWorkstreamSnapshotSlow(db, workstream, opts = {}, baseSnapshot) {
|
|
7348
|
-
const view = await listLiveAgents(db, { workstream
|
|
7560
|
+
const view = await listLiveAgents(db, { workstream });
|
|
7349
7561
|
let workspaces = listWorkspaces(db, workstream);
|
|
7350
7562
|
if (opts.withDirty === true) workspaces = await decorateWithDirty(workspaces);
|
|
7351
7563
|
const commits = await loadRecentCommits(opts.withRecentCommits);
|
|
@@ -7388,7 +7600,7 @@ function emptyLiveAgentsView() {
|
|
|
7388
7600
|
return {
|
|
7389
7601
|
agents: [],
|
|
7390
7602
|
orphans: [],
|
|
7391
|
-
report: { prunedGhosts: 0, statusChanges: 0, orphans: [], mode: "
|
|
7603
|
+
report: { prunedGhosts: 0, statusChanges: 0, orphans: [], mode: "full" }
|
|
7392
7604
|
};
|
|
7393
7605
|
}
|
|
7394
7606
|
function minimalSnapshot(workstream) {
|
|
@@ -7450,14 +7662,14 @@ function agentStatusHistogram(agents) {
|
|
|
7450
7662
|
return out;
|
|
7451
7663
|
}
|
|
7452
7664
|
function summarizeOwnedTasks(owned) {
|
|
7453
|
-
const
|
|
7454
|
-
if (
|
|
7455
|
-
if (
|
|
7665
|
+
const count2 = owned.length;
|
|
7666
|
+
if (count2 === 0) return { bit: "\u2014", count: 0 };
|
|
7667
|
+
if (count2 === 1) {
|
|
7456
7668
|
const only = owned[0];
|
|
7457
7669
|
if (!only) return { bit: "\u2014", count: 0 };
|
|
7458
7670
|
return { bit: only.name, count: 1, onlyTaskId: only.name };
|
|
7459
7671
|
}
|
|
7460
|
-
return { bit: `\u2295${
|
|
7672
|
+
return { bit: `\u2295${count2}`, count: count2 };
|
|
7461
7673
|
}
|
|
7462
7674
|
var init_state = __esm({
|
|
7463
7675
|
"src/state.ts"() {
|
|
@@ -7472,9 +7684,30 @@ var init_state = __esm({
|
|
|
7472
7684
|
}
|
|
7473
7685
|
});
|
|
7474
7686
|
|
|
7687
|
+
// src/cli/tui/agent-display.ts
|
|
7688
|
+
function agentByName(snapshot) {
|
|
7689
|
+
const agents = snapshot?.view?.agents ?? [];
|
|
7690
|
+
return new Map(agents.map((a) => [a.name, a]));
|
|
7691
|
+
}
|
|
7692
|
+
function formatKnownAgentDisplayName(agent) {
|
|
7693
|
+
return `${agentStatusGlyph(agent.status)} ${agent.name}`;
|
|
7694
|
+
}
|
|
7695
|
+
function formatAgentRefDisplayName(agentName, agents) {
|
|
7696
|
+
if (agentName === null) return "\u2014";
|
|
7697
|
+
const agent = agents.get(agentName);
|
|
7698
|
+
if (agent === void 0) return agentName;
|
|
7699
|
+
return formatKnownAgentDisplayName(agent);
|
|
7700
|
+
}
|
|
7701
|
+
var init_agent_display = __esm({
|
|
7702
|
+
"src/cli/tui/agent-display.ts"() {
|
|
7703
|
+
"use strict";
|
|
7704
|
+
init_agents();
|
|
7705
|
+
}
|
|
7706
|
+
});
|
|
7707
|
+
|
|
7475
7708
|
// src/cli/format.ts
|
|
7476
7709
|
function statusIcon(status) {
|
|
7477
|
-
return STATUS_COLORS[status](
|
|
7710
|
+
return STATUS_COLORS[status](agentStatusGlyph(status));
|
|
7478
7711
|
}
|
|
7479
7712
|
function colorStatus(status) {
|
|
7480
7713
|
switch (status) {
|
|
@@ -7655,17 +7888,19 @@ function printLogRow(row2) {
|
|
|
7655
7888
|
}
|
|
7656
7889
|
function formatWorkstreamsTable(rows) {
|
|
7657
7890
|
const table = muTable({
|
|
7658
|
-
head: ["name", "tmux", "agents", "tasks", "edges", "notes"].map((h) => pc.bold(h)),
|
|
7659
|
-
colWidths: [40, null, null, null, null, null]
|
|
7891
|
+
head: ["name", "tmux", "agents", "tasks", "edges", "notes", "parked"].map((h) => pc.bold(h)),
|
|
7892
|
+
colWidths: [40, null, null, null, null, null, null]
|
|
7660
7893
|
});
|
|
7661
7894
|
for (const r of rows) {
|
|
7895
|
+
const parkedCell = r.parked ? pc.yellow(`${r.parked.sinceDays}d`) : pc.dim("\u2014");
|
|
7662
7896
|
table.push([
|
|
7663
7897
|
r.name,
|
|
7664
7898
|
r.tmuxAlive ? pc.green("alive") : pc.dim("\u2014"),
|
|
7665
7899
|
String(r.agentCount),
|
|
7666
7900
|
String(r.taskCount),
|
|
7667
7901
|
String(r.edgeCount),
|
|
7668
|
-
String(r.noteCount)
|
|
7902
|
+
String(r.noteCount),
|
|
7903
|
+
parkedCell
|
|
7669
7904
|
]);
|
|
7670
7905
|
}
|
|
7671
7906
|
return table.toString();
|
|
@@ -7957,6 +8192,58 @@ function cullCardsForRows(visibleCardIds2, availableRows) {
|
|
|
7957
8192
|
}
|
|
7958
8193
|
return { cards: cards.filter((id) => remaining.has(id)), hidden };
|
|
7959
8194
|
}
|
|
8195
|
+
function balanceColumns(assignments, dataCountFn) {
|
|
8196
|
+
if (assignments.length < 2) return assignments.map((a) => ({ cards: [...a.cards] }));
|
|
8197
|
+
const totalCards = assignments.reduce((sum, a) => sum + a.cards.length, 0);
|
|
8198
|
+
if (totalCards >= TOTAL_CARD_COUNT) return assignments.map((a) => ({ cards: [...a.cards] }));
|
|
8199
|
+
const cols = assignments.map((a) => [...a.cards]);
|
|
8200
|
+
const heightOf = (id) => {
|
|
8201
|
+
const config = CARD_CONFIGS[id];
|
|
8202
|
+
const data = Math.max(0, Math.floor(dataCountFn(id)));
|
|
8203
|
+
return config.chrome + clamp(data, config.minRows, config.maxRows);
|
|
8204
|
+
};
|
|
8205
|
+
const isAnchored = (id) => id === 0 || id === 3;
|
|
8206
|
+
const heightsOf = (lanes) => lanes.map((lane) => lane.reduce((sum, id) => sum + heightOf(id), 0));
|
|
8207
|
+
const safetyMax = cols.length * TOTAL_CARD_COUNT;
|
|
8208
|
+
for (let iter = 0; iter < safetyMax; iter++) {
|
|
8209
|
+
const heights = heightsOf(cols);
|
|
8210
|
+
const startSpread = Math.max(...heights) - Math.min(...heights);
|
|
8211
|
+
if (startSpread <= 0) break;
|
|
8212
|
+
let best = null;
|
|
8213
|
+
for (let donor = 0; donor < cols.length; donor++) {
|
|
8214
|
+
const donorCards2 = cols[donor];
|
|
8215
|
+
const donorH = heights[donor];
|
|
8216
|
+
if (donorCards2 === void 0 || donorH === void 0) continue;
|
|
8217
|
+
if (donorCards2.length <= 1) continue;
|
|
8218
|
+
for (let cardIndex = 0; cardIndex < donorCards2.length; cardIndex++) {
|
|
8219
|
+
const card = donorCards2[cardIndex];
|
|
8220
|
+
if (card === void 0 || isAnchored(card)) continue;
|
|
8221
|
+
const cardH = heightOf(card);
|
|
8222
|
+
for (let receiver = 0; receiver < cols.length; receiver++) {
|
|
8223
|
+
if (receiver === donor) continue;
|
|
8224
|
+
const receiverH = heights[receiver];
|
|
8225
|
+
if (receiverH === void 0) continue;
|
|
8226
|
+
const newHeights = heights.slice();
|
|
8227
|
+
newHeights[donor] = donorH - cardH;
|
|
8228
|
+
newHeights[receiver] = receiverH + cardH;
|
|
8229
|
+
const newSpread = Math.max(...newHeights) - Math.min(...newHeights);
|
|
8230
|
+
if (newSpread < startSpread && (best === null || newSpread < best.spread)) {
|
|
8231
|
+
best = { donor, receiver, cardIndex, spread: newSpread };
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
if (best === null) break;
|
|
8237
|
+
const donorCards = cols[best.donor];
|
|
8238
|
+
const receiverCards = cols[best.receiver];
|
|
8239
|
+
if (donorCards === void 0 || receiverCards === void 0) break;
|
|
8240
|
+
const moved = donorCards.splice(best.cardIndex, 1)[0];
|
|
8241
|
+
if (moved === void 0) break;
|
|
8242
|
+
receiverCards.push(moved);
|
|
8243
|
+
receiverCards.sort(compareSlot);
|
|
8244
|
+
}
|
|
8245
|
+
return cols.map((cards) => ({ cards }));
|
|
8246
|
+
}
|
|
7960
8247
|
function allocateRowBudgets(availableRows, cards) {
|
|
7961
8248
|
const entries = cards.map((card) => {
|
|
7962
8249
|
const config = card.config ?? CARD_CONFIGS[card.id];
|
|
@@ -8070,7 +8357,7 @@ function tallestMinStackRows(ids) {
|
|
|
8070
8357
|
function clamp(value, min, max) {
|
|
8071
8358
|
return Math.max(min, Math.min(max, value));
|
|
8072
8359
|
}
|
|
8073
|
-
var CARD_CHROME_ROWS, CARD_CONFIGS, CARD_CULL_PRIORITY, CARD_CULL_LAYOUT_COLS;
|
|
8360
|
+
var CARD_CHROME_ROWS, CARD_CONFIGS, CARD_CULL_PRIORITY, CARD_CULL_LAYOUT_COLS, TOTAL_CARD_COUNT;
|
|
8074
8361
|
var init_layout = __esm({
|
|
8075
8362
|
"src/cli/tui/layout.ts"() {
|
|
8076
8363
|
"use strict";
|
|
@@ -8159,6 +8446,7 @@ var init_layout = __esm({
|
|
|
8159
8446
|
};
|
|
8160
8447
|
CARD_CULL_PRIORITY = [9, 8, 5, 2, 7, 6, 4, 0, 1, 3];
|
|
8161
8448
|
CARD_CULL_LAYOUT_COLS = 140;
|
|
8449
|
+
TOTAL_CARD_COUNT = 10;
|
|
8162
8450
|
}
|
|
8163
8451
|
});
|
|
8164
8452
|
|
|
@@ -8420,7 +8708,7 @@ function AgentsCard({ snapshot, rowBudget, cols }) {
|
|
|
8420
8708
|
const owned = snapshot.inProgress.filter((t) => t.ownerName === a.name);
|
|
8421
8709
|
const taskBit = summarizeOwnedTasks(owned).bit;
|
|
8422
8710
|
const idle = a.idle ? "\u26A0 idle" : "";
|
|
8423
|
-
return [
|
|
8711
|
+
return [agentStatusGlyph(a.status), a.name, taskBit, idle];
|
|
8424
8712
|
});
|
|
8425
8713
|
const widths = layoutColumns(rows, COLUMN_SPECS, contentWidth);
|
|
8426
8714
|
return /* @__PURE__ */ jsx6(
|
|
@@ -8462,8 +8750,8 @@ var cardConfig, COLUMN_SPECS;
|
|
|
8462
8750
|
var init_agents2 = __esm({
|
|
8463
8751
|
"src/cli/tui/cards/agents.tsx"() {
|
|
8464
8752
|
"use strict";
|
|
8465
|
-
init_agents();
|
|
8466
8753
|
init_state();
|
|
8754
|
+
init_agent_display();
|
|
8467
8755
|
init_columns();
|
|
8468
8756
|
init_layout();
|
|
8469
8757
|
init_list_row();
|
|
@@ -8909,11 +9197,12 @@ function InProgressCard({ snapshot, rowBudget, cols }) {
|
|
|
8909
9197
|
const shown = inProgress.slice(0, rowBudget ?? cardConfig5.maxRows);
|
|
8910
9198
|
const more = inProgress.length - shown.length;
|
|
8911
9199
|
const bottomLabel = more > 0 ? `+${more} more \xB7 Shift+6` : void 0;
|
|
9200
|
+
const agentLookup = agentByName(snapshot);
|
|
8912
9201
|
const rows = shown.map((t, i) => [
|
|
8913
9202
|
GLYPH2,
|
|
8914
9203
|
t.name,
|
|
8915
9204
|
t.status,
|
|
8916
|
-
t.ownerName
|
|
9205
|
+
formatAgentRefDisplayName(t.ownerName, agentLookup),
|
|
8917
9206
|
formatSinceClaim(ages[i] ?? null),
|
|
8918
9207
|
t.title
|
|
8919
9208
|
]);
|
|
@@ -8964,8 +9253,8 @@ var cardConfig5, GLYPH2, STALE_CLAIM_THRESHOLD_MS, COLUMN_SPECS5, glyphFor3;
|
|
|
8964
9253
|
var init_inprogress = __esm({
|
|
8965
9254
|
"src/cli/tui/cards/inprogress.tsx"() {
|
|
8966
9255
|
"use strict";
|
|
8967
|
-
init_agents();
|
|
8968
9256
|
init_format();
|
|
9257
|
+
init_agent_display();
|
|
8969
9258
|
init_columns();
|
|
8970
9259
|
init_format_helpers();
|
|
8971
9260
|
init_layout();
|
|
@@ -8973,7 +9262,7 @@ var init_inprogress = __esm({
|
|
|
8973
9262
|
init_titled_box();
|
|
8974
9263
|
init_placeholder();
|
|
8975
9264
|
cardConfig5 = CARD_CONFIGS[6];
|
|
8976
|
-
GLYPH2 =
|
|
9265
|
+
GLYPH2 = agentStatusGlyph("busy");
|
|
8977
9266
|
STALE_CLAIM_THRESHOLD_MS = 3e5;
|
|
8978
9267
|
COLUMN_SPECS5 = [
|
|
8979
9268
|
{ kind: "protect" },
|
|
@@ -9114,12 +9403,13 @@ function ReadyCard({ snapshot, rowBudget, cols }) {
|
|
|
9114
9403
|
const roiText = formatRoi(t.impact, t.effortDays);
|
|
9115
9404
|
return { bucket, roiText };
|
|
9116
9405
|
});
|
|
9406
|
+
const agentLookup = agentByName(snapshot);
|
|
9117
9407
|
const rows = shown.map((t, i) => [
|
|
9118
9408
|
t.name,
|
|
9119
9409
|
t.status,
|
|
9120
9410
|
`ROI ${meta[i]?.roiText ?? ""}`,
|
|
9121
9411
|
t.title,
|
|
9122
|
-
t.ownerName
|
|
9412
|
+
formatAgentRefDisplayName(t.ownerName, agentLookup)
|
|
9123
9413
|
]);
|
|
9124
9414
|
const widths = layoutColumns(rows, COLUMN_SPECS7, contentWidth);
|
|
9125
9415
|
return /* @__PURE__ */ jsx12(
|
|
@@ -9159,6 +9449,7 @@ var init_ready = __esm({
|
|
|
9159
9449
|
"use strict";
|
|
9160
9450
|
init_state();
|
|
9161
9451
|
init_format();
|
|
9452
|
+
init_agent_display();
|
|
9162
9453
|
init_columns();
|
|
9163
9454
|
init_format_helpers();
|
|
9164
9455
|
init_layout();
|
|
@@ -9409,9 +9700,10 @@ function WorkspacesCard({ snapshot, rowBudget, cols }) {
|
|
|
9409
9700
|
const shown = workspaces.slice(0, rowBudget ?? cardConfig10.maxRows);
|
|
9410
9701
|
const more = workspaces.length - shown.length;
|
|
9411
9702
|
const bottomLabel = more > 0 ? `+${more} more \xB7 Shift+5` : void 0;
|
|
9703
|
+
const agentLookup = agentByName(snapshot);
|
|
9412
9704
|
const rows = shown.map((w) => [
|
|
9413
9705
|
glyphFor5(w),
|
|
9414
|
-
w.agentName,
|
|
9706
|
+
formatAgentRefDisplayName(w.agentName, agentLookup),
|
|
9415
9707
|
w.backend,
|
|
9416
9708
|
formatBehind2(w.commitsBehindMain),
|
|
9417
9709
|
w.parentRef ? w.parentRef.slice(0, 12) : "\u2014"
|
|
@@ -9481,6 +9773,7 @@ var init_workspaces = __esm({
|
|
|
9481
9773
|
"src/cli/tui/cards/workspaces.tsx"() {
|
|
9482
9774
|
"use strict";
|
|
9483
9775
|
init_workspace();
|
|
9776
|
+
init_agent_display();
|
|
9484
9777
|
init_columns();
|
|
9485
9778
|
init_layout();
|
|
9486
9779
|
init_list_row();
|
|
@@ -9622,6 +9915,12 @@ var init_keymap_spec = __esm({
|
|
|
9622
9915
|
row("/", "filter/search rows", ["/"]),
|
|
9623
9916
|
row("Enter", "drill into focused row", ["Enter"]),
|
|
9624
9917
|
row("y", "yank action for focused row", ["y"]),
|
|
9918
|
+
row("a", "attach to focused agent's tmux pane (Agents popup only; user-driven TUI escape)", [
|
|
9919
|
+
"a"
|
|
9920
|
+
]),
|
|
9921
|
+
row("l", "launch lazygit in the project root (Commits popup only; user-driven TUI escape)", [
|
|
9922
|
+
"l"
|
|
9923
|
+
]),
|
|
9625
9924
|
row("Shift 0-9", "switch numbered popup", ["Shift 0-9"]),
|
|
9626
9925
|
row("Esc/q", "back to dashboard", ["Esc", "q"]),
|
|
9627
9926
|
row("?", "toggle this overlay", ["?"])
|
|
@@ -10116,6 +10415,65 @@ var init_popup_shell = __esm({
|
|
|
10116
10415
|
}
|
|
10117
10416
|
});
|
|
10118
10417
|
|
|
10418
|
+
// src/cli/tui/tmux-attach.ts
|
|
10419
|
+
import { spawnSync } from "child_process";
|
|
10420
|
+
function runTmuxAttachInteractive(opts, deps = {}) {
|
|
10421
|
+
const run3 = deps.spawn ?? spawnSync;
|
|
10422
|
+
const write = deps.write ?? ((text2) => process.stdout.write(text2));
|
|
10423
|
+
const env = deps.env ?? process.env;
|
|
10424
|
+
const target = `${opts.session}:${opts.window}`;
|
|
10425
|
+
const insideTmux = typeof env.TMUX === "string" && env.TMUX.length > 0;
|
|
10426
|
+
let result = { ok: true };
|
|
10427
|
+
try {
|
|
10428
|
+
write(ALT_SCREEN_EXIT);
|
|
10429
|
+
if (insideTmux) {
|
|
10430
|
+
const r = run3("tmux", ["switch-client", "-t", target], {
|
|
10431
|
+
stdio: "inherit",
|
|
10432
|
+
env
|
|
10433
|
+
});
|
|
10434
|
+
if (r.error !== void 0) {
|
|
10435
|
+
result = { ok: false, error: tmuxAttachErrorMessage(r.error) };
|
|
10436
|
+
} else if (typeof r.status === "number" && r.status !== 0) {
|
|
10437
|
+
result = { ok: false, error: `tmux switch-client exited ${r.status}` };
|
|
10438
|
+
}
|
|
10439
|
+
} else {
|
|
10440
|
+
const attach = run3("tmux", ["attach-session", "-t", opts.session], {
|
|
10441
|
+
stdio: "inherit",
|
|
10442
|
+
env
|
|
10443
|
+
});
|
|
10444
|
+
if (attach.error !== void 0) {
|
|
10445
|
+
result = { ok: false, error: tmuxAttachErrorMessage(attach.error) };
|
|
10446
|
+
} else if (typeof attach.status === "number" && attach.status !== 0) {
|
|
10447
|
+
result = { ok: false, error: `tmux attach-session exited ${attach.status}` };
|
|
10448
|
+
} else {
|
|
10449
|
+
run3("tmux", ["select-window", "-t", target], { stdio: "inherit", env });
|
|
10450
|
+
}
|
|
10451
|
+
}
|
|
10452
|
+
} catch (err) {
|
|
10453
|
+
result = { ok: false, error: tmuxAttachErrorMessage(err) };
|
|
10454
|
+
} finally {
|
|
10455
|
+
try {
|
|
10456
|
+
write(ALT_SCREEN_ENTER);
|
|
10457
|
+
} catch {
|
|
10458
|
+
}
|
|
10459
|
+
}
|
|
10460
|
+
return result;
|
|
10461
|
+
}
|
|
10462
|
+
function tmuxAttachErrorMessage(err) {
|
|
10463
|
+
if (err instanceof Error) {
|
|
10464
|
+
const code = err.code;
|
|
10465
|
+
if (code === "ENOENT") return "tmux not found \xB7 install tmux";
|
|
10466
|
+
return err.message.length > 0 ? err.message : String(err);
|
|
10467
|
+
}
|
|
10468
|
+
return String(err);
|
|
10469
|
+
}
|
|
10470
|
+
var init_tmux_attach = __esm({
|
|
10471
|
+
"src/cli/tui/tmux-attach.ts"() {
|
|
10472
|
+
"use strict";
|
|
10473
|
+
init_escapes();
|
|
10474
|
+
}
|
|
10475
|
+
});
|
|
10476
|
+
|
|
10119
10477
|
// src/cli/tui/use-popup-action-queue.ts
|
|
10120
10478
|
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
10121
10479
|
function usePopupActionQueue(actions, dispatch) {
|
|
@@ -10482,6 +10840,7 @@ function AgentsPopup({
|
|
|
10482
10840
|
onModeChange,
|
|
10483
10841
|
onFilterEditingChange,
|
|
10484
10842
|
popupActions,
|
|
10843
|
+
onFooter,
|
|
10485
10844
|
db,
|
|
10486
10845
|
workstream
|
|
10487
10846
|
}) {
|
|
@@ -10600,6 +10959,14 @@ function AgentsPopup({
|
|
|
10600
10959
|
void yank2(`mu agent close ${a.name} -w ${ws}`);
|
|
10601
10960
|
return;
|
|
10602
10961
|
}
|
|
10962
|
+
if (action.key === "a") {
|
|
10963
|
+
const session = `mu-${ws}`;
|
|
10964
|
+
const window = a.tab ?? a.name;
|
|
10965
|
+
const r = runTmuxAttachInteractive({ session, window });
|
|
10966
|
+
if (!r.ok) onFooter?.(r.error ?? "tmux attach failed", false, "error");
|
|
10967
|
+
else onFooter?.(`tmux switch-client \u2192 ${session}:${window}`, true, "info");
|
|
10968
|
+
return;
|
|
10969
|
+
}
|
|
10603
10970
|
return;
|
|
10604
10971
|
}
|
|
10605
10972
|
}
|
|
@@ -10640,13 +11007,13 @@ function AgentsPopup({
|
|
|
10640
11007
|
) }) });
|
|
10641
11008
|
}
|
|
10642
11009
|
const { start, visible } = centredVisibleSlice(agents, safeCursor, viewport);
|
|
10643
|
-
const rows = visible.map((a) => [
|
|
11010
|
+
const rows = visible.map((a) => [agentStatusGlyph(a.status), a.name, a.status, a.role]);
|
|
10644
11011
|
const widths = layoutColumns(rows, COLUMN_SPECS11, contentWidth);
|
|
10645
11012
|
return /* @__PURE__ */ jsxs10(
|
|
10646
11013
|
PopupShell,
|
|
10647
11014
|
{
|
|
10648
11015
|
title: `Agents \xB7 popup (${safeCursor + 1}/${agents.length})`,
|
|
10649
|
-
hint: "f free \xB7 x close \xB7 y yanks `mu agent send`",
|
|
11016
|
+
hint: "a attach \xB7 f free \xB7 x close \xB7 y yanks `mu agent send`",
|
|
10650
11017
|
children: [
|
|
10651
11018
|
/* @__PURE__ */ jsx20(Box7, { flexDirection: "column", flexGrow: 1, children: visible.map((a, i) => {
|
|
10652
11019
|
const sel = start + i === safeCursor;
|
|
@@ -10674,10 +11041,12 @@ var init_agents3 = __esm({
|
|
|
10674
11041
|
"src/cli/tui/popups/agents.tsx"() {
|
|
10675
11042
|
"use strict";
|
|
10676
11043
|
init_agents();
|
|
11044
|
+
init_agent_display();
|
|
10677
11045
|
init_columns();
|
|
10678
11046
|
init_keys();
|
|
10679
11047
|
init_list_row();
|
|
10680
11048
|
init_popup_shell();
|
|
11049
|
+
init_tmux_attach();
|
|
10681
11050
|
init_use_popup_action_queue();
|
|
10682
11051
|
init_use_popup_filter();
|
|
10683
11052
|
init_drill();
|
|
@@ -11030,10 +11399,11 @@ function AllTasksPopup({
|
|
|
11030
11399
|
) }) });
|
|
11031
11400
|
}
|
|
11032
11401
|
const { start, visible: windowed } = centredVisibleSlice(visibleTasks, safeCursor, viewport);
|
|
11402
|
+
const agentLookup = agentByName(snapshot);
|
|
11033
11403
|
const rows = windowed.map((t) => [
|
|
11034
11404
|
t.name,
|
|
11035
11405
|
t.status,
|
|
11036
|
-
t.ownerName
|
|
11406
|
+
formatAgentRefDisplayName(t.ownerName, agentLookup),
|
|
11037
11407
|
formatRoi(t.impact, t.effortDays),
|
|
11038
11408
|
t.title
|
|
11039
11409
|
]);
|
|
@@ -11126,6 +11496,7 @@ var init_all_tasks = __esm({
|
|
|
11126
11496
|
init_tasks();
|
|
11127
11497
|
init_sort();
|
|
11128
11498
|
init_format();
|
|
11499
|
+
init_agent_display();
|
|
11129
11500
|
init_columns();
|
|
11130
11501
|
init_format_helpers();
|
|
11131
11502
|
init_keys();
|
|
@@ -11362,10 +11733,56 @@ var init_blocked2 = __esm({
|
|
|
11362
11733
|
}
|
|
11363
11734
|
});
|
|
11364
11735
|
|
|
11736
|
+
// src/cli/tui/lazygit.ts
|
|
11737
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
11738
|
+
function runLazygitInteractive(opts, deps = {}) {
|
|
11739
|
+
const run3 = deps.spawn ?? spawnSync2;
|
|
11740
|
+
const write = deps.write ?? ((text2) => process.stdout.write(text2));
|
|
11741
|
+
const env = deps.env ?? process.env;
|
|
11742
|
+
let result = { ok: true };
|
|
11743
|
+
try {
|
|
11744
|
+
write(ALT_SCREEN_EXIT);
|
|
11745
|
+
const r = run3("lazygit", [], {
|
|
11746
|
+
cwd: opts.cwd,
|
|
11747
|
+
stdio: "inherit",
|
|
11748
|
+
env
|
|
11749
|
+
});
|
|
11750
|
+
if (r.error !== void 0) {
|
|
11751
|
+
result = { ok: false, error: lazygitErrorMessage(r.error) };
|
|
11752
|
+
} else if (typeof r.status === "number" && r.status !== 0) {
|
|
11753
|
+
result = { ok: false, error: `lazygit exited ${r.status}` };
|
|
11754
|
+
}
|
|
11755
|
+
} catch (err) {
|
|
11756
|
+
result = { ok: false, error: lazygitErrorMessage(err) };
|
|
11757
|
+
} finally {
|
|
11758
|
+
try {
|
|
11759
|
+
write(ALT_SCREEN_ENTER);
|
|
11760
|
+
} catch {
|
|
11761
|
+
}
|
|
11762
|
+
}
|
|
11763
|
+
return result;
|
|
11764
|
+
}
|
|
11765
|
+
function lazygitErrorMessage(err) {
|
|
11766
|
+
if (err instanceof Error) {
|
|
11767
|
+
const code = err.code;
|
|
11768
|
+
if (code === "ENOENT") {
|
|
11769
|
+
return "lazygit not found \xB7 install from https://github.com/jesseduffield/lazygit";
|
|
11770
|
+
}
|
|
11771
|
+
return err.message.length > 0 ? err.message : String(err);
|
|
11772
|
+
}
|
|
11773
|
+
return String(err);
|
|
11774
|
+
}
|
|
11775
|
+
var init_lazygit = __esm({
|
|
11776
|
+
"src/cli/tui/lazygit.ts"() {
|
|
11777
|
+
"use strict";
|
|
11778
|
+
init_escapes();
|
|
11779
|
+
}
|
|
11780
|
+
});
|
|
11781
|
+
|
|
11365
11782
|
// src/cli/tui/tuicr.ts
|
|
11366
|
-
import { spawnSync } from "child_process";
|
|
11783
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
11367
11784
|
function runTuicrInteractive(opts, deps = {}) {
|
|
11368
|
-
const run3 = deps.spawn ??
|
|
11785
|
+
const run3 = deps.spawn ?? spawnSync3;
|
|
11369
11786
|
const write = deps.write ?? ((text2) => process.stdout.write(text2));
|
|
11370
11787
|
const env = deps.env ?? process.env;
|
|
11371
11788
|
let result = { ok: true };
|
|
@@ -11544,6 +11961,13 @@ function CommitsPopup({
|
|
|
11544
11961
|
void yank2(showCommandForBackend(backendName, c.sha));
|
|
11545
11962
|
return;
|
|
11546
11963
|
}
|
|
11964
|
+
case "verb":
|
|
11965
|
+
if (action.key === "l") {
|
|
11966
|
+
const r = runLazygitInteractive({ cwd: projectRoot });
|
|
11967
|
+
if (!r.ok) onFooter?.(r.error ?? "lazygit failed", false, "error");
|
|
11968
|
+
else onFooter?.("lazygit", true, "info");
|
|
11969
|
+
}
|
|
11970
|
+
return;
|
|
11547
11971
|
}
|
|
11548
11972
|
};
|
|
11549
11973
|
usePopupActionQueue(popupActions, dispatchListAction);
|
|
@@ -11589,7 +12013,7 @@ function CommitsPopup({
|
|
|
11589
12013
|
PopupShell,
|
|
11590
12014
|
{
|
|
11591
12015
|
title: `Commits \xB7 ${formatBackend2(backendName)} (${safeCursor + 1}/${commits.length})`,
|
|
11592
|
-
hint: "y yanks VCS show command",
|
|
12016
|
+
hint: "y yanks VCS show command \xB7 l lazygit",
|
|
11593
12017
|
children: [
|
|
11594
12018
|
/* @__PURE__ */ jsx25(Box11, { flexDirection: "column", flexGrow: 1, children: visible.map((c, i) => {
|
|
11595
12019
|
const row2 = rows[i];
|
|
@@ -11646,6 +12070,7 @@ var init_commits2 = __esm({
|
|
|
11646
12070
|
init_vcs2();
|
|
11647
12071
|
init_columns();
|
|
11648
12072
|
init_keys();
|
|
12073
|
+
init_lazygit();
|
|
11649
12074
|
init_list_row();
|
|
11650
12075
|
init_popup_shell();
|
|
11651
12076
|
init_tuicr();
|
|
@@ -12081,13 +12506,14 @@ function InProgressPopup({
|
|
|
12081
12506
|
const now = Date.now();
|
|
12082
12507
|
const ages = tasks.map((t) => ageMs(t, now));
|
|
12083
12508
|
const { start, visible } = centredVisibleSlice(tasks, safeCursor, viewport);
|
|
12509
|
+
const agentLookup = agentByName(snapshot);
|
|
12084
12510
|
const rows = visible.map((t, i) => {
|
|
12085
12511
|
const absoluteIndex = start + i;
|
|
12086
12512
|
return [
|
|
12087
12513
|
glyphFor3(),
|
|
12088
12514
|
t.name,
|
|
12089
12515
|
t.status,
|
|
12090
|
-
t.ownerName
|
|
12516
|
+
formatAgentRefDisplayName(t.ownerName, agentLookup),
|
|
12091
12517
|
formatSinceClaim(ages[absoluteIndex] ?? null),
|
|
12092
12518
|
formatRoi(t.impact, t.effortDays),
|
|
12093
12519
|
t.title
|
|
@@ -12147,6 +12573,7 @@ var init_inprogress2 = __esm({
|
|
|
12147
12573
|
"src/cli/tui/popups/inprogress.tsx"() {
|
|
12148
12574
|
"use strict";
|
|
12149
12575
|
init_format();
|
|
12576
|
+
init_agent_display();
|
|
12150
12577
|
init_inprogress();
|
|
12151
12578
|
init_columns();
|
|
12152
12579
|
init_format_helpers();
|
|
@@ -12492,7 +12919,13 @@ function ReadyPopup({
|
|
|
12492
12919
|
) }) });
|
|
12493
12920
|
}
|
|
12494
12921
|
const { start, visible } = centredVisibleSlice(tasks, safeCursor, viewport);
|
|
12495
|
-
const
|
|
12922
|
+
const agentLookup = agentByName(snapshot);
|
|
12923
|
+
const rows = visible.map((t) => [
|
|
12924
|
+
t.name,
|
|
12925
|
+
t.status,
|
|
12926
|
+
formatAgentRefDisplayName(t.ownerName, agentLookup),
|
|
12927
|
+
t.title
|
|
12928
|
+
]);
|
|
12496
12929
|
const widths = layoutColumns(rows, COLUMN_SPECS18, contentWidth);
|
|
12497
12930
|
return /* @__PURE__ */ jsxs19(
|
|
12498
12931
|
PopupShell,
|
|
@@ -12550,6 +12983,7 @@ var init_ready2 = __esm({
|
|
|
12550
12983
|
"src/cli/tui/popups/ready.tsx"() {
|
|
12551
12984
|
"use strict";
|
|
12552
12985
|
init_format();
|
|
12986
|
+
init_agent_display();
|
|
12553
12987
|
init_columns();
|
|
12554
12988
|
init_keys();
|
|
12555
12989
|
init_list_row();
|
|
@@ -13362,9 +13796,10 @@ function WorkspacesPopup({
|
|
|
13362
13796
|
] });
|
|
13363
13797
|
}
|
|
13364
13798
|
const { start, visible } = centredVisibleSlice(workspaces, safeCursor, viewport);
|
|
13799
|
+
const agentLookup = agentByName(snapshot);
|
|
13365
13800
|
const rows = visible.map((w) => [
|
|
13366
13801
|
glyphFor5(w),
|
|
13367
|
-
w.agentName,
|
|
13802
|
+
formatAgentRefDisplayName(w.agentName, agentLookup),
|
|
13368
13803
|
w.backend,
|
|
13369
13804
|
formatBehind2(w.commitsBehindMain),
|
|
13370
13805
|
formatDirty(w.dirty),
|
|
@@ -13505,6 +13940,7 @@ var init_workspaces2 = __esm({
|
|
|
13505
13940
|
"use strict";
|
|
13506
13941
|
init_vcs2();
|
|
13507
13942
|
init_workspace();
|
|
13943
|
+
init_agent_display();
|
|
13508
13944
|
init_workspaces();
|
|
13509
13945
|
init_columns();
|
|
13510
13946
|
init_keys();
|
|
@@ -13928,8 +14364,8 @@ function rowWidth(workstreams, active, indexes, leftHidden, rightHidden, showHin
|
|
|
13928
14364
|
function chromeWidth(leftHidden, rightHidden, showHint) {
|
|
13929
14365
|
return stringWidth4(PREFIX) + counterWidth("\u2039", leftHidden) + counterWidth("\u203A", rightHidden) + (showHint ? stringWidth4(leftHidden > 0 || rightHidden > 0 ? NARROW_HINT : WIDE_HINT) : 0);
|
|
13930
14366
|
}
|
|
13931
|
-
function counterWidth(prefix,
|
|
13932
|
-
return
|
|
14367
|
+
function counterWidth(prefix, count2) {
|
|
14368
|
+
return count2 > 0 ? stringWidth4(`${prefix}${count2} `) : 0;
|
|
13933
14369
|
}
|
|
13934
14370
|
function truncateToWidth(text2, width) {
|
|
13935
14371
|
if (width <= 0) return "";
|
|
@@ -13964,22 +14400,25 @@ import { jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
|
13964
14400
|
function TabStrip({
|
|
13965
14401
|
workstreams,
|
|
13966
14402
|
active,
|
|
13967
|
-
terminalColumns
|
|
14403
|
+
terminalColumns,
|
|
14404
|
+
parked
|
|
13968
14405
|
}) {
|
|
13969
14406
|
if (workstreams.length <= 1) return null;
|
|
13970
14407
|
const layout = layoutTabStrip(workstreams, active, terminalColumns);
|
|
13971
14408
|
if (layout === null) return null;
|
|
14409
|
+
const isParked = (name) => parked?.has(name) ?? false;
|
|
14410
|
+
const decorate = (name) => isParked(name) ? `~${name}` : name;
|
|
13972
14411
|
const tabs = [];
|
|
13973
14412
|
for (let i = 0; i < layout.visible.length; i++) {
|
|
13974
14413
|
const tab = layout.visible[i];
|
|
13975
14414
|
if (tab === void 0) continue;
|
|
13976
14415
|
if (tab.isActive) {
|
|
13977
14416
|
tabs.push(
|
|
13978
|
-
/* @__PURE__ */ jsx35(Text28, { bold: true, color: "cyan", children: `\u25B8 ${tab.name}` }, `t-${i}`)
|
|
14417
|
+
/* @__PURE__ */ jsx35(Text28, { bold: true, color: "cyan", children: `\u25B8 ${decorate(tab.name)}` }, `t-${i}`)
|
|
13979
14418
|
);
|
|
13980
14419
|
} else {
|
|
13981
14420
|
tabs.push(
|
|
13982
|
-
/* @__PURE__ */ jsx35(Text28, { dimColor: true, children: tab.name }, `t-${i}`)
|
|
14421
|
+
/* @__PURE__ */ jsx35(Text28, { dimColor: true, children: decorate(tab.name) }, `t-${i}`)
|
|
13983
14422
|
);
|
|
13984
14423
|
}
|
|
13985
14424
|
if (i < layout.visible.length - 1) {
|
|
@@ -14095,7 +14534,7 @@ var init_yank = __esm({
|
|
|
14095
14534
|
|
|
14096
14535
|
// src/cli/tui/app.tsx
|
|
14097
14536
|
import { Box as Box22, Text as Text29, useApp, useInput as useInput14, useStdin, useStdout as useStdout16 } from "ink";
|
|
14098
|
-
import { useCallback as useCallback7, useEffect as useEffect10, useRef as useRef8, useState as useState18 } from "react";
|
|
14537
|
+
import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo9, useRef as useRef8, useState as useState18 } from "react";
|
|
14099
14538
|
import { jsx as jsx36, jsxs as jsxs25 } from "react/jsx-runtime";
|
|
14100
14539
|
function dashboardAvailableRows(rows, opts) {
|
|
14101
14540
|
const tabRows = opts.hasTabStrip ? 1 : 0;
|
|
@@ -14154,6 +14593,14 @@ function App({ db, workstreams, initialActive = 0 }) {
|
|
|
14154
14593
|
const safeActive = Math.max(0, Math.min(activeWs, workstreams.length - 1));
|
|
14155
14594
|
const workstream = workstreams[safeActive] ?? "";
|
|
14156
14595
|
const snap = useDashboardSnapshot(db, workstream, tickMs, true, refreshNonce);
|
|
14596
|
+
const parkedSet = useMemo9(() => {
|
|
14597
|
+
void snap.slowTickNonce;
|
|
14598
|
+
const set = /* @__PURE__ */ new Set();
|
|
14599
|
+
for (const ws of workstreams) {
|
|
14600
|
+
if (parkedStatus(db, ws).parked) set.add(ws);
|
|
14601
|
+
}
|
|
14602
|
+
return set;
|
|
14603
|
+
}, [db, workstreams, snap.slowTickNonce]);
|
|
14157
14604
|
const { stdout } = useStdout16();
|
|
14158
14605
|
const cols = stdout.columns ?? 80;
|
|
14159
14606
|
const rows = stdout.rows ?? 24;
|
|
@@ -14333,7 +14780,15 @@ function App({ db, workstreams, initialActive = 0 }) {
|
|
|
14333
14780
|
] });
|
|
14334
14781
|
}
|
|
14335
14782
|
return /* @__PURE__ */ jsxs25(Box22, { flexDirection: "column", height: rows, overflow: "hidden", children: [
|
|
14336
|
-
/* @__PURE__ */ jsx36(
|
|
14783
|
+
/* @__PURE__ */ jsx36(
|
|
14784
|
+
TabStrip,
|
|
14785
|
+
{
|
|
14786
|
+
workstreams,
|
|
14787
|
+
active: safeActive,
|
|
14788
|
+
terminalColumns: cols,
|
|
14789
|
+
parked: parkedSet
|
|
14790
|
+
}
|
|
14791
|
+
),
|
|
14337
14792
|
hasSnapshotError && /* @__PURE__ */ jsx36(Box22, { borderStyle: "round", borderColor: "red", paddingX: 1, children: /* @__PURE__ */ jsxs25(Text29, { color: "red", children: [
|
|
14338
14793
|
"snapshot error: ",
|
|
14339
14794
|
snap.error
|
|
@@ -14390,7 +14845,8 @@ function buildDashboardLayoutModel(cols, rows, visibility, snapshot) {
|
|
|
14390
14845
|
const cullBudget = firstCull.hidden.length > 0 ? Math.max(1, rows - 1) : rows;
|
|
14391
14846
|
const culled = cullBudget === rows ? firstCull : cullCardsForRows(visible, cullBudget);
|
|
14392
14847
|
const cardsRows = culled.hidden.length > 0 ? Math.max(1, rows - 1) : rows;
|
|
14393
|
-
const
|
|
14848
|
+
const packed = layoutColumns2(cols, culled.cards);
|
|
14849
|
+
const assignments = balanceColumns(packed, (id) => dataCountForCard(id, snapshot));
|
|
14394
14850
|
const widths = columnWidths(cols, assignments.length);
|
|
14395
14851
|
const budgetsByColumn = assignments.map(
|
|
14396
14852
|
(assignment) => capRowBudgetsForColumn(
|
|
@@ -14468,6 +14924,7 @@ var CARD_REGISTRY, POPUP_REGISTRY, DASHBOARD_MIN_ROWS, POPUP_CHROME_TOP;
|
|
|
14468
14924
|
var init_app = __esm({
|
|
14469
14925
|
"src/cli/tui/app.tsx"() {
|
|
14470
14926
|
"use strict";
|
|
14927
|
+
init_parked();
|
|
14471
14928
|
init_agents2();
|
|
14472
14929
|
init_blocked();
|
|
14473
14930
|
init_commits();
|
|
@@ -14564,7 +15021,7 @@ var init_tui = __esm({
|
|
|
14564
15021
|
init_agents();
|
|
14565
15022
|
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "fs";
|
|
14566
15023
|
import { dirname as dirname7, join as join10 } from "path";
|
|
14567
|
-
import { fileURLToPath as
|
|
15024
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
14568
15025
|
import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
14569
15026
|
|
|
14570
15027
|
// src/cli/agents.ts
|
|
@@ -16169,10 +16626,10 @@ async function cmdAttach(db, rawName, opts) {
|
|
|
16169
16626
|
const { name } = await resolveEntityRef(db, rawName, opts, "agent");
|
|
16170
16627
|
const workstream = await resolveWorkstream(opts.workstream);
|
|
16171
16628
|
const sessionName = `mu-${workstream}`;
|
|
16629
|
+
const view = await listLiveAgents(db, { workstream });
|
|
16172
16630
|
if (!await sessionExists(sessionName)) {
|
|
16173
16631
|
throw new UsageError(`workstream "${workstream}" has no tmux session yet`);
|
|
16174
16632
|
}
|
|
16175
|
-
const view = await listLiveAgents(db, { workstream, mode: "status-only" });
|
|
16176
16633
|
const agent = view.agents.find((a) => a.name === name);
|
|
16177
16634
|
if (!agent) {
|
|
16178
16635
|
throw new AgentNotFoundError(name);
|
|
@@ -16443,9 +16900,15 @@ async function cmdArchiveAdd(db, label2, opts) {
|
|
|
16443
16900
|
command: `mu archive add ${label2} -w ${workstream}`
|
|
16444
16901
|
},
|
|
16445
16902
|
{
|
|
16446
|
-
intent: opts.destroy ? "
|
|
16447
|
-
command: opts.destroy ?
|
|
16448
|
-
}
|
|
16903
|
+
intent: opts.destroy ? "Restore the archived workstream under a fresh name" : "Destroy the source workstream now that its memory is preserved",
|
|
16904
|
+
command: opts.destroy ? `mu archive restore ${label2} --as <new-workstream> --source ${workstream}` : `mu archive add ${label2} -w ${workstream} --destroy`
|
|
16905
|
+
},
|
|
16906
|
+
...opts.destroy ? [
|
|
16907
|
+
{
|
|
16908
|
+
intent: "Undo the destroy (DB only; tmux NOT rolled back)",
|
|
16909
|
+
command: "mu undo --yes"
|
|
16910
|
+
}
|
|
16911
|
+
] : []
|
|
16449
16912
|
]);
|
|
16450
16913
|
}
|
|
16451
16914
|
async function cmdArchiveRemove(db, label2, opts) {
|
|
@@ -16471,6 +16934,35 @@ async function cmdArchiveRemove(db, label2, opts) {
|
|
|
16471
16934
|
}
|
|
16472
16935
|
]);
|
|
16473
16936
|
}
|
|
16937
|
+
async function cmdArchiveRestore(db, label2, opts = {}) {
|
|
16938
|
+
if (!opts.as || opts.as.trim().length === 0) {
|
|
16939
|
+
throw new UsageError("--as <new-ws-name> is required for `mu archive restore`");
|
|
16940
|
+
}
|
|
16941
|
+
const result = restoreArchive(db, label2, opts.as, {
|
|
16942
|
+
sourceWorkstream: opts.source
|
|
16943
|
+
});
|
|
16944
|
+
const nextSteps = [
|
|
16945
|
+
{ intent: "Inspect restored tasks", command: `mu task list -w ${result.workstreamName}` },
|
|
16946
|
+
{ intent: "Undo (a snapshot was taken before the restore)", command: "mu undo --yes" }
|
|
16947
|
+
];
|
|
16948
|
+
if (opts.json) {
|
|
16949
|
+
emitJson({ ...result, nextSteps });
|
|
16950
|
+
return;
|
|
16951
|
+
}
|
|
16952
|
+
console.log(
|
|
16953
|
+
`Restored archive ${pc.bold(label2)} source ${pc.bold(result.sourceWorkstream)} as workstream ${pc.bold(
|
|
16954
|
+
result.workstreamName
|
|
16955
|
+
)} ${pc.dim(
|
|
16956
|
+
`(tasks=${result.restoredTasks}, edges=${result.restoredEdges}, notes=${result.restoredNotes})`
|
|
16957
|
+
)}`
|
|
16958
|
+
);
|
|
16959
|
+
console.log(
|
|
16960
|
+
pc.dim(
|
|
16961
|
+
"agents, workspace_path, and agent_logs are not restored (archives preserve task graph rows, not live panes or the live event log)."
|
|
16962
|
+
)
|
|
16963
|
+
);
|
|
16964
|
+
printNextSteps(nextSteps);
|
|
16965
|
+
}
|
|
16474
16966
|
async function cmdArchiveDelete(db, label2, opts = {}) {
|
|
16475
16967
|
const summary = getArchive(db, label2);
|
|
16476
16968
|
if (!opts.yes) {
|
|
@@ -16484,6 +16976,10 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
|
|
|
16484
16976
|
{
|
|
16485
16977
|
intent: "Confirm and actually delete (a snapshot is taken first)",
|
|
16486
16978
|
command: `mu archive delete ${label2} --yes`
|
|
16979
|
+
},
|
|
16980
|
+
{
|
|
16981
|
+
intent: "Recover a source before deleting the archive",
|
|
16982
|
+
command: `mu archive restore ${label2} --as <new-workstream> --source <workstream>`
|
|
16487
16983
|
}
|
|
16488
16984
|
]
|
|
16489
16985
|
});
|
|
@@ -16507,6 +17003,10 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
|
|
|
16507
17003
|
intent: "Confirm and actually delete",
|
|
16508
17004
|
command: `mu archive delete ${label2} --yes`
|
|
16509
17005
|
},
|
|
17006
|
+
{
|
|
17007
|
+
intent: "Recover a source before deleting the archive",
|
|
17008
|
+
command: `mu archive restore ${label2} --as <new-workstream> --source <workstream>`
|
|
17009
|
+
},
|
|
16510
17010
|
{
|
|
16511
17011
|
intent: "Surgically remove a single source workstream instead",
|
|
16512
17012
|
command: `mu archive remove ${label2} -w <workstream>`
|
|
@@ -16524,7 +17024,7 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
|
|
|
16524
17024
|
removedTasks: summary.totalTasks,
|
|
16525
17025
|
nextSteps: [
|
|
16526
17026
|
{
|
|
16527
|
-
intent: "
|
|
17027
|
+
intent: "Recover the deleted archive (a snapshot was taken before the delete)",
|
|
16528
17028
|
command: "mu undo --yes"
|
|
16529
17029
|
}
|
|
16530
17030
|
]
|
|
@@ -16538,7 +17038,7 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
|
|
|
16538
17038
|
);
|
|
16539
17039
|
printNextSteps([
|
|
16540
17040
|
{
|
|
16541
|
-
intent: "
|
|
17041
|
+
intent: "Recover the deleted archive (a snapshot was taken before the delete)",
|
|
16542
17042
|
command: "mu undo --yes"
|
|
16543
17043
|
}
|
|
16544
17044
|
]);
|
|
@@ -16605,7 +17105,11 @@ async function cmdArchiveExport(db, label2, opts = {}) {
|
|
|
16605
17105
|
0
|
|
16606
17106
|
);
|
|
16607
17107
|
const nextSteps = [
|
|
16608
|
-
{ intent: "Browse the bucket", command: `ls ${result.outDir}` },
|
|
17108
|
+
{ intent: "Browse the read-only human/git/docs bucket", command: `ls ${result.outDir}` },
|
|
17109
|
+
{
|
|
17110
|
+
intent: "Restore losslessly from the archive (not from this bucket)",
|
|
17111
|
+
command: `mu archive restore ${label2} --as <new-workstream> --source <workstream>`
|
|
17112
|
+
},
|
|
16609
17113
|
{
|
|
16610
17114
|
intent: "Re-export to refresh (additive; existing source-ws subdirs untouched)",
|
|
16611
17115
|
command: `mu archive export ${label2} --out ${result.outDir}`
|
|
@@ -16635,6 +17139,11 @@ async function cmdArchiveExport(db, label2, opts = {}) {
|
|
|
16635
17139
|
`(sources=${result.sourceCount}, tasks=${totalTasks}, written=${result.written}, unchanged=${result.unchanged}, preserved=${result.preserved})`
|
|
16636
17140
|
)}`
|
|
16637
17141
|
);
|
|
17142
|
+
console.log(
|
|
17143
|
+
pc.dim(
|
|
17144
|
+
"This bucket is a read-only artifact for humans/git/docs; use `mu archive restore` for lossless un-archive."
|
|
17145
|
+
)
|
|
17146
|
+
);
|
|
16638
17147
|
printNextSteps(nextSteps);
|
|
16639
17148
|
}
|
|
16640
17149
|
function wireArchiveCommands(program) {
|
|
@@ -16658,14 +17167,23 @@ function wireArchiveCommands(program) {
|
|
|
16658
17167
|
return handle((db) => cmdArchiveShow(db, label2, opts), this)();
|
|
16659
17168
|
});
|
|
16660
17169
|
archive.command("add <label>").description(
|
|
16661
|
-
"Snapshot a workstream's task graph (tasks + edges + notes + events) into an existing archive. Idempotent at (archive, source_workstream) granularity. With --destroy, cascades to `mu workstream destroy --yes` after the archive succeeds
|
|
17170
|
+
"Snapshot a workstream's task graph (tasks + edges + notes + events) into an existing archive. Idempotent at (archive, source_workstream) granularity. With --destroy, cascades to `mu workstream destroy --yes` after the archive succeeds; reverse with `mu archive restore <label> --as <new>`."
|
|
16662
17171
|
).option(...WORKSTREAM_OPT).option(
|
|
16663
17172
|
"--destroy",
|
|
16664
|
-
"After a successful archive, also destroy the source workstream (kills tmux + frees workspaces + cascade-deletes DB rows)."
|
|
17173
|
+
"After a successful archive, also destroy the source workstream (kills tmux + frees workspaces + cascade-deletes DB rows). Recover later with `mu archive restore <label> --as <new>`."
|
|
16665
17174
|
).option(...JSON_OPT).action(function(label2) {
|
|
16666
17175
|
const opts = this.optsWithGlobals();
|
|
16667
17176
|
return handle((db) => cmdArchiveAdd(db, label2, opts), this)();
|
|
16668
17177
|
});
|
|
17178
|
+
archive.command("restore <label>").description(
|
|
17179
|
+
"Restore one archived source workstream into a fresh workstream directly from archived_* tables. This is the lossless un-archive path; does not restore agents, workspace_path, or agent_logs (archives do not snapshot live panes or the live event log)."
|
|
17180
|
+
).requiredOption("--as <new-ws-name>", "fresh workstream name to create; refuses collisions").option(
|
|
17181
|
+
"--source <orig-ws-name>",
|
|
17182
|
+
"required when the archive contains multiple source workstreams"
|
|
17183
|
+
).option(...JSON_OPT).action(function(label2) {
|
|
17184
|
+
const opts = this.opts();
|
|
17185
|
+
return handle((db) => cmdArchiveRestore(db, label2, opts), this)();
|
|
17186
|
+
});
|
|
16669
17187
|
archive.command("remove <label>").description(
|
|
16670
17188
|
"Surgically remove a single source workstream's contribution from an archive (rare; recovery). Other source workstreams' rows are untouched."
|
|
16671
17189
|
).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(label2) {
|
|
@@ -16679,7 +17197,7 @@ function wireArchiveCommands(program) {
|
|
|
16679
17197
|
return handle((db) => cmdArchiveSearch(db, pattern, opts), this)();
|
|
16680
17198
|
});
|
|
16681
17199
|
archive.command("export <label>").description(
|
|
16682
|
-
"Render every source workstream in an archive to a bucket directory of markdown
|
|
17200
|
+
"Render every source workstream in an archive to a READ-ONLY bucket directory of markdown for humans/git/docs. Idempotent + additive: re-running refreshes only changed task files. For lossless un-archive, use `mu archive restore`."
|
|
16683
17201
|
).option("--out <dir>", "output directory (the bucket); required").option(...JSON_OPT).action(function(label2) {
|
|
16684
17202
|
const opts = this.opts();
|
|
16685
17203
|
return handle((db) => cmdArchiveExport(db, label2, opts), this)();
|
|
@@ -16692,28 +17210,1049 @@ function wireArchiveCommands(program) {
|
|
|
16692
17210
|
});
|
|
16693
17211
|
}
|
|
16694
17212
|
|
|
16695
|
-
// src/
|
|
16696
|
-
init_agents();
|
|
17213
|
+
// src/db-sync.ts
|
|
16697
17214
|
init_db();
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
17215
|
+
init_logs();
|
|
17216
|
+
init_snapshots();
|
|
17217
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
17218
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync2, unlinkSync as unlinkSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
17219
|
+
import { hostname as hostname2 } from "os";
|
|
17220
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
17221
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
17222
|
+
|
|
17223
|
+
// src/db-sync-replay.ts
|
|
17224
|
+
init_db();
|
|
17225
|
+
init_snapshots();
|
|
17226
|
+
import { createHash as createHash2 } from "crypto";
|
|
17227
|
+
var DbReplayWorkstreamMissingError = class extends Error {
|
|
17228
|
+
constructor(workstream) {
|
|
17229
|
+
super(
|
|
17230
|
+
`replay sidecar is for workstream "${workstream}", which does not exist locally; restore it first via mu db import or mu archive restore`
|
|
17231
|
+
);
|
|
17232
|
+
this.workstream = workstream;
|
|
16704
17233
|
}
|
|
16705
|
-
|
|
16706
|
-
|
|
17234
|
+
workstream;
|
|
17235
|
+
name = "DbReplayWorkstreamMissingError";
|
|
17236
|
+
errorNextSteps() {
|
|
17237
|
+
return [
|
|
17238
|
+
{
|
|
17239
|
+
intent: "Restore this workstream from a DB export",
|
|
17240
|
+
command: "mu db import <file> --apply"
|
|
17241
|
+
},
|
|
17242
|
+
{
|
|
17243
|
+
intent: "Or restore it from an archive",
|
|
17244
|
+
command: `mu archive restore <label> --as ${this.workstream}`
|
|
17245
|
+
}
|
|
17246
|
+
];
|
|
17247
|
+
}
|
|
17248
|
+
};
|
|
17249
|
+
var DbReplayLocalIdConflictError = class extends Error {
|
|
17250
|
+
constructor(workstream, conflicts) {
|
|
17251
|
+
super(
|
|
17252
|
+
`sidecar task id collides with different local content in ${workstream}: ${conflicts.map(
|
|
17253
|
+
(c) => `${c.localId} (local: ${c.local.status} ${JSON.stringify(c.local.title)}; sidecar: ${c.sidecar.status} ${JSON.stringify(c.sidecar.title)})`
|
|
17254
|
+
).join(", ")}`
|
|
17255
|
+
);
|
|
17256
|
+
this.workstream = workstream;
|
|
17257
|
+
this.conflicts = conflicts;
|
|
17258
|
+
}
|
|
17259
|
+
workstream;
|
|
17260
|
+
conflicts;
|
|
17261
|
+
name = "DbReplayLocalIdConflictError";
|
|
17262
|
+
errorNextSteps() {
|
|
17263
|
+
const first = this.conflicts[0];
|
|
17264
|
+
return [
|
|
17265
|
+
{
|
|
17266
|
+
intent: "Create a renamed local task manually, then replay notes if desired",
|
|
17267
|
+
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>`
|
|
17268
|
+
},
|
|
17269
|
+
{
|
|
17270
|
+
intent: "Skip the colliding id and replay another task",
|
|
17271
|
+
command: "mu db replay <sidecar> --task <other-id> --apply"
|
|
17272
|
+
}
|
|
17273
|
+
];
|
|
17274
|
+
}
|
|
17275
|
+
};
|
|
17276
|
+
function replayDb(db, file, opts = {}) {
|
|
17277
|
+
const sidecarDb = openDb({ path: file, readonly: true });
|
|
16707
17278
|
try {
|
|
16708
|
-
const
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
|
|
17279
|
+
const plan = buildReplayPlan(db, sidecarDb, file);
|
|
17280
|
+
const taskFilter = new Set(opts.tasks ?? []);
|
|
17281
|
+
const noteFilter = new Set(opts.notes ?? []);
|
|
17282
|
+
const selectedTaskIds = opts.all === true ? new Set(plan.tasks.map((t) => t.localId)) : taskFilter;
|
|
17283
|
+
const selectedNoteIds = opts.all === true ? new Set(plan.notes.map((n) => n.taskLocalId)) : noteFilter;
|
|
17284
|
+
const hasSelectors = opts.all === true || selectedTaskIds.size > 0 || selectedNoteIds.size > 0;
|
|
17285
|
+
const noteTaskIds = /* @__PURE__ */ new Set([...selectedNoteIds, ...selectedTaskIds]);
|
|
17286
|
+
const hasWrites = plan.tasks.some((t) => selectedTaskIds.has(t.localId)) || plan.notes.some((n) => noteTaskIds.has(n.taskLocalId)) || plan.edges.some(
|
|
17287
|
+
(e) => opts.all === true || selectedTaskIds.has(e.fromLocalId) || selectedTaskIds.has(e.toLocalId)
|
|
17288
|
+
);
|
|
17289
|
+
const relevantConflicts = opts.all === true ? plan.conflicts : plan.conflicts.filter((c) => selectedTaskIds.has(c.localId));
|
|
17290
|
+
if (relevantConflicts.length > 0) {
|
|
17291
|
+
throw new DbReplayLocalIdConflictError(plan.workstream, relevantConflicts);
|
|
17292
|
+
}
|
|
17293
|
+
if (opts.apply !== true || !hasSelectors) return replayResult(plan, true, false);
|
|
17294
|
+
if (!hasWrites) return replayResult(plan, false, true);
|
|
17295
|
+
const snapshot = captureSnapshot(db, `db replay ${file}`, null);
|
|
17296
|
+
const applied = applyReplayPlan(db, plan, selectedTaskIds, selectedNoteIds, opts.all === true);
|
|
17297
|
+
return { ...replayResult(plan, false, true), snapshotId: snapshot.id, ...applied };
|
|
17298
|
+
} finally {
|
|
17299
|
+
sidecarDb.close();
|
|
16712
17300
|
}
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
17301
|
+
}
|
|
17302
|
+
function buildReplayPlan(localDb, sidecarDb, sourceFile) {
|
|
17303
|
+
const sidecarWorkstreams = listLocalWorkstreams(sidecarDb);
|
|
17304
|
+
const sidecarWs = sidecarWorkstreams[0];
|
|
17305
|
+
if (sidecarWorkstreams.length !== 1 || !sidecarWs) {
|
|
17306
|
+
throw new Error(
|
|
17307
|
+
`replay sidecar must contain exactly one workstream; found ${sidecarWorkstreams.length}`
|
|
17308
|
+
);
|
|
17309
|
+
}
|
|
17310
|
+
const localWs = listLocalWorkstreams(localDb).find((w) => w.name === sidecarWs.name);
|
|
17311
|
+
if (!localWs) throw new DbReplayWorkstreamMissingError(sidecarWs.name);
|
|
17312
|
+
const localTasks = new Map(
|
|
17313
|
+
localDb.prepare("SELECT local_id, title, status FROM tasks WHERE workstream_id = ?").all(localWs.id).map((t) => [t.local_id, t])
|
|
17314
|
+
);
|
|
17315
|
+
const tasks = [];
|
|
17316
|
+
const conflicts = [];
|
|
17317
|
+
for (const task of listReplayTasks(sidecarDb, sidecarWs.id)) {
|
|
17318
|
+
const local = localTasks.get(task.localId);
|
|
17319
|
+
if (!local) tasks.push(task);
|
|
17320
|
+
else if (local.title !== task.title || local.status !== task.status) {
|
|
17321
|
+
conflicts.push({
|
|
17322
|
+
localId: task.localId,
|
|
17323
|
+
local: { title: local.title, status: local.status },
|
|
17324
|
+
sidecar: { title: task.title, status: task.status }
|
|
17325
|
+
});
|
|
17326
|
+
}
|
|
17327
|
+
}
|
|
17328
|
+
const localNoteHashes = new Set(listReplayNotes(localDb, localWs.id).map((n) => n.hash));
|
|
17329
|
+
const localEdges = new Set(listReplayEdges(localDb, localWs.id).map(edgeKey));
|
|
17330
|
+
return {
|
|
17331
|
+
sourceFile,
|
|
17332
|
+
workstream: sidecarWs.name,
|
|
17333
|
+
tasks,
|
|
17334
|
+
notes: listReplayNotes(sidecarDb, sidecarWs.id).filter((n) => !localNoteHashes.has(n.hash)),
|
|
17335
|
+
edges: listReplayEdges(sidecarDb, sidecarWs.id).filter((e) => !localEdges.has(edgeKey(e))),
|
|
17336
|
+
conflicts
|
|
17337
|
+
};
|
|
17338
|
+
}
|
|
17339
|
+
function applyReplayPlan(db, plan, selectedTaskIds, selectedNoteIds, replayAllEdges) {
|
|
17340
|
+
const warnings = [];
|
|
17341
|
+
const added = db.transaction(() => {
|
|
17342
|
+
const wsId = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(plan.workstream)?.id;
|
|
17343
|
+
if (wsId === void 0) throw new DbReplayWorkstreamMissingError(plan.workstream);
|
|
17344
|
+
const taskIds = new Set(selectedTaskIds);
|
|
17345
|
+
const noteTaskIds = /* @__PURE__ */ new Set([...selectedNoteIds, ...taskIds]);
|
|
17346
|
+
let tasks = 0;
|
|
17347
|
+
let notes = 0;
|
|
17348
|
+
let edges = 0;
|
|
17349
|
+
const insertTask = db.prepare(
|
|
17350
|
+
`INSERT OR IGNORE INTO tasks (workstream_id, local_id, title, status, impact, effort_days, created_at, updated_at)
|
|
17351
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
17352
|
+
);
|
|
17353
|
+
for (const task of plan.tasks) {
|
|
17354
|
+
if (!taskIds.has(task.localId)) continue;
|
|
17355
|
+
const result = insertTask.run(
|
|
17356
|
+
wsId,
|
|
17357
|
+
task.localId,
|
|
17358
|
+
task.title,
|
|
17359
|
+
task.status,
|
|
17360
|
+
task.impact,
|
|
17361
|
+
task.effortDays,
|
|
17362
|
+
task.createdAt,
|
|
17363
|
+
task.updatedAt
|
|
17364
|
+
);
|
|
17365
|
+
if (result.changes > 0) tasks += 1;
|
|
17366
|
+
}
|
|
17367
|
+
const existingNoteHashes = new Set(listReplayNotes(db, wsId).map((n) => n.hash));
|
|
17368
|
+
const insertNote = db.prepare(
|
|
17369
|
+
`INSERT INTO task_notes (task_id, author, content, created_at)
|
|
17370
|
+
SELECT id, ?, ?, ? FROM tasks WHERE workstream_id = ? AND local_id = ?`
|
|
17371
|
+
);
|
|
17372
|
+
for (const note of plan.notes) {
|
|
17373
|
+
if (!noteTaskIds.has(note.taskLocalId) || existingNoteHashes.has(note.hash)) continue;
|
|
17374
|
+
const result = insertNote.run(
|
|
17375
|
+
note.author,
|
|
17376
|
+
note.content,
|
|
17377
|
+
note.createdAt,
|
|
17378
|
+
wsId,
|
|
17379
|
+
note.taskLocalId
|
|
17380
|
+
);
|
|
17381
|
+
if (result.changes > 0) {
|
|
17382
|
+
notes += 1;
|
|
17383
|
+
existingNoteHashes.add(note.hash);
|
|
17384
|
+
}
|
|
17385
|
+
}
|
|
17386
|
+
const insertEdge = db.prepare(
|
|
17387
|
+
`INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
|
|
17388
|
+
SELECT f.id, t.id, ?
|
|
17389
|
+
FROM tasks f, tasks t
|
|
17390
|
+
WHERE f.workstream_id = ? AND f.local_id = ?
|
|
17391
|
+
AND t.workstream_id = ? AND t.local_id = ?`
|
|
17392
|
+
);
|
|
17393
|
+
for (const edge of plan.edges) {
|
|
17394
|
+
if (!replayAllEdges && !taskIds.has(edge.fromLocalId) && !taskIds.has(edge.toLocalId)) {
|
|
17395
|
+
continue;
|
|
17396
|
+
}
|
|
17397
|
+
if (!hasTask(db, wsId, edge.fromLocalId) || !hasTask(db, wsId, edge.toLocalId)) {
|
|
17398
|
+
warnings.push(
|
|
17399
|
+
`skipped edge ${edge.fromLocalId} -> ${edge.toLocalId}: one endpoint is missing locally`
|
|
17400
|
+
);
|
|
17401
|
+
continue;
|
|
17402
|
+
}
|
|
17403
|
+
const result = insertEdge.run(edge.createdAt, wsId, edge.fromLocalId, wsId, edge.toLocalId);
|
|
17404
|
+
if (result.changes > 0) edges += 1;
|
|
17405
|
+
}
|
|
17406
|
+
return { tasks, notes, edges };
|
|
17407
|
+
})();
|
|
17408
|
+
return { added, warnings };
|
|
17409
|
+
}
|
|
17410
|
+
function replayResult(plan, dryRun, applied) {
|
|
17411
|
+
return { ...plan, dryRun, applied, added: { tasks: 0, notes: 0, edges: 0 }, warnings: [] };
|
|
17412
|
+
}
|
|
17413
|
+
function listLocalWorkstreams(db) {
|
|
17414
|
+
return db.prepare("SELECT id, name FROM workstreams ORDER BY name").all();
|
|
17415
|
+
}
|
|
17416
|
+
function listReplayTasks(db, wsId) {
|
|
17417
|
+
return db.prepare(
|
|
17418
|
+
`SELECT local_id, title, status, impact, effort_days, created_at, updated_at
|
|
17419
|
+
FROM tasks
|
|
17420
|
+
WHERE workstream_id = ?
|
|
17421
|
+
ORDER BY local_id`
|
|
17422
|
+
).all(wsId).map((row2) => ({
|
|
17423
|
+
localId: row2.local_id,
|
|
17424
|
+
title: row2.title,
|
|
17425
|
+
status: row2.status,
|
|
17426
|
+
impact: row2.impact,
|
|
17427
|
+
effortDays: row2.effort_days,
|
|
17428
|
+
createdAt: row2.created_at,
|
|
17429
|
+
updatedAt: row2.updated_at
|
|
17430
|
+
}));
|
|
17431
|
+
}
|
|
17432
|
+
function listReplayNotes(db, wsId) {
|
|
17433
|
+
const rows = db.prepare(
|
|
17434
|
+
`SELECT t.local_id AS taskLocalId, n.author, n.content, n.created_at AS createdAt
|
|
17435
|
+
FROM task_notes n
|
|
17436
|
+
JOIN tasks t ON t.id = n.task_id
|
|
17437
|
+
WHERE t.workstream_id = ?
|
|
17438
|
+
ORDER BY n.created_at, n.id`
|
|
17439
|
+
).all(wsId);
|
|
17440
|
+
return rows.map((row2) => ({ ...row2, hash: noteHash(row2) }));
|
|
17441
|
+
}
|
|
17442
|
+
function listReplayEdges(db, wsId) {
|
|
17443
|
+
return db.prepare(
|
|
17444
|
+
`SELECT f.local_id AS fromLocalId, t.local_id AS toLocalId, e.created_at AS createdAt
|
|
17445
|
+
FROM task_edges e
|
|
17446
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
17447
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
17448
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?
|
|
17449
|
+
ORDER BY f.local_id, t.local_id`
|
|
17450
|
+
).all(wsId, wsId);
|
|
17451
|
+
}
|
|
17452
|
+
function noteHash(note) {
|
|
17453
|
+
return createHash2("sha256").update(`${note.taskLocalId}\0${note.content}\0${note.createdAt}`).digest("hex");
|
|
17454
|
+
}
|
|
17455
|
+
function edgeKey(edge) {
|
|
17456
|
+
return `${edge.fromLocalId}\0${edge.toLocalId}`;
|
|
17457
|
+
}
|
|
17458
|
+
function hasTask(db, wsId, localId) {
|
|
17459
|
+
return db.prepare("SELECT 1 FROM tasks WHERE workstream_id = ? AND local_id = ?").get(wsId, localId) !== void 0;
|
|
17460
|
+
}
|
|
17461
|
+
function shellQuote2(s) {
|
|
17462
|
+
return `'${s.replace(/'/g, `'"'"'`)}'`;
|
|
17463
|
+
}
|
|
17464
|
+
|
|
17465
|
+
// src/db-sync.ts
|
|
17466
|
+
var DbExportTargetExistsError = class extends Error {
|
|
17467
|
+
constructor(file) {
|
|
17468
|
+
super(`DB export target already exists: ${file}`);
|
|
17469
|
+
this.file = file;
|
|
17470
|
+
}
|
|
17471
|
+
file;
|
|
17472
|
+
name = "DbExportTargetExistsError";
|
|
17473
|
+
errorNextSteps() {
|
|
17474
|
+
return [
|
|
17475
|
+
{ intent: "Choose a different target", command: "mu db export <new-file>" },
|
|
17476
|
+
{ intent: "Overwrite this target", command: `mu db export ${shellQuote3(this.file)} --force` }
|
|
17477
|
+
];
|
|
17478
|
+
}
|
|
17479
|
+
};
|
|
17480
|
+
var DbImportManifestMissingError = class extends Error {
|
|
17481
|
+
constructor(manifestPath) {
|
|
17482
|
+
super(`DB import manifest not found: ${manifestPath}`);
|
|
17483
|
+
this.manifestPath = manifestPath;
|
|
17484
|
+
}
|
|
17485
|
+
manifestPath;
|
|
17486
|
+
name = "DbImportManifestMissingError";
|
|
17487
|
+
errorNextSteps() {
|
|
17488
|
+
return [
|
|
17489
|
+
{ intent: "Export the DB with its sidecar", command: "mu db export /tmp/mu.db --force" },
|
|
17490
|
+
{ intent: "Copy the sidecar too", command: `scp <host>:${shellQuote3(this.manifestPath)} .` }
|
|
17491
|
+
];
|
|
17492
|
+
}
|
|
17493
|
+
};
|
|
17494
|
+
var DbImportSchemaTooOldError = class extends Error {
|
|
17495
|
+
constructor(sourceVersion) {
|
|
17496
|
+
super(
|
|
17497
|
+
`source DB schema v${sourceVersion} is older than local mu requires (v${CURRENT_SCHEMA_VERSION})`
|
|
17498
|
+
);
|
|
17499
|
+
this.sourceVersion = sourceVersion;
|
|
17500
|
+
}
|
|
17501
|
+
sourceVersion;
|
|
17502
|
+
name = "DbImportSchemaTooOldError";
|
|
17503
|
+
errorNextSteps() {
|
|
17504
|
+
return [
|
|
17505
|
+
{
|
|
17506
|
+
intent: "Upgrade mu on the source machine",
|
|
17507
|
+
command: "npm run build && mu db export <file> --force"
|
|
17508
|
+
},
|
|
17509
|
+
{ intent: "Then retry this import", command: "mu db import <file> --apply" }
|
|
17510
|
+
];
|
|
17511
|
+
}
|
|
17512
|
+
};
|
|
17513
|
+
var DbImportSchemaTooNewError = class extends Error {
|
|
17514
|
+
constructor(sourceVersion) {
|
|
17515
|
+
super(
|
|
17516
|
+
`source DB schema v${sourceVersion} is newer than this mu supports (v${CURRENT_SCHEMA_VERSION}); upgrade local mu`
|
|
17517
|
+
);
|
|
17518
|
+
this.sourceVersion = sourceVersion;
|
|
17519
|
+
}
|
|
17520
|
+
sourceVersion;
|
|
17521
|
+
name = "DbImportSchemaTooNewError";
|
|
17522
|
+
errorNextSteps() {
|
|
17523
|
+
return [
|
|
17524
|
+
{ intent: "Upgrade local mu", command: "git pull && npm install && npm run build" },
|
|
17525
|
+
{ intent: "Then retry this import", command: "mu db import <file> --apply" }
|
|
17526
|
+
];
|
|
17527
|
+
}
|
|
17528
|
+
};
|
|
17529
|
+
var DbImportSourceStaleError = class extends Error {
|
|
17530
|
+
constructor(workstreams) {
|
|
17531
|
+
super(`source DB is stale for local-ahead workstream(s): ${workstreams.join(", ")}`);
|
|
17532
|
+
this.workstreams = workstreams;
|
|
17533
|
+
}
|
|
17534
|
+
workstreams;
|
|
17535
|
+
name = "DbImportSourceStaleError";
|
|
17536
|
+
errorNextSteps() {
|
|
17537
|
+
return [
|
|
17538
|
+
{ intent: "Re-export from this machine", command: "mu db export /tmp/mu-fresh.db --force" },
|
|
17539
|
+
{ intent: "Dry-run the incoming file first", command: "mu db import <file>" }
|
|
17540
|
+
];
|
|
17541
|
+
}
|
|
17542
|
+
};
|
|
17543
|
+
var DbImportConflictError = class extends Error {
|
|
17544
|
+
constructor(workstreams) {
|
|
17545
|
+
super(`source and local both advanced for workstream(s): ${workstreams.join(", ")}`);
|
|
17546
|
+
this.workstreams = workstreams;
|
|
17547
|
+
}
|
|
17548
|
+
workstreams;
|
|
17549
|
+
name = "DbImportConflictError";
|
|
17550
|
+
errorNextSteps() {
|
|
17551
|
+
return [
|
|
17552
|
+
{ intent: "Preview the conflicting workstreams", command: "mu db import <file> --json" },
|
|
17553
|
+
{
|
|
17554
|
+
intent: "Clobber from source after parking local divergence",
|
|
17555
|
+
command: "mu db import <file> --apply --force-source"
|
|
17556
|
+
}
|
|
17557
|
+
];
|
|
17558
|
+
}
|
|
17559
|
+
};
|
|
17560
|
+
function exportDb(db, file, opts = {}) {
|
|
17561
|
+
const target = file;
|
|
17562
|
+
const manifestPath = `${target}.manifest.json`;
|
|
17563
|
+
const targetExists = existsSync12(target);
|
|
17564
|
+
if (targetExists && opts.force !== true) throw new DbExportTargetExistsError(target);
|
|
17565
|
+
const preEventManifest = buildExportManifest(db);
|
|
17566
|
+
for (const ws of preEventManifest.workstreams) {
|
|
17567
|
+
emitEvent(db, ws.name, `db export ${ws.name} seq=${ws.latestSeq}`);
|
|
17568
|
+
}
|
|
17569
|
+
const manifest = buildExportManifest(db);
|
|
17570
|
+
mkdirSync4(dirname5(target), { recursive: true });
|
|
17571
|
+
try {
|
|
17572
|
+
if (targetExists) unlinkSync4(target);
|
|
17573
|
+
db.exec(`VACUUM INTO ${quoteSqlString2(target)}`);
|
|
17574
|
+
writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
17575
|
+
`, "utf8");
|
|
17576
|
+
} catch (err) {
|
|
17577
|
+
try {
|
|
17578
|
+
if (existsSync12(target)) unlinkSync4(target);
|
|
17579
|
+
} catch {
|
|
17580
|
+
}
|
|
17581
|
+
throw err;
|
|
17582
|
+
}
|
|
17583
|
+
return { file: target, manifestPath, manifest, overwritten: targetExists };
|
|
17584
|
+
}
|
|
17585
|
+
function importDb(db, file, opts = {}) {
|
|
17586
|
+
const manifest = readImportManifest(file);
|
|
17587
|
+
assertImportSchemaCompatible(manifest.schemaVersion);
|
|
17588
|
+
const sourceDb = openDb({ path: file, readonly: true });
|
|
17589
|
+
try {
|
|
17590
|
+
const summary = buildImportPlan(db, manifest, file, opts.onlyWorkstreams);
|
|
17591
|
+
if (opts.apply !== true) {
|
|
17592
|
+
return {
|
|
17593
|
+
machineId: manifest.machineId,
|
|
17594
|
+
sourceFile: file,
|
|
17595
|
+
dryRun: true,
|
|
17596
|
+
applied: false,
|
|
17597
|
+
summary
|
|
17598
|
+
};
|
|
17599
|
+
}
|
|
17600
|
+
const stale = summary.filter((s) => s.decision === "LOCAL_AHEAD").map((s) => s.workstream);
|
|
17601
|
+
if (stale.length > 0) throw new DbImportSourceStaleError(stale);
|
|
17602
|
+
const conflicts = summary.filter((s) => s.decision === "CONFLICT").map((s) => s.workstream);
|
|
17603
|
+
if (conflicts.length > 0 && opts.forceSource !== true)
|
|
17604
|
+
throw new DbImportConflictError(conflicts);
|
|
17605
|
+
const mutating = summary.some((s) => shouldReplace(s.decision, opts.forceSource === true));
|
|
17606
|
+
const snapshot = mutating ? captureSnapshot(db, `db import ${file}`, null) : void 0;
|
|
17607
|
+
for (const item of summary) {
|
|
17608
|
+
if (!shouldReplace(item.decision, opts.forceSource === true)) continue;
|
|
17609
|
+
if (item.decision === "CONFLICT") {
|
|
17610
|
+
item.parkPath = parkLocalWorkstream(db, item.workstream);
|
|
17611
|
+
}
|
|
17612
|
+
const sourceWs = manifest.workstreams.find((w) => w.name === item.workstream);
|
|
17613
|
+
const sourceSeq = sourceWs?.latestSeq ?? 0;
|
|
17614
|
+
replaceWorkstreamFromSource(db, sourceDb, item.workstream, manifest.machineId, sourceSeq);
|
|
17615
|
+
}
|
|
17616
|
+
return {
|
|
17617
|
+
machineId: manifest.machineId,
|
|
17618
|
+
sourceFile: file,
|
|
17619
|
+
dryRun: false,
|
|
17620
|
+
applied: true,
|
|
17621
|
+
...snapshot ? { snapshotId: snapshot.id } : {},
|
|
17622
|
+
summary
|
|
17623
|
+
};
|
|
17624
|
+
} finally {
|
|
17625
|
+
sourceDb.close();
|
|
17626
|
+
}
|
|
17627
|
+
}
|
|
17628
|
+
function buildImportPlan(localDb, manifest, sourceFile, onlyWorkstreams) {
|
|
17629
|
+
const sourceByName = new Map(manifest.workstreams.map((w) => [w.name, w]));
|
|
17630
|
+
const localByName = new Map(listLocalWorkstreams2(localDb).map((w) => [w.name, w]));
|
|
17631
|
+
const localMachineId = getMachineIdentity(localDb)?.machine_id ?? "";
|
|
17632
|
+
const only = normaliseOnlyWorkstreams(onlyWorkstreams);
|
|
17633
|
+
const names = Array.from(/* @__PURE__ */ new Set([...sourceByName.keys(), ...localByName.keys()])).filter((name) => only.size === 0 || only.has(name)).sort();
|
|
17634
|
+
return names.map((name) => {
|
|
17635
|
+
const source = sourceByName.get(name);
|
|
17636
|
+
const local = localByName.get(name);
|
|
17637
|
+
const sourceSeq = source?.latestSeq ?? 0;
|
|
17638
|
+
const localSeq = local ? latestSeq(localDb, local.id) : 0;
|
|
17639
|
+
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 };
|
|
17640
|
+
const decision = classifyWorkstream({
|
|
17641
|
+
hasSource: source !== void 0,
|
|
17642
|
+
hasLocal: local !== void 0,
|
|
17643
|
+
sourceSeq,
|
|
17644
|
+
localSeq,
|
|
17645
|
+
syncedSourceSeq: synced.sourceSeq,
|
|
17646
|
+
syncedLocalSeq: synced.localSeq
|
|
17647
|
+
});
|
|
17648
|
+
return {
|
|
17649
|
+
workstream: name,
|
|
17650
|
+
decision,
|
|
17651
|
+
delta: {
|
|
17652
|
+
sourceFile,
|
|
17653
|
+
sourceSeq,
|
|
17654
|
+
localSeq,
|
|
17655
|
+
lastSynced: synced.sourceSeq,
|
|
17656
|
+
localSynced: synced.localSeq,
|
|
17657
|
+
source: source ? countsFromManifest(source) : null,
|
|
17658
|
+
local: local ? countWorkstream(localDb, local.id) : null
|
|
17659
|
+
},
|
|
17660
|
+
...decision === "LOCAL_AHEAD" ? { needs: "re-export from this machine" } : {},
|
|
17661
|
+
...decision === "CONFLICT" ? { needs: "--force-source" } : {}
|
|
17662
|
+
};
|
|
17663
|
+
});
|
|
17664
|
+
}
|
|
17665
|
+
function classifyWorkstream(opts) {
|
|
17666
|
+
if (opts.hasSource && !opts.hasLocal) return "IMPORT";
|
|
17667
|
+
if (!opts.hasSource && opts.hasLocal)
|
|
17668
|
+
return opts.syncedSourceSeq > 0 || opts.syncedLocalSeq > 0 ? "LOCAL_AHEAD" : "LEAVE_ALONE";
|
|
17669
|
+
if (!opts.hasSource && !opts.hasLocal) return "IDENTICAL";
|
|
17670
|
+
const sourceAdvanced = opts.sourceSeq > opts.syncedSourceSeq;
|
|
17671
|
+
const localAdvanced = opts.localSeq > opts.syncedLocalSeq;
|
|
17672
|
+
if (!sourceAdvanced && !localAdvanced) return "IDENTICAL";
|
|
17673
|
+
if (sourceAdvanced && !localAdvanced) return "FAST_FORWARD";
|
|
17674
|
+
if (!sourceAdvanced && localAdvanced) return "LOCAL_AHEAD";
|
|
17675
|
+
return "CONFLICT";
|
|
17676
|
+
}
|
|
17677
|
+
function shouldReplace(decision, forceSource) {
|
|
17678
|
+
return decision === "FAST_FORWARD" || decision === "IMPORT" || decision === "CONFLICT" && forceSource;
|
|
17679
|
+
}
|
|
17680
|
+
function replaceWorkstreamFromSource(localDb, sourceDb, workstream, sourceMachineId, sourceSeq) {
|
|
17681
|
+
localDb.transaction(() => {
|
|
17682
|
+
const existing = localDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream);
|
|
17683
|
+
if (existing) {
|
|
17684
|
+
localDb.prepare("DELETE FROM vcs_workspaces WHERE workstream_id = ?").run(existing.id);
|
|
17685
|
+
localDb.prepare("DELETE FROM agents WHERE workstream_id = ?").run(existing.id);
|
|
17686
|
+
localDb.prepare("DELETE FROM workstreams WHERE id = ?").run(existing.id);
|
|
17687
|
+
}
|
|
17688
|
+
copyWorkstreamRows(sourceDb, localDb, workstream, {
|
|
17689
|
+
includeMachineLocalRows: false,
|
|
17690
|
+
preserveLogSeq: false,
|
|
17691
|
+
includeSync: false
|
|
17692
|
+
});
|
|
17693
|
+
const wsId = localDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream)?.id;
|
|
17694
|
+
if (wsId === void 0) throw new Error(`importDb: failed to import workstream ${workstream}`);
|
|
17695
|
+
writeSyncState(localDb, wsId, sourceMachineId, sourceSeq);
|
|
17696
|
+
})();
|
|
17697
|
+
}
|
|
17698
|
+
function parkLocalWorkstream(db, workstream) {
|
|
17699
|
+
const dir = join8(defaultStateDir(), "divergence");
|
|
17700
|
+
mkdirSync4(dir, { recursive: true });
|
|
17701
|
+
const path2 = join8(
|
|
17702
|
+
dir,
|
|
17703
|
+
`${workstream}-${(/* @__PURE__ */ new Date()).toISOString()}-${randomUUID2().slice(0, 8)}.db`
|
|
17704
|
+
);
|
|
17705
|
+
const parkDb = openDb({ path: path2 });
|
|
17706
|
+
try {
|
|
17707
|
+
const identity = getMachineIdentity(db);
|
|
17708
|
+
if (identity) {
|
|
17709
|
+
parkDb.prepare(
|
|
17710
|
+
`UPDATE machine_identity
|
|
17711
|
+
SET machine_id = ?, hostname = ?, created_at = ?
|
|
17712
|
+
WHERE id = 1`
|
|
17713
|
+
).run(
|
|
17714
|
+
identity.machine_id,
|
|
17715
|
+
identity.hostname,
|
|
17716
|
+
identity.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
17717
|
+
);
|
|
17718
|
+
}
|
|
17719
|
+
copyWorkstreamRows(db, parkDb, workstream, {
|
|
17720
|
+
includeMachineLocalRows: true,
|
|
17721
|
+
preserveLogSeq: true,
|
|
17722
|
+
includeSync: true
|
|
17723
|
+
});
|
|
17724
|
+
} catch (err) {
|
|
17725
|
+
try {
|
|
17726
|
+
parkDb.close();
|
|
17727
|
+
} catch {
|
|
17728
|
+
}
|
|
17729
|
+
try {
|
|
17730
|
+
if (existsSync12(path2)) unlinkSync4(path2);
|
|
17731
|
+
} catch {
|
|
17732
|
+
}
|
|
17733
|
+
throw err;
|
|
17734
|
+
}
|
|
17735
|
+
parkDb.close();
|
|
17736
|
+
return path2;
|
|
17737
|
+
}
|
|
17738
|
+
function copyWorkstreamRows(sourceDb, targetDb, workstream, opts) {
|
|
17739
|
+
const sourceWs = sourceDb.prepare("SELECT id, name, created_at FROM workstreams WHERE name = ?").get(workstream);
|
|
17740
|
+
if (!sourceWs) throw new Error(`copyWorkstreamRows: no such workstream ${workstream}`);
|
|
17741
|
+
targetDb.prepare("INSERT INTO workstreams (name, created_at) VALUES (?, ?)").run(sourceWs.name, sourceWs.created_at);
|
|
17742
|
+
const targetWsId = targetDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream).id;
|
|
17743
|
+
if (opts.includeMachineLocalRows) copyAgents(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
17744
|
+
copyTasks(sourceDb, targetDb, sourceWs.id, targetWsId, opts.includeMachineLocalRows);
|
|
17745
|
+
copyEdges(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
17746
|
+
copyNotes(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
17747
|
+
copyLogs(sourceDb, targetDb, sourceWs.id, targetWsId, opts.preserveLogSeq);
|
|
17748
|
+
if (opts.includeMachineLocalRows) copyWorkspaces(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
17749
|
+
if (opts.includeSync) copySync(sourceDb, targetDb, sourceWs.id, targetWsId);
|
|
17750
|
+
}
|
|
17751
|
+
function copyAgents(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
17752
|
+
const rows = sourceDb.prepare(
|
|
17753
|
+
`SELECT name, cli, pane_id, status, role, tab, created_at, updated_at
|
|
17754
|
+
FROM agents
|
|
17755
|
+
WHERE workstream_id = ?
|
|
17756
|
+
ORDER BY id`
|
|
17757
|
+
).all(sourceWsId);
|
|
17758
|
+
const insert = targetDb.prepare(
|
|
17759
|
+
`INSERT INTO agents (workstream_id, name, cli, pane_id, status, role, tab, created_at, updated_at)
|
|
17760
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
17761
|
+
);
|
|
17762
|
+
for (const row2 of rows) {
|
|
17763
|
+
insert.run(
|
|
17764
|
+
targetWsId,
|
|
17765
|
+
row2.name,
|
|
17766
|
+
row2.cli,
|
|
17767
|
+
row2.pane_id,
|
|
17768
|
+
row2.status,
|
|
17769
|
+
row2.role,
|
|
17770
|
+
row2.tab,
|
|
17771
|
+
row2.created_at,
|
|
17772
|
+
row2.updated_at
|
|
17773
|
+
);
|
|
17774
|
+
}
|
|
17775
|
+
}
|
|
17776
|
+
function copyTasks(sourceDb, targetDb, sourceWsId, targetWsId, includeOwners) {
|
|
17777
|
+
const rows = sourceDb.prepare(
|
|
17778
|
+
`SELECT t.local_id, t.title, t.status, t.impact, t.effort_days, a.name AS owner_name,
|
|
17779
|
+
t.created_at, t.updated_at
|
|
17780
|
+
FROM tasks t
|
|
17781
|
+
LEFT JOIN agents a ON a.id = t.owner_id
|
|
17782
|
+
WHERE t.workstream_id = ?
|
|
17783
|
+
ORDER BY t.id`
|
|
17784
|
+
).all(sourceWsId);
|
|
17785
|
+
const ownerLookup = targetDb.prepare(
|
|
17786
|
+
"SELECT id FROM agents WHERE workstream_id = ? AND name = ?"
|
|
17787
|
+
);
|
|
17788
|
+
const insert = targetDb.prepare(
|
|
17789
|
+
`INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
|
|
17790
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
17791
|
+
);
|
|
17792
|
+
for (const row2 of rows) {
|
|
17793
|
+
const ownerId = includeOwners && row2.owner_name !== null ? ownerLookup.get(targetWsId, row2.owner_name)?.id ?? null : null;
|
|
17794
|
+
insert.run(
|
|
17795
|
+
targetWsId,
|
|
17796
|
+
row2.local_id,
|
|
17797
|
+
row2.title,
|
|
17798
|
+
row2.status,
|
|
17799
|
+
row2.impact,
|
|
17800
|
+
row2.effort_days,
|
|
17801
|
+
ownerId,
|
|
17802
|
+
row2.created_at,
|
|
17803
|
+
row2.updated_at
|
|
17804
|
+
);
|
|
17805
|
+
}
|
|
17806
|
+
}
|
|
17807
|
+
function copyEdges(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
17808
|
+
const rows = sourceDb.prepare(
|
|
17809
|
+
`SELECT f.local_id AS from_local_id, t.local_id AS to_local_id, e.created_at
|
|
17810
|
+
FROM task_edges e
|
|
17811
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
17812
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
17813
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?
|
|
17814
|
+
ORDER BY e.created_at, f.local_id, t.local_id`
|
|
17815
|
+
).all(sourceWsId, sourceWsId);
|
|
17816
|
+
const insert = targetDb.prepare(
|
|
17817
|
+
`INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
|
|
17818
|
+
SELECT f.id, t.id, ?
|
|
17819
|
+
FROM tasks f, tasks t
|
|
17820
|
+
WHERE f.workstream_id = ? AND f.local_id = ?
|
|
17821
|
+
AND t.workstream_id = ? AND t.local_id = ?`
|
|
17822
|
+
);
|
|
17823
|
+
for (const row2 of rows) {
|
|
17824
|
+
insert.run(row2.created_at, targetWsId, row2.from_local_id, targetWsId, row2.to_local_id);
|
|
17825
|
+
}
|
|
17826
|
+
}
|
|
17827
|
+
function copyNotes(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
17828
|
+
const rows = sourceDb.prepare(
|
|
17829
|
+
`SELECT t.local_id AS task_local_id, n.author, n.content, n.created_at
|
|
17830
|
+
FROM task_notes n
|
|
17831
|
+
JOIN tasks t ON t.id = n.task_id
|
|
17832
|
+
WHERE t.workstream_id = ?
|
|
17833
|
+
ORDER BY n.id`
|
|
17834
|
+
).all(sourceWsId);
|
|
17835
|
+
const insert = targetDb.prepare(
|
|
17836
|
+
`INSERT INTO task_notes (task_id, author, content, created_at)
|
|
17837
|
+
SELECT id, ?, ?, ? FROM tasks WHERE workstream_id = ? AND local_id = ?`
|
|
17838
|
+
);
|
|
17839
|
+
for (const row2 of rows) {
|
|
17840
|
+
insert.run(row2.author, row2.content, row2.created_at, targetWsId, row2.task_local_id);
|
|
17841
|
+
}
|
|
17842
|
+
}
|
|
17843
|
+
function copyLogs(sourceDb, targetDb, sourceWsId, targetWsId, preserveSeq) {
|
|
17844
|
+
const rows = sourceDb.prepare(
|
|
17845
|
+
`SELECT seq, source, kind, payload, created_at
|
|
17846
|
+
FROM agent_logs
|
|
17847
|
+
WHERE workstream_id = ?
|
|
17848
|
+
ORDER BY seq`
|
|
17849
|
+
).all(sourceWsId);
|
|
17850
|
+
const insertPreserve = targetDb.prepare(
|
|
17851
|
+
"INSERT INTO agent_logs (seq, workstream_id, source, kind, payload, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
17852
|
+
);
|
|
17853
|
+
const insertRenumber = targetDb.prepare(
|
|
17854
|
+
"INSERT INTO agent_logs (workstream_id, source, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
17855
|
+
);
|
|
17856
|
+
for (const row2 of rows) {
|
|
17857
|
+
if (preserveSeq) {
|
|
17858
|
+
insertPreserve.run(row2.seq, targetWsId, row2.source, row2.kind, row2.payload, row2.created_at);
|
|
17859
|
+
} else {
|
|
17860
|
+
insertRenumber.run(targetWsId, row2.source, row2.kind, row2.payload, row2.created_at);
|
|
17861
|
+
}
|
|
17862
|
+
}
|
|
17863
|
+
}
|
|
17864
|
+
function copyWorkspaces(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
17865
|
+
const rows = sourceDb.prepare(
|
|
17866
|
+
`SELECT a.name AS agent_name, v.backend, v.path, v.parent_ref, v.created_at
|
|
17867
|
+
FROM vcs_workspaces v
|
|
17868
|
+
JOIN agents a ON a.id = v.agent_id
|
|
17869
|
+
WHERE v.workstream_id = ?
|
|
17870
|
+
ORDER BY v.id`
|
|
17871
|
+
).all(sourceWsId);
|
|
17872
|
+
const agentLookup = targetDb.prepare(
|
|
17873
|
+
"SELECT id FROM agents WHERE workstream_id = ? AND name = ?"
|
|
17874
|
+
);
|
|
17875
|
+
const insert = targetDb.prepare(
|
|
17876
|
+
`INSERT INTO vcs_workspaces (agent_id, workstream_id, backend, path, parent_ref, created_at)
|
|
17877
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
17878
|
+
);
|
|
17879
|
+
for (const row2 of rows) {
|
|
17880
|
+
const agentId = agentLookup.get(targetWsId, row2.agent_name)?.id;
|
|
17881
|
+
if (agentId === void 0) continue;
|
|
17882
|
+
insert.run(agentId, targetWsId, row2.backend, row2.path, row2.parent_ref, row2.created_at);
|
|
17883
|
+
}
|
|
17884
|
+
}
|
|
17885
|
+
function copySync(sourceDb, targetDb, sourceWsId, targetWsId) {
|
|
17886
|
+
const row2 = sourceDb.prepare("SELECT last_known_peer_seqs FROM workstream_sync WHERE workstream_id = ?").get(sourceWsId);
|
|
17887
|
+
if (!row2) return;
|
|
17888
|
+
targetDb.prepare("INSERT INTO workstream_sync (workstream_id, last_known_peer_seqs) VALUES (?, ?)").run(targetWsId, row2.last_known_peer_seqs);
|
|
17889
|
+
}
|
|
17890
|
+
function writeSyncState(db, workstreamId, sourceMachineId, sourceSeq) {
|
|
17891
|
+
const localSeq = latestSeq(db, workstreamId);
|
|
17892
|
+
const peers = {
|
|
17893
|
+
[sourceMachineId]: sourceSeq,
|
|
17894
|
+
[localSeqKey(sourceMachineId)]: localSeq
|
|
17895
|
+
};
|
|
17896
|
+
db.prepare(
|
|
17897
|
+
`INSERT OR REPLACE INTO workstream_sync (workstream_id, last_known_peer_seqs)
|
|
17898
|
+
VALUES (?, ?)`
|
|
17899
|
+
).run(workstreamId, JSON.stringify(peers));
|
|
17900
|
+
}
|
|
17901
|
+
function lastKnownPeerSync(db, workstreamId, machineId) {
|
|
17902
|
+
const row2 = db.prepare("SELECT last_known_peer_seqs FROM workstream_sync WHERE workstream_id = ?").get(workstreamId);
|
|
17903
|
+
if (!row2) return { sourceSeq: 0, localSeq: 0 };
|
|
17904
|
+
const parsed = parsePeerSeqs(row2.last_known_peer_seqs);
|
|
17905
|
+
const sourceSeq = parsed[machineId] ?? 0;
|
|
17906
|
+
return { sourceSeq, localSeq: parsed[localSeqKey(machineId)] ?? sourceSeq };
|
|
17907
|
+
}
|
|
17908
|
+
function localSeqKey(machineId) {
|
|
17909
|
+
return `${machineId}:local`;
|
|
17910
|
+
}
|
|
17911
|
+
function parsePeerSeqs(raw) {
|
|
17912
|
+
try {
|
|
17913
|
+
const parsed = JSON.parse(raw);
|
|
17914
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
17915
|
+
const result = {};
|
|
17916
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
17917
|
+
if (typeof value === "number" && Number.isFinite(value)) result[key] = value;
|
|
17918
|
+
}
|
|
17919
|
+
return result;
|
|
17920
|
+
} catch {
|
|
17921
|
+
return {};
|
|
17922
|
+
}
|
|
17923
|
+
}
|
|
17924
|
+
function readImportManifest(file) {
|
|
17925
|
+
const manifestPath = `${file}.manifest.json`;
|
|
17926
|
+
if (!existsSync12(manifestPath)) throw new DbImportManifestMissingError(manifestPath);
|
|
17927
|
+
return JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
17928
|
+
}
|
|
17929
|
+
function assertImportSchemaCompatible(sourceVersion) {
|
|
17930
|
+
if (sourceVersion < CURRENT_SCHEMA_VERSION) throw new DbImportSchemaTooOldError(sourceVersion);
|
|
17931
|
+
if (sourceVersion > CURRENT_SCHEMA_VERSION) throw new DbImportSchemaTooNewError(sourceVersion);
|
|
17932
|
+
}
|
|
17933
|
+
function buildExportManifest(db) {
|
|
17934
|
+
const identity = getMachineIdentity(db);
|
|
17935
|
+
const schemaRow = db.prepare("SELECT version FROM schema_version WHERE id = 1").get();
|
|
17936
|
+
const workstreams = listLocalWorkstreams2(db);
|
|
17937
|
+
return {
|
|
17938
|
+
muVersion: readPackageVersion(),
|
|
17939
|
+
schemaVersion: schemaRow?.version ?? CURRENT_SCHEMA_VERSION,
|
|
17940
|
+
machineId: identity?.machine_id ?? "",
|
|
17941
|
+
hostname: identity?.hostname ?? hostname2(),
|
|
17942
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17943
|
+
workstreams: workstreams.map((ws) => ({
|
|
17944
|
+
name: ws.name,
|
|
17945
|
+
tasks: count(db, "SELECT COUNT(*) AS n FROM tasks WHERE workstream_id = ?", ws.id),
|
|
17946
|
+
edges: count(
|
|
17947
|
+
db,
|
|
17948
|
+
`SELECT COUNT(*) AS n
|
|
17949
|
+
FROM task_edges e
|
|
17950
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
17951
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
17952
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?`,
|
|
17953
|
+
ws.id,
|
|
17954
|
+
ws.id
|
|
17955
|
+
),
|
|
17956
|
+
notes: count(
|
|
17957
|
+
db,
|
|
17958
|
+
`SELECT COUNT(*) AS n
|
|
17959
|
+
FROM task_notes n
|
|
17960
|
+
JOIN tasks t ON t.id = n.task_id
|
|
17961
|
+
WHERE t.workstream_id = ?`,
|
|
17962
|
+
ws.id
|
|
17963
|
+
),
|
|
17964
|
+
latestSeq: latestSeq(db, ws.id)
|
|
17965
|
+
}))
|
|
17966
|
+
};
|
|
17967
|
+
}
|
|
17968
|
+
function listLocalWorkstreams2(db) {
|
|
17969
|
+
return db.prepare("SELECT id, name FROM workstreams ORDER BY name").all();
|
|
17970
|
+
}
|
|
17971
|
+
function getMachineIdentity(db) {
|
|
17972
|
+
return db.prepare("SELECT machine_id, hostname, created_at FROM machine_identity WHERE id = 1").get();
|
|
17973
|
+
}
|
|
17974
|
+
function countWorkstream(db, wsId) {
|
|
17975
|
+
return {
|
|
17976
|
+
tasks: count(db, "SELECT COUNT(*) AS n FROM tasks WHERE workstream_id = ?", wsId),
|
|
17977
|
+
edges: count(
|
|
17978
|
+
db,
|
|
17979
|
+
`SELECT COUNT(*) AS n
|
|
17980
|
+
FROM task_edges e
|
|
17981
|
+
JOIN tasks f ON f.id = e.from_task_id
|
|
17982
|
+
JOIN tasks t ON t.id = e.to_task_id
|
|
17983
|
+
WHERE f.workstream_id = ? AND t.workstream_id = ?`,
|
|
17984
|
+
wsId,
|
|
17985
|
+
wsId
|
|
17986
|
+
),
|
|
17987
|
+
notes: count(
|
|
17988
|
+
db,
|
|
17989
|
+
`SELECT COUNT(*) AS n
|
|
17990
|
+
FROM task_notes n
|
|
17991
|
+
JOIN tasks t ON t.id = n.task_id
|
|
17992
|
+
WHERE t.workstream_id = ?`,
|
|
17993
|
+
wsId
|
|
17994
|
+
)
|
|
17995
|
+
};
|
|
17996
|
+
}
|
|
17997
|
+
function countsFromManifest(ws) {
|
|
17998
|
+
return { tasks: ws.tasks, edges: ws.edges, notes: ws.notes };
|
|
17999
|
+
}
|
|
18000
|
+
function normaliseOnlyWorkstreams(input) {
|
|
18001
|
+
if (!input || input.length === 0) return /* @__PURE__ */ new Set();
|
|
18002
|
+
return new Set(
|
|
18003
|
+
input.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0)
|
|
18004
|
+
);
|
|
18005
|
+
}
|
|
18006
|
+
function count(db, sql, ...params) {
|
|
18007
|
+
const row2 = db.prepare(sql).get(...params);
|
|
18008
|
+
return row2?.n ?? 0;
|
|
18009
|
+
}
|
|
18010
|
+
function quoteSqlString2(s) {
|
|
18011
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
18012
|
+
}
|
|
18013
|
+
function readPackageVersion() {
|
|
18014
|
+
try {
|
|
18015
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
18016
|
+
const raw = readFileSync2(join8(here, "..", "package.json"), "utf8");
|
|
18017
|
+
const parsed = JSON.parse(raw);
|
|
18018
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
18019
|
+
} catch {
|
|
18020
|
+
return "unknown";
|
|
18021
|
+
}
|
|
18022
|
+
}
|
|
18023
|
+
function shellQuote3(s) {
|
|
18024
|
+
return `'${s.replace(/'/g, `'"'"'`)}'`;
|
|
18025
|
+
}
|
|
18026
|
+
|
|
18027
|
+
// src/cli/db.ts
|
|
18028
|
+
init_output();
|
|
18029
|
+
function exportNextSteps(file) {
|
|
18030
|
+
return [
|
|
18031
|
+
{ intent: "Ship the DB copy", command: `scp ${file} <other-machine>:/tmp/mu.db` },
|
|
18032
|
+
{ intent: "Ship the manifest too", command: `scp ${file}.manifest.json <other-machine>:/tmp/` },
|
|
18033
|
+
{ intent: "Import on the other side", command: "mu db import /tmp/mu.db" }
|
|
18034
|
+
];
|
|
18035
|
+
}
|
|
18036
|
+
function importNextSteps(result) {
|
|
18037
|
+
if (result.dryRun) {
|
|
18038
|
+
const hasConflict = result.summary.some((s) => s.decision === "CONFLICT");
|
|
18039
|
+
const hasStale = result.summary.some((s) => s.decision === "LOCAL_AHEAD");
|
|
18040
|
+
return [
|
|
18041
|
+
...hasStale ? [{ intent: "Source is stale", command: "re-export from this machine before importing" }] : [],
|
|
18042
|
+
...hasConflict ? [
|
|
18043
|
+
{
|
|
18044
|
+
intent: "Clobber source after parking local",
|
|
18045
|
+
command: `mu db import ${result.sourceFile} --apply --force-source`
|
|
18046
|
+
}
|
|
18047
|
+
] : [],
|
|
18048
|
+
...!hasConflict && !hasStale ? [{ intent: "Apply this plan", command: `mu db import ${result.sourceFile} --apply` }] : []
|
|
18049
|
+
];
|
|
18050
|
+
}
|
|
18051
|
+
return [
|
|
18052
|
+
{ intent: "Undo if needed", command: "mu undo --yes" },
|
|
18053
|
+
{ intent: "Inspect workstreams", command: "mu state --json" }
|
|
18054
|
+
];
|
|
18055
|
+
}
|
|
18056
|
+
function replayNextSteps(result) {
|
|
18057
|
+
if (result.dryRun) {
|
|
18058
|
+
const firstTask = result.tasks[0]?.localId;
|
|
18059
|
+
return [
|
|
18060
|
+
...firstTask ? [
|
|
18061
|
+
{
|
|
18062
|
+
intent: "Replay one parked task",
|
|
18063
|
+
command: `mu db replay ${result.sourceFile} --task ${firstTask} --apply`
|
|
18064
|
+
}
|
|
18065
|
+
] : [],
|
|
18066
|
+
{
|
|
18067
|
+
intent: "Replay all parked rows",
|
|
18068
|
+
command: `mu db replay ${result.sourceFile} --all --apply`
|
|
18069
|
+
}
|
|
18070
|
+
];
|
|
18071
|
+
}
|
|
18072
|
+
return [
|
|
18073
|
+
{ intent: "Undo if needed", command: "mu undo --yes" },
|
|
18074
|
+
{ intent: "Inspect the workstream", command: `mu task list -w ${result.workstream}` }
|
|
18075
|
+
];
|
|
18076
|
+
}
|
|
18077
|
+
async function cmdDbExport(db, file, opts = {}) {
|
|
18078
|
+
const result = exportDb(db, file, opts);
|
|
18079
|
+
const nextSteps = exportNextSteps(result.file);
|
|
18080
|
+
if (opts.json) {
|
|
18081
|
+
emitJson({ ...result, nextSteps });
|
|
18082
|
+
return;
|
|
18083
|
+
}
|
|
18084
|
+
console.log(
|
|
18085
|
+
`Exported whole mu DB \u2192 ${pc.bold(result.file)} ${pc.dim(
|
|
18086
|
+
`(schema=v${result.manifest.schemaVersion}, workstreams=${result.manifest.workstreams.length}, manifest=${result.manifestPath})`
|
|
18087
|
+
)}`
|
|
18088
|
+
);
|
|
18089
|
+
if (result.overwritten) console.log(pc.dim("Overwrote existing target due to --force."));
|
|
18090
|
+
printNextSteps(nextSteps);
|
|
18091
|
+
}
|
|
18092
|
+
async function cmdDbImport(db, file, opts = {}) {
|
|
18093
|
+
const result = importDb(db, file, {
|
|
18094
|
+
apply: opts.apply,
|
|
18095
|
+
forceSource: opts.forceSource,
|
|
18096
|
+
onlyWorkstreams: opts.onlyWs
|
|
18097
|
+
});
|
|
18098
|
+
const nextSteps = importNextSteps(result);
|
|
18099
|
+
if (opts.json) {
|
|
18100
|
+
emitJson({ ...result, nextSteps });
|
|
18101
|
+
return;
|
|
18102
|
+
}
|
|
18103
|
+
console.log(
|
|
18104
|
+
`${result.dryRun ? "Dry-run" : "Applied"} DB import from ${pc.bold(result.sourceFile)} ${pc.dim(
|
|
18105
|
+
`(source machine=${result.machineId})`
|
|
18106
|
+
)}`
|
|
18107
|
+
);
|
|
18108
|
+
if (result.snapshotId !== void 0) {
|
|
18109
|
+
console.log(pc.dim(`Safety snapshot #${result.snapshotId} captured before import.`));
|
|
18110
|
+
}
|
|
18111
|
+
console.log(renderImportSummary(result.summary));
|
|
18112
|
+
printNextSteps(nextSteps);
|
|
18113
|
+
}
|
|
18114
|
+
function renderImportSummary(summary) {
|
|
18115
|
+
const table = muTable({
|
|
18116
|
+
head: ["workstream", "decision", "source_seq", "local_seq", "last_synced", "needs"]
|
|
18117
|
+
});
|
|
18118
|
+
for (const item of summary) {
|
|
18119
|
+
const delta = item.delta;
|
|
18120
|
+
table.push([
|
|
18121
|
+
item.workstream,
|
|
18122
|
+
item.decision,
|
|
18123
|
+
String(delta.sourceSeq ?? ""),
|
|
18124
|
+
String(delta.localSeq ?? ""),
|
|
18125
|
+
String(delta.lastSynced ?? ""),
|
|
18126
|
+
item.parkPath ?? item.needs ?? ""
|
|
18127
|
+
]);
|
|
18128
|
+
}
|
|
18129
|
+
return table.toString();
|
|
18130
|
+
}
|
|
18131
|
+
async function cmdDbReplay(db, file, opts = {}) {
|
|
18132
|
+
const result = replayDb(db, file, {
|
|
18133
|
+
apply: opts.apply,
|
|
18134
|
+
tasks: opts.task,
|
|
18135
|
+
notes: opts.note,
|
|
18136
|
+
all: opts.all
|
|
18137
|
+
});
|
|
18138
|
+
const nextSteps = replayNextSteps(result);
|
|
18139
|
+
if (opts.json) {
|
|
18140
|
+
emitJson({ ...result, nextSteps });
|
|
18141
|
+
return;
|
|
18142
|
+
}
|
|
18143
|
+
console.log(
|
|
18144
|
+
`${result.dryRun ? "Dry-run" : "Applied"} DB replay from ${pc.bold(result.sourceFile)} ${pc.dim(
|
|
18145
|
+
`(workstream=${result.workstream})`
|
|
18146
|
+
)}`
|
|
18147
|
+
);
|
|
18148
|
+
if (result.snapshotId !== void 0) {
|
|
18149
|
+
console.log(pc.dim(`Safety snapshot #${result.snapshotId} captured before replay.`));
|
|
18150
|
+
}
|
|
18151
|
+
console.log(renderReplaySummary(result));
|
|
18152
|
+
for (const warning of result.warnings) console.warn(pc.yellow(`warning: ${warning}`));
|
|
18153
|
+
printNextSteps(nextSteps);
|
|
18154
|
+
}
|
|
18155
|
+
function renderReplaySummary(result) {
|
|
18156
|
+
const table = muTable({ head: ["kind", "count", "details"] });
|
|
18157
|
+
table.push([
|
|
18158
|
+
"tasks",
|
|
18159
|
+
String(result.tasks.length),
|
|
18160
|
+
result.tasks.map((t) => `${t.localId} (${t.status})`).join(", ")
|
|
18161
|
+
]);
|
|
18162
|
+
table.push([
|
|
18163
|
+
"notes",
|
|
18164
|
+
String(result.notes.length),
|
|
18165
|
+
result.notes.map((n) => `${n.taskLocalId}@${n.createdAt}`).join(", ")
|
|
18166
|
+
]);
|
|
18167
|
+
table.push([
|
|
18168
|
+
"edges",
|
|
18169
|
+
String(result.edges.length),
|
|
18170
|
+
result.edges.map((e) => `${e.fromLocalId}->${e.toLocalId}`).join(", ")
|
|
18171
|
+
]);
|
|
18172
|
+
table.push([
|
|
18173
|
+
"conflicts",
|
|
18174
|
+
String(result.conflicts.length),
|
|
18175
|
+
result.conflicts.map(
|
|
18176
|
+
(c) => `${c.localId}: local=${c.local.status}/${c.local.title}; sidecar=${c.sidecar.status}/${c.sidecar.title}`
|
|
18177
|
+
).join(", ")
|
|
18178
|
+
]);
|
|
18179
|
+
if (!result.dryRun) {
|
|
18180
|
+
table.push([
|
|
18181
|
+
"added",
|
|
18182
|
+
String(result.added.tasks + result.added.notes + result.added.edges),
|
|
18183
|
+
`tasks=${result.added.tasks}, notes=${result.added.notes}, edges=${result.added.edges}`
|
|
18184
|
+
]);
|
|
18185
|
+
}
|
|
18186
|
+
return table.toString();
|
|
18187
|
+
}
|
|
18188
|
+
function collectOnlyWs(value, previous = []) {
|
|
18189
|
+
return collectRepeatedCsv(value, previous);
|
|
18190
|
+
}
|
|
18191
|
+
function collectRepeatedCsv(value, previous = []) {
|
|
18192
|
+
return [
|
|
18193
|
+
...previous,
|
|
18194
|
+
...value.split(",").map((v) => v.trim()).filter((v) => v.length > 0)
|
|
18195
|
+
];
|
|
18196
|
+
}
|
|
18197
|
+
function wireDbCommands(program) {
|
|
18198
|
+
const db = program.command("db").description("Whole-machine DB sync commands");
|
|
18199
|
+
db.command("export <file>").description(
|
|
18200
|
+
"Export the entire mu SQLite DB to <file> via VACUUM INTO and write <file>.manifest.json. Whole-machine by design; no --workstream flag."
|
|
18201
|
+
).option("--force", "overwrite an existing target file").option(...JSON_OPT).action(function(file) {
|
|
18202
|
+
const opts = this.opts();
|
|
18203
|
+
return handle((dbHandle) => cmdDbExport(dbHandle, file, opts), this)();
|
|
18204
|
+
});
|
|
18205
|
+
db.command("import <file>").description(
|
|
18206
|
+
"Import an exported mu DB with per-workstream drift detection. Dry-run by default; pass --apply to commit. Use --force-source to clobber conflicts after parking local divergence."
|
|
18207
|
+
).option("--apply", "actually apply the import plan (default is dry-run)").option(
|
|
18208
|
+
"--only-ws <names>",
|
|
18209
|
+
"restrict to workstream names; repeat or comma-separate",
|
|
18210
|
+
collectOnlyWs,
|
|
18211
|
+
[]
|
|
18212
|
+
).option("--force-source", "on conflict, park local divergence then replace from source").option(...JSON_OPT).action(function(file) {
|
|
18213
|
+
const opts = this.opts();
|
|
18214
|
+
return handle((dbHandle) => cmdDbImport(dbHandle, file, opts), this)();
|
|
18215
|
+
});
|
|
18216
|
+
db.command("replay <sidecar-file>").description(
|
|
18217
|
+
"Manually cherry-pick tasks, notes, and eligible edges from a divergence sidecar parked by mu db import --force-source. Dry-run by default; pass --apply to write."
|
|
18218
|
+
).option("--apply", "actually apply the replay selection (default is dry-run)").option(
|
|
18219
|
+
"--task <id>",
|
|
18220
|
+
"replay a missing task plus its notes and eligible edges; repeat or comma-separate",
|
|
18221
|
+
collectRepeatedCsv,
|
|
18222
|
+
[]
|
|
18223
|
+
).option(
|
|
18224
|
+
"--note <task-id>",
|
|
18225
|
+
"replay missing notes for a task; repeat or comma-separate",
|
|
18226
|
+
collectRepeatedCsv,
|
|
18227
|
+
[]
|
|
18228
|
+
).option("--all", "replay every missing local-only item from the sidecar").option(...JSON_OPT).action(function(file) {
|
|
18229
|
+
const opts = this.opts();
|
|
18230
|
+
return handle((dbHandle) => cmdDbReplay(dbHandle, file, opts), this)();
|
|
18231
|
+
});
|
|
18232
|
+
}
|
|
18233
|
+
|
|
18234
|
+
// src/cli/doctor.ts
|
|
18235
|
+
init_agents();
|
|
18236
|
+
init_db();
|
|
18237
|
+
init_output();
|
|
18238
|
+
init_tmux();
|
|
18239
|
+
init_workstream();
|
|
18240
|
+
async function cmdDoctor(db, opts = {}) {
|
|
18241
|
+
if (opts.json) {
|
|
18242
|
+
return cmdDoctorJson(db);
|
|
18243
|
+
}
|
|
18244
|
+
console.log(pc.bold("mu doctor"));
|
|
18245
|
+
console.log(pc.bold("\nenvironment"));
|
|
18246
|
+
try {
|
|
18247
|
+
const version = (await tmux(["-V"])).trim();
|
|
18248
|
+
console.log(` tmux : ${pc.green("ok")} (${version})`);
|
|
18249
|
+
} catch {
|
|
18250
|
+
console.log(` tmux : ${pc.red("NOT FOUND")} \u2014 install tmux \u2265 3.0`);
|
|
18251
|
+
}
|
|
18252
|
+
console.log(` $TMUX : ${process.env.TMUX ? pc.green("set") : pc.yellow("not set")}`);
|
|
18253
|
+
console.log(
|
|
18254
|
+
` $TMUX_PANE : ${process.env.TMUX_PANE ? pc.green(process.env.TMUX_PANE) : pc.dim("not set")}`
|
|
18255
|
+
);
|
|
16717
18256
|
console.log(
|
|
16718
18257
|
` $MU_SESSION : ${process.env.MU_SESSION ? pc.green(process.env.MU_SESSION) : pc.dim("not set")}`
|
|
16719
18258
|
);
|
|
@@ -16793,7 +18332,10 @@ state (workstream=${ws})`));
|
|
|
16793
18332
|
console.log(` agent_logs rows : ${counts.logs}`);
|
|
16794
18333
|
try {
|
|
16795
18334
|
const view = await listLiveAgents(db, { workstream: ws, mode: "report-only" });
|
|
16796
|
-
const
|
|
18335
|
+
const ghosts = view.report.prunedGhosts;
|
|
18336
|
+
const ghostNote = ghosts > 0 ? pc.yellow(
|
|
18337
|
+
`${ghosts} ghost pane${ghosts === 1 ? "" : "s"} would be reaped by \`mu state\` or \`mu agent list\``
|
|
18338
|
+
) : pc.green("none");
|
|
16797
18339
|
console.log(` ghosts : ${ghostNote}`);
|
|
16798
18340
|
const orphanColor = view.orphans.length > 0 ? pc.yellow : pc.green;
|
|
16799
18341
|
console.log(
|
|
@@ -16933,553 +18475,8 @@ function wireDoctorCommand(program) {
|
|
|
16933
18475
|
// src/cli/handle.ts
|
|
16934
18476
|
init_agents();
|
|
16935
18477
|
init_archives();
|
|
16936
|
-
init_db();
|
|
16937
18478
|
import { CommanderError } from "commander";
|
|
16938
|
-
|
|
16939
|
-
// src/importing.ts
|
|
16940
18479
|
init_db();
|
|
16941
|
-
init_exporting();
|
|
16942
|
-
init_logs();
|
|
16943
|
-
init_status();
|
|
16944
|
-
init_workstream();
|
|
16945
|
-
import { existsSync as existsSync12, readFileSync as readFileSync2, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
16946
|
-
import { basename as basename2, dirname as dirname5, join as join8 } from "path";
|
|
16947
|
-
var ImportBucketInvalidError = class extends Error {
|
|
16948
|
-
constructor(bucketDir, reason) {
|
|
16949
|
-
super(`not a valid mu bucket export at ${bucketDir}: ${reason}`);
|
|
16950
|
-
this.bucketDir = bucketDir;
|
|
16951
|
-
this.reason = reason;
|
|
16952
|
-
}
|
|
16953
|
-
bucketDir;
|
|
16954
|
-
reason;
|
|
16955
|
-
name = "ImportBucketInvalidError";
|
|
16956
|
-
errorNextSteps() {
|
|
16957
|
-
return [
|
|
16958
|
-
{ intent: "List the directory's contents", command: `ls ${this.bucketDir}` },
|
|
16959
|
-
{
|
|
16960
|
-
intent: "Inspect the manifest (must be bucketVersion 2)",
|
|
16961
|
-
command: `cat ${this.bucketDir}/manifest.json`
|
|
16962
|
-
}
|
|
16963
|
-
];
|
|
16964
|
-
}
|
|
16965
|
-
};
|
|
16966
|
-
var ImportSourceNotInBucketError = class extends Error {
|
|
16967
|
-
constructor(bucketDir, badName, validNames) {
|
|
16968
|
-
super(
|
|
16969
|
-
`--source-ws "${badName}" is not a source-ws in bucket ${bucketDir}; valid: ${validNames.length === 0 ? "<none>" : validNames.join(", ")}`
|
|
16970
|
-
);
|
|
16971
|
-
this.bucketDir = bucketDir;
|
|
16972
|
-
this.badName = badName;
|
|
16973
|
-
this.validNames = validNames;
|
|
16974
|
-
}
|
|
16975
|
-
bucketDir;
|
|
16976
|
-
badName;
|
|
16977
|
-
validNames;
|
|
16978
|
-
name = "ImportSourceNotInBucketError";
|
|
16979
|
-
errorNextSteps() {
|
|
16980
|
-
return [
|
|
16981
|
-
{ intent: "List the bucket's source-ws subdirs", command: `ls ${this.bucketDir}` },
|
|
16982
|
-
{
|
|
16983
|
-
intent: "Inspect the bucket manifest's sources map",
|
|
16984
|
-
command: `cat ${this.bucketDir}/manifest.json | head -40`
|
|
16985
|
-
}
|
|
16986
|
-
];
|
|
16987
|
-
}
|
|
16988
|
-
};
|
|
16989
|
-
var WorkstreamAlreadyExistsError = class extends Error {
|
|
16990
|
-
constructor(workstream) {
|
|
16991
|
-
super(
|
|
16992
|
-
`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.`
|
|
16993
|
-
);
|
|
16994
|
-
this.workstream = workstream;
|
|
16995
|
-
}
|
|
16996
|
-
workstream;
|
|
16997
|
-
name = "WorkstreamAlreadyExistsError";
|
|
16998
|
-
errorNextSteps() {
|
|
16999
|
-
return [
|
|
17000
|
-
{
|
|
17001
|
-
intent: "Import under a new name (single-source bucket only)",
|
|
17002
|
-
command: "mu workstream import <bucket> --workstream <new-name>"
|
|
17003
|
-
},
|
|
17004
|
-
{
|
|
17005
|
-
intent: "Or destroy the existing workstream first",
|
|
17006
|
-
command: `mu workstream destroy -w ${this.workstream} --yes`
|
|
17007
|
-
}
|
|
17008
|
-
];
|
|
17009
|
-
}
|
|
17010
|
-
};
|
|
17011
|
-
var ImportFrontmatterParseError = class extends Error {
|
|
17012
|
-
constructor(path2, line, raw) {
|
|
17013
|
-
super(`failed to parse frontmatter at ${path2}:${line}: ${raw}`);
|
|
17014
|
-
this.path = path2;
|
|
17015
|
-
this.line = line;
|
|
17016
|
-
this.raw = raw;
|
|
17017
|
-
}
|
|
17018
|
-
path;
|
|
17019
|
-
line;
|
|
17020
|
-
raw;
|
|
17021
|
-
name = "ImportFrontmatterParseError";
|
|
17022
|
-
errorNextSteps() {
|
|
17023
|
-
return [{ intent: "Inspect the offending file", command: `sed -n 1,30p ${this.path}` }];
|
|
17024
|
-
}
|
|
17025
|
-
};
|
|
17026
|
-
var ImportEdgeRefMissingError = class extends Error {
|
|
17027
|
-
constructor(fromTask, toTask, direction) {
|
|
17028
|
-
super(
|
|
17029
|
-
`task "${fromTask}" references "${toTask}" via ${direction}, but no task with that id was found in the import`
|
|
17030
|
-
);
|
|
17031
|
-
this.fromTask = fromTask;
|
|
17032
|
-
this.toTask = toTask;
|
|
17033
|
-
this.direction = direction;
|
|
17034
|
-
}
|
|
17035
|
-
fromTask;
|
|
17036
|
-
toTask;
|
|
17037
|
-
direction;
|
|
17038
|
-
name = "ImportEdgeRefMissingError";
|
|
17039
|
-
errorNextSteps() {
|
|
17040
|
-
return [
|
|
17041
|
-
{
|
|
17042
|
-
intent: "Inspect the offending task file in the bucket",
|
|
17043
|
-
command: `grep -l 'id: "${this.fromTask}"' <bucket>/*/tasks/`
|
|
17044
|
-
}
|
|
17045
|
-
];
|
|
17046
|
-
}
|
|
17047
|
-
};
|
|
17048
|
-
function unquote(raw) {
|
|
17049
|
-
const trimmed = raw.trim();
|
|
17050
|
-
if (trimmed === "null") return null;
|
|
17051
|
-
if (trimmed.length < 2 || trimmed[0] !== '"' || trimmed[trimmed.length - 1] !== '"') {
|
|
17052
|
-
return raw.trim();
|
|
17053
|
-
}
|
|
17054
|
-
const inner = trimmed.slice(1, -1);
|
|
17055
|
-
let out = "";
|
|
17056
|
-
for (let i = 0; i < inner.length; i++) {
|
|
17057
|
-
const ch = inner[i];
|
|
17058
|
-
if (ch === "\\" && i + 1 < inner.length) {
|
|
17059
|
-
const next = inner[i + 1];
|
|
17060
|
-
if (next === '"' || next === "\\") {
|
|
17061
|
-
out += next;
|
|
17062
|
-
i += 1;
|
|
17063
|
-
continue;
|
|
17064
|
-
}
|
|
17065
|
-
}
|
|
17066
|
-
if (ch !== void 0) out += ch;
|
|
17067
|
-
}
|
|
17068
|
-
return out;
|
|
17069
|
-
}
|
|
17070
|
-
function parseStringArray(raw) {
|
|
17071
|
-
const trimmed = raw.trim();
|
|
17072
|
-
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
|
|
17073
|
-
throw new Error(`expected [..] array, got ${JSON.stringify(raw)}`);
|
|
17074
|
-
}
|
|
17075
|
-
const inner = trimmed.slice(1, -1).trim();
|
|
17076
|
-
if (inner === "") return [];
|
|
17077
|
-
const out = [];
|
|
17078
|
-
let i = 0;
|
|
17079
|
-
while (i < inner.length) {
|
|
17080
|
-
while (i < inner.length && (inner[i] === " " || inner[i] === ",")) i += 1;
|
|
17081
|
-
if (i >= inner.length) break;
|
|
17082
|
-
if (inner[i] !== '"') {
|
|
17083
|
-
throw new Error(`expected quoted string in array, got ${JSON.stringify(inner.slice(i))}`);
|
|
17084
|
-
}
|
|
17085
|
-
let j = i + 1;
|
|
17086
|
-
while (j < inner.length) {
|
|
17087
|
-
if (inner[j] === "\\" && j + 1 < inner.length) {
|
|
17088
|
-
j += 2;
|
|
17089
|
-
continue;
|
|
17090
|
-
}
|
|
17091
|
-
if (inner[j] === '"') break;
|
|
17092
|
-
j += 1;
|
|
17093
|
-
}
|
|
17094
|
-
if (j >= inner.length) {
|
|
17095
|
-
throw new Error("unterminated quoted string in array");
|
|
17096
|
-
}
|
|
17097
|
-
const scalar = unquote(inner.slice(i, j + 1));
|
|
17098
|
-
if (scalar === null) {
|
|
17099
|
-
throw new Error("null is not a legal array element");
|
|
17100
|
-
}
|
|
17101
|
-
out.push(scalar);
|
|
17102
|
-
i = j + 1;
|
|
17103
|
-
}
|
|
17104
|
-
return out;
|
|
17105
|
-
}
|
|
17106
|
-
function parseTaskMarkdown(path2) {
|
|
17107
|
-
const raw = readFileSync2(path2, "utf8");
|
|
17108
|
-
const lines = raw.split("\n");
|
|
17109
|
-
if (lines[0] !== "---") {
|
|
17110
|
-
throw new ImportFrontmatterParseError(path2, 1, lines[0] ?? "");
|
|
17111
|
-
}
|
|
17112
|
-
let end = -1;
|
|
17113
|
-
for (let i2 = 1; i2 < lines.length; i2++) {
|
|
17114
|
-
if (lines[i2] === "---") {
|
|
17115
|
-
end = i2;
|
|
17116
|
-
break;
|
|
17117
|
-
}
|
|
17118
|
-
}
|
|
17119
|
-
if (end === -1) {
|
|
17120
|
-
throw new ImportFrontmatterParseError(path2, 1, "missing closing '---' for frontmatter");
|
|
17121
|
-
}
|
|
17122
|
-
const fields = {};
|
|
17123
|
-
for (let i2 = 1; i2 < end; i2++) {
|
|
17124
|
-
const line = lines[i2] ?? "";
|
|
17125
|
-
const colon = line.indexOf(":");
|
|
17126
|
-
if (colon === -1) {
|
|
17127
|
-
throw new ImportFrontmatterParseError(path2, i2 + 1, line);
|
|
17128
|
-
}
|
|
17129
|
-
const key = line.slice(0, colon).trim();
|
|
17130
|
-
const value = line.slice(colon + 1).trim();
|
|
17131
|
-
fields[key] = value;
|
|
17132
|
-
}
|
|
17133
|
-
function require2(key) {
|
|
17134
|
-
const v = fields[key];
|
|
17135
|
-
if (v === void 0) {
|
|
17136
|
-
throw new ImportFrontmatterParseError(path2, 1, `missing frontmatter key: ${key}`);
|
|
17137
|
-
}
|
|
17138
|
-
return v;
|
|
17139
|
-
}
|
|
17140
|
-
const id = unquote(require2("id"));
|
|
17141
|
-
if (id === null || id === "") {
|
|
17142
|
-
throw new ImportFrontmatterParseError(path2, 1, "id must be a non-empty string");
|
|
17143
|
-
}
|
|
17144
|
-
const workstream = unquote(require2("workstream"));
|
|
17145
|
-
if (workstream === null || workstream === "") {
|
|
17146
|
-
throw new ImportFrontmatterParseError(path2, 1, "workstream must be a non-empty string");
|
|
17147
|
-
}
|
|
17148
|
-
const statusRaw = require2("status");
|
|
17149
|
-
if (!isTaskStatus(statusRaw)) {
|
|
17150
|
-
throw new ImportFrontmatterParseError(path2, 1, `unknown status: ${statusRaw}`);
|
|
17151
|
-
}
|
|
17152
|
-
const impact = Number(require2("impact"));
|
|
17153
|
-
const effortDays = Number(require2("effort_days"));
|
|
17154
|
-
if (!Number.isFinite(impact) || !Number.isFinite(effortDays)) {
|
|
17155
|
-
throw new ImportFrontmatterParseError(path2, 1, "impact / effort_days must be numeric");
|
|
17156
|
-
}
|
|
17157
|
-
const ownerName = unquote(require2("owner"));
|
|
17158
|
-
const createdAt = unquote(require2("created_at"));
|
|
17159
|
-
const updatedAt = unquote(require2("updated_at"));
|
|
17160
|
-
if (createdAt === null || updatedAt === null) {
|
|
17161
|
-
throw new ImportFrontmatterParseError(path2, 1, "created_at / updated_at cannot be null");
|
|
17162
|
-
}
|
|
17163
|
-
let blockedBy;
|
|
17164
|
-
let blocks;
|
|
17165
|
-
try {
|
|
17166
|
-
blockedBy = parseStringArray(require2("blocked_by"));
|
|
17167
|
-
blocks = parseStringArray(require2("blocks"));
|
|
17168
|
-
} catch (err) {
|
|
17169
|
-
throw new ImportFrontmatterParseError(
|
|
17170
|
-
path2,
|
|
17171
|
-
1,
|
|
17172
|
-
err instanceof Error ? err.message : String(err)
|
|
17173
|
-
);
|
|
17174
|
-
}
|
|
17175
|
-
let bodyIdx = end + 1;
|
|
17176
|
-
while (bodyIdx < lines.length && lines[bodyIdx] === "") bodyIdx += 1;
|
|
17177
|
-
const titleLine = lines[bodyIdx] ?? "";
|
|
17178
|
-
if (!titleLine.startsWith("# ")) {
|
|
17179
|
-
throw new ImportFrontmatterParseError(
|
|
17180
|
-
path2,
|
|
17181
|
-
bodyIdx + 1,
|
|
17182
|
-
"expected '# <title>' after frontmatter"
|
|
17183
|
-
);
|
|
17184
|
-
}
|
|
17185
|
-
const title = titleLine.slice(2).trim();
|
|
17186
|
-
const notes = [];
|
|
17187
|
-
let i = bodyIdx + 1;
|
|
17188
|
-
while (i < lines.length && !(lines[i] ?? "").startsWith("## Notes (")) i += 1;
|
|
17189
|
-
if (i < lines.length) {
|
|
17190
|
-
i += 1;
|
|
17191
|
-
while (i < lines.length) {
|
|
17192
|
-
const line = lines[i] ?? "";
|
|
17193
|
-
if (!line.startsWith("### #")) {
|
|
17194
|
-
i += 1;
|
|
17195
|
-
continue;
|
|
17196
|
-
}
|
|
17197
|
-
const headerRest = line.slice(line.indexOf(" by ") + 4);
|
|
17198
|
-
const lastComma = headerRest.lastIndexOf(", ");
|
|
17199
|
-
if (lastComma === -1) {
|
|
17200
|
-
throw new ImportFrontmatterParseError(path2, i + 1, line);
|
|
17201
|
-
}
|
|
17202
|
-
const authorRaw = headerRest.slice(0, lastComma);
|
|
17203
|
-
const author = unquote(authorRaw);
|
|
17204
|
-
const createdAtNote = headerRest.slice(lastComma + 2);
|
|
17205
|
-
i += 1;
|
|
17206
|
-
while (i < lines.length && lines[i] === "") i += 1;
|
|
17207
|
-
const openFence = lines[i] ?? "";
|
|
17208
|
-
if (!/^`{3,}$/.test(openFence)) {
|
|
17209
|
-
throw new ImportFrontmatterParseError(path2, i + 1, openFence);
|
|
17210
|
-
}
|
|
17211
|
-
const fence = openFence;
|
|
17212
|
-
i += 1;
|
|
17213
|
-
const contentLines = [];
|
|
17214
|
-
while (i < lines.length && lines[i] !== fence) {
|
|
17215
|
-
contentLines.push(lines[i] ?? "");
|
|
17216
|
-
i += 1;
|
|
17217
|
-
}
|
|
17218
|
-
if (i >= lines.length) {
|
|
17219
|
-
throw new ImportFrontmatterParseError(path2, i + 1, `unterminated note fence ${fence}`);
|
|
17220
|
-
}
|
|
17221
|
-
i += 1;
|
|
17222
|
-
notes.push({ author, createdAt: createdAtNote, content: contentLines.join("\n") });
|
|
17223
|
-
}
|
|
17224
|
-
}
|
|
17225
|
-
return {
|
|
17226
|
-
id,
|
|
17227
|
-
workstream,
|
|
17228
|
-
status: statusRaw,
|
|
17229
|
-
impact,
|
|
17230
|
-
effortDays,
|
|
17231
|
-
ownerName,
|
|
17232
|
-
createdAt,
|
|
17233
|
-
updatedAt,
|
|
17234
|
-
blockedBy,
|
|
17235
|
-
blocks,
|
|
17236
|
-
title,
|
|
17237
|
-
notes
|
|
17238
|
-
};
|
|
17239
|
-
}
|
|
17240
|
-
function walkBucket(bucketDir) {
|
|
17241
|
-
if (!existsSync12(bucketDir)) {
|
|
17242
|
-
throw new ImportBucketInvalidError(bucketDir, "directory does not exist");
|
|
17243
|
-
}
|
|
17244
|
-
if (!statSync4(bucketDir).isDirectory()) {
|
|
17245
|
-
throw new ImportBucketInvalidError(bucketDir, "not a directory");
|
|
17246
|
-
}
|
|
17247
|
-
const manifestPath = join8(bucketDir, "manifest.json");
|
|
17248
|
-
const probe = readManifest(manifestPath);
|
|
17249
|
-
if (probe.kind === "v2") {
|
|
17250
|
-
const manifest = probe.manifest;
|
|
17251
|
-
const sources = [];
|
|
17252
|
-
for (const entry of readdirSync3(bucketDir, { withFileTypes: true })) {
|
|
17253
|
-
if (!entry.isDirectory()) continue;
|
|
17254
|
-
const sourceDir = join8(bucketDir, entry.name);
|
|
17255
|
-
const tasksDir2 = join8(sourceDir, "tasks");
|
|
17256
|
-
if (!existsSync12(tasksDir2) || !statSync4(tasksDir2).isDirectory()) continue;
|
|
17257
|
-
const taskFiles2 = [];
|
|
17258
|
-
for (const f of readdirSync3(tasksDir2, { withFileTypes: true })) {
|
|
17259
|
-
if (f.isFile() && f.name.endsWith(".md")) {
|
|
17260
|
-
taskFiles2.push(join8(tasksDir2, f.name));
|
|
17261
|
-
}
|
|
17262
|
-
}
|
|
17263
|
-
taskFiles2.sort();
|
|
17264
|
-
sources.push({ diskName: entry.name, taskFiles: taskFiles2 });
|
|
17265
|
-
}
|
|
17266
|
-
sources.sort((a, b) => a.diskName.localeCompare(b.diskName));
|
|
17267
|
-
return { manifest, sources, shape: "bucket" };
|
|
17268
|
-
}
|
|
17269
|
-
if (probe.kind === "corrupt") {
|
|
17270
|
-
throw new ImportBucketInvalidError(bucketDir, "manifest.json is unreadable / malformed");
|
|
17271
|
-
}
|
|
17272
|
-
const tasksDir = join8(bucketDir, "tasks");
|
|
17273
|
-
const looksLikeSourceWs = existsSync12(join8(bucketDir, "README.md")) && existsSync12(join8(bucketDir, "INDEX.md")) && existsSync12(tasksDir) && statSync4(tasksDir).isDirectory();
|
|
17274
|
-
if (!looksLikeSourceWs) {
|
|
17275
|
-
throw new ImportBucketInvalidError(bucketDir, "manifest.json missing");
|
|
17276
|
-
}
|
|
17277
|
-
const parentDir = dirname5(bucketDir);
|
|
17278
|
-
const baseName = basename2(bucketDir);
|
|
17279
|
-
const parentProbe = readManifest(join8(parentDir, "manifest.json"));
|
|
17280
|
-
if (parentProbe.kind !== "v2") {
|
|
17281
|
-
throw new ImportBucketInvalidError(
|
|
17282
|
-
bucketDir,
|
|
17283
|
-
`${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.`
|
|
17284
|
-
);
|
|
17285
|
-
}
|
|
17286
|
-
const parentManifest = parentProbe.manifest;
|
|
17287
|
-
if (!Object.prototype.hasOwnProperty.call(parentManifest.sources ?? {}, baseName)) {
|
|
17288
|
-
throw new ImportBucketInvalidError(
|
|
17289
|
-
bucketDir,
|
|
17290
|
-
`${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.`
|
|
17291
|
-
);
|
|
17292
|
-
}
|
|
17293
|
-
const taskFiles = [];
|
|
17294
|
-
for (const f of readdirSync3(tasksDir, { withFileTypes: true })) {
|
|
17295
|
-
if (f.isFile() && f.name.endsWith(".md")) {
|
|
17296
|
-
taskFiles.push(join8(tasksDir, f.name));
|
|
17297
|
-
}
|
|
17298
|
-
}
|
|
17299
|
-
taskFiles.sort();
|
|
17300
|
-
return {
|
|
17301
|
-
manifest: parentManifest,
|
|
17302
|
-
sources: [{ diskName: baseName, taskFiles }],
|
|
17303
|
-
shape: "sourceWsSubdir"
|
|
17304
|
-
};
|
|
17305
|
-
}
|
|
17306
|
-
function importOneSource(db, targetWorkstream, parsed, options) {
|
|
17307
|
-
const idSet = new Set(parsed.map((p) => p.id));
|
|
17308
|
-
for (const t of parsed) {
|
|
17309
|
-
for (const blocker of t.blockedBy) {
|
|
17310
|
-
if (!idSet.has(blocker)) {
|
|
17311
|
-
throw new ImportEdgeRefMissingError(t.id, blocker, "blocked_by");
|
|
17312
|
-
}
|
|
17313
|
-
}
|
|
17314
|
-
for (const dep of t.blocks) {
|
|
17315
|
-
if (!idSet.has(dep)) {
|
|
17316
|
-
throw new ImportEdgeRefMissingError(t.id, dep, "blocks");
|
|
17317
|
-
}
|
|
17318
|
-
}
|
|
17319
|
-
}
|
|
17320
|
-
if (options.dryRun) {
|
|
17321
|
-
const edgePairs = /* @__PURE__ */ new Set();
|
|
17322
|
-
for (const t of parsed) {
|
|
17323
|
-
for (const blocker of t.blockedBy) edgePairs.add(`${blocker}\0${t.id}`);
|
|
17324
|
-
for (const dep of t.blocks) edgePairs.add(`${t.id}\0${dep}`);
|
|
17325
|
-
}
|
|
17326
|
-
const noteCount = parsed.reduce((acc, p) => acc + p.notes.length, 0);
|
|
17327
|
-
return {
|
|
17328
|
-
tasksImported: parsed.length,
|
|
17329
|
-
edgesImported: edgePairs.size,
|
|
17330
|
-
notesImported: noteCount
|
|
17331
|
-
};
|
|
17332
|
-
}
|
|
17333
|
-
return db.transaction(() => {
|
|
17334
|
-
ensureWorkstream(db, targetWorkstream);
|
|
17335
|
-
const wsId = resolveWorkstreamId(db, targetWorkstream);
|
|
17336
|
-
const insertTask = db.prepare(
|
|
17337
|
-
`INSERT INTO tasks
|
|
17338
|
-
(workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
|
|
17339
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
17340
|
-
);
|
|
17341
|
-
const taskIdByLocalId = /* @__PURE__ */ new Map();
|
|
17342
|
-
for (const t of parsed) {
|
|
17343
|
-
const r = insertTask.run(
|
|
17344
|
-
wsId,
|
|
17345
|
-
t.id,
|
|
17346
|
-
t.title,
|
|
17347
|
-
t.status,
|
|
17348
|
-
t.impact,
|
|
17349
|
-
t.effortDays,
|
|
17350
|
-
t.createdAt,
|
|
17351
|
-
t.updatedAt
|
|
17352
|
-
);
|
|
17353
|
-
taskIdByLocalId.set(t.id, Number(r.lastInsertRowid));
|
|
17354
|
-
}
|
|
17355
|
-
const insertEdge = db.prepare(
|
|
17356
|
-
"INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
|
|
17357
|
-
);
|
|
17358
|
-
const seenEdges = /* @__PURE__ */ new Set();
|
|
17359
|
-
let edgesImported = 0;
|
|
17360
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17361
|
-
const recordEdge = (fromLocal, toLocal) => {
|
|
17362
|
-
const key = `${fromLocal}\0${toLocal}`;
|
|
17363
|
-
if (seenEdges.has(key)) return;
|
|
17364
|
-
seenEdges.add(key);
|
|
17365
|
-
const fromId = taskIdByLocalId.get(fromLocal);
|
|
17366
|
-
const toId = taskIdByLocalId.get(toLocal);
|
|
17367
|
-
if (fromId === void 0 || toId === void 0) return;
|
|
17368
|
-
const r = insertEdge.run(fromId, toId, now);
|
|
17369
|
-
if (r.changes > 0) edgesImported += 1;
|
|
17370
|
-
};
|
|
17371
|
-
for (const t of parsed) {
|
|
17372
|
-
for (const blocker of t.blockedBy) recordEdge(blocker, t.id);
|
|
17373
|
-
for (const dep of t.blocks) recordEdge(t.id, dep);
|
|
17374
|
-
}
|
|
17375
|
-
const insertNote = db.prepare(
|
|
17376
|
-
"INSERT INTO task_notes (task_id, author, content, created_at) VALUES (?, ?, ?, ?)"
|
|
17377
|
-
);
|
|
17378
|
-
let notesImported = 0;
|
|
17379
|
-
for (const t of parsed) {
|
|
17380
|
-
const taskId = taskIdByLocalId.get(t.id);
|
|
17381
|
-
if (taskId === void 0) continue;
|
|
17382
|
-
for (const note of t.notes) {
|
|
17383
|
-
insertNote.run(taskId, note.author, note.content, note.createdAt);
|
|
17384
|
-
notesImported += 1;
|
|
17385
|
-
}
|
|
17386
|
-
}
|
|
17387
|
-
emitEvent(
|
|
17388
|
-
db,
|
|
17389
|
-
targetWorkstream,
|
|
17390
|
-
`workstream import ${targetWorkstream} (tasks=${parsed.length}, edges=${edgesImported}, notes=${notesImported})`
|
|
17391
|
-
);
|
|
17392
|
-
return {
|
|
17393
|
-
tasksImported: parsed.length,
|
|
17394
|
-
edgesImported,
|
|
17395
|
-
notesImported
|
|
17396
|
-
};
|
|
17397
|
-
})();
|
|
17398
|
-
}
|
|
17399
|
-
function importBucket(db, opts) {
|
|
17400
|
-
const { manifest, sources, shape } = walkBucket(opts.bucketDir);
|
|
17401
|
-
if (shape === "sourceWsSubdir" && opts.sourceWs !== void 0 && opts.sourceWs.length > 0) {
|
|
17402
|
-
throw new ImportBucketInvalidError(
|
|
17403
|
-
opts.bucketDir,
|
|
17404
|
-
`cannot pass --source-ws when ${opts.bucketDir} is itself a source-ws subdir; drop the flag`
|
|
17405
|
-
);
|
|
17406
|
-
}
|
|
17407
|
-
let filteredSources = sources;
|
|
17408
|
-
if (opts.sourceWs !== void 0 && opts.sourceWs.length > 0) {
|
|
17409
|
-
const validNames = Object.keys(manifest.sources ?? {}).sort();
|
|
17410
|
-
const validSet = new Set(validNames);
|
|
17411
|
-
for (const wanted of opts.sourceWs) {
|
|
17412
|
-
if (!validSet.has(wanted)) {
|
|
17413
|
-
throw new ImportSourceNotInBucketError(opts.bucketDir, wanted, validNames);
|
|
17414
|
-
}
|
|
17415
|
-
}
|
|
17416
|
-
const wantedSet = new Set(opts.sourceWs);
|
|
17417
|
-
filteredSources = sources.filter((s) => wantedSet.has(s.diskName));
|
|
17418
|
-
}
|
|
17419
|
-
if (opts.workstreamOverride !== void 0 && filteredSources.length !== 1) {
|
|
17420
|
-
throw new ImportBucketInvalidError(
|
|
17421
|
-
opts.bucketDir,
|
|
17422
|
-
`--workstream override requires a single source-ws subdir; this bucket has ${filteredSources.length}`
|
|
17423
|
-
);
|
|
17424
|
-
}
|
|
17425
|
-
const dryRun = opts.dryRun === true;
|
|
17426
|
-
const results = [];
|
|
17427
|
-
for (const source of filteredSources) {
|
|
17428
|
-
const targetName = opts.workstreamOverride ?? source.diskName;
|
|
17429
|
-
const result = {
|
|
17430
|
-
workstreamName: targetName,
|
|
17431
|
-
tasksImported: 0,
|
|
17432
|
-
edgesImported: 0,
|
|
17433
|
-
notesImported: 0,
|
|
17434
|
-
tombstonesSkipped: 0,
|
|
17435
|
-
errors: []
|
|
17436
|
-
};
|
|
17437
|
-
const liveFiles = [];
|
|
17438
|
-
for (const file of source.taskFiles) {
|
|
17439
|
-
const head = readFileSync2(file, "utf8").slice(0, DELETED_BANNER_PREFIX.length);
|
|
17440
|
-
if (head.startsWith(DELETED_BANNER_PREFIX)) {
|
|
17441
|
-
result.tombstonesSkipped += 1;
|
|
17442
|
-
continue;
|
|
17443
|
-
}
|
|
17444
|
-
liveFiles.push(file);
|
|
17445
|
-
}
|
|
17446
|
-
if (!dryRun) {
|
|
17447
|
-
const existing = db.prepare("SELECT 1 AS x FROM workstreams WHERE name = ?").get(targetName);
|
|
17448
|
-
if (existing !== void 0) {
|
|
17449
|
-
const err = new WorkstreamAlreadyExistsError(targetName);
|
|
17450
|
-
result.errors.push(err.message);
|
|
17451
|
-
results.push(result);
|
|
17452
|
-
throw err;
|
|
17453
|
-
}
|
|
17454
|
-
}
|
|
17455
|
-
let parsed;
|
|
17456
|
-
try {
|
|
17457
|
-
parsed = liveFiles.map(parseTaskMarkdown);
|
|
17458
|
-
} catch (err) {
|
|
17459
|
-
result.errors.push(err instanceof Error ? err.message : String(err));
|
|
17460
|
-
results.push(result);
|
|
17461
|
-
throw err;
|
|
17462
|
-
}
|
|
17463
|
-
try {
|
|
17464
|
-
const counts = importOneSource(db, targetName, parsed, { dryRun });
|
|
17465
|
-
result.tasksImported = counts.tasksImported;
|
|
17466
|
-
result.edgesImported = counts.edgesImported;
|
|
17467
|
-
result.notesImported = counts.notesImported;
|
|
17468
|
-
} catch (err) {
|
|
17469
|
-
result.errors.push(err instanceof Error ? err.message : String(err));
|
|
17470
|
-
results.push(result);
|
|
17471
|
-
throw err;
|
|
17472
|
-
}
|
|
17473
|
-
results.push(result);
|
|
17474
|
-
}
|
|
17475
|
-
return {
|
|
17476
|
-
bucketLabel: manifest.bucketLabel,
|
|
17477
|
-
bucketVersion: manifest.bucketVersion,
|
|
17478
|
-
sources: results
|
|
17479
|
-
};
|
|
17480
|
-
}
|
|
17481
|
-
|
|
17482
|
-
// src/cli/handle.ts
|
|
17483
18480
|
init_output();
|
|
17484
18481
|
init_snapshots();
|
|
17485
18482
|
init_tasks();
|
|
@@ -17534,13 +18531,34 @@ var NameAmbiguousError = class extends Error {
|
|
|
17534
18531
|
}
|
|
17535
18532
|
};
|
|
17536
18533
|
function classifyError(err) {
|
|
17537
|
-
if (err instanceof UsageError || err instanceof WorkstreamNameInvalidError || err instanceof ArchiveLabelInvalidError || err instanceof
|
|
18534
|
+
if (err instanceof UsageError || err instanceof WorkstreamNameInvalidError || err instanceof ArchiveLabelInvalidError || err instanceof PruneOptionsInvalidError) {
|
|
17538
18535
|
return { label: "error", exitCode: 2 };
|
|
17539
18536
|
}
|
|
17540
18537
|
if (err instanceof AgentNotFoundError || err instanceof TaskNotFoundError || err instanceof WorkstreamNotFoundError || err instanceof WorkspaceNotFoundError || err instanceof SnapshotNotFoundError || err instanceof ArchiveNotFoundError) {
|
|
17541
18538
|
return { label: "not found", exitCode: 3 };
|
|
17542
18539
|
}
|
|
17543
|
-
if (err instanceof
|
|
18540
|
+
if (err instanceof DbImportManifestMissingError) {
|
|
18541
|
+
return { label: "db import manifest missing", exitCode: 8 };
|
|
18542
|
+
}
|
|
18543
|
+
if (err instanceof DbImportSchemaTooOldError) {
|
|
18544
|
+
return { label: "db import schema too old", exitCode: 9 };
|
|
18545
|
+
}
|
|
18546
|
+
if (err instanceof DbImportSchemaTooNewError) {
|
|
18547
|
+
return { label: "db import schema too new", exitCode: 10 };
|
|
18548
|
+
}
|
|
18549
|
+
if (err instanceof DbImportSourceStaleError) {
|
|
18550
|
+
return { label: "db import source stale", exitCode: 11 };
|
|
18551
|
+
}
|
|
18552
|
+
if (err instanceof DbImportConflictError) {
|
|
18553
|
+
return { label: "db import conflict", exitCode: 12 };
|
|
18554
|
+
}
|
|
18555
|
+
if (err instanceof DbReplayWorkstreamMissingError) {
|
|
18556
|
+
return { label: "db replay workstream missing", exitCode: 13 };
|
|
18557
|
+
}
|
|
18558
|
+
if (err instanceof DbReplayLocalIdConflictError) {
|
|
18559
|
+
return { label: "db replay local-id conflict", exitCode: 14 };
|
|
18560
|
+
}
|
|
18561
|
+
if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof TaskHasOpenDependentsError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SnapshotVersionMismatchError || err instanceof SchemaTooOldError || err instanceof TaskIdInvalidError || err instanceof ArchiveAlreadyExistsError || err instanceof ArchiveSourceAmbiguousError || err instanceof DbExportTargetExistsError || err instanceof WorkstreamExistsError) {
|
|
17544
18562
|
return { label: "conflict", exitCode: 4 };
|
|
17545
18563
|
}
|
|
17546
18564
|
if (err instanceof AgentSpawnCliNotFoundError) {
|
|
@@ -17923,9 +18941,9 @@ async function cmdUndo(db, opts = {}) {
|
|
|
17923
18941
|
wouldBePrunedGhosts: totalGhostsWouldBePruned,
|
|
17924
18942
|
orphansSurfaced: totalOrphans,
|
|
17925
18943
|
// Reconcile mode: "report-only" preserves the snapshot's
|
|
17926
|
-
// restored rows verbatim. (Was `dryRun: true` before
|
|
17927
|
-
//
|
|
17928
|
-
//
|
|
18944
|
+
// restored rows verbatim. (Was `dryRun: true` before named
|
|
18945
|
+
// modes — BREAKING for SDK consumers reading this field; see
|
|
18946
|
+
// CHANGELOG.)
|
|
17929
18947
|
mode: "report-only",
|
|
17930
18948
|
perWorkstream: reconcilePerWorkstream
|
|
17931
18949
|
},
|
|
@@ -18394,7 +19412,7 @@ async function cmdSql(db, query, opts = {}) {
|
|
|
18394
19412
|
}
|
|
18395
19413
|
}
|
|
18396
19414
|
function countTopLevelStatements(sql) {
|
|
18397
|
-
let
|
|
19415
|
+
let count2 = 0;
|
|
18398
19416
|
let inSingle = false;
|
|
18399
19417
|
let inDouble = false;
|
|
18400
19418
|
let inLineComment = false;
|
|
@@ -18450,15 +19468,15 @@ function countTopLevelStatements(sql) {
|
|
|
18450
19468
|
}
|
|
18451
19469
|
if (c === ";") {
|
|
18452
19470
|
if (sawNonWs) {
|
|
18453
|
-
|
|
19471
|
+
count2++;
|
|
18454
19472
|
sawNonWs = false;
|
|
18455
19473
|
}
|
|
18456
19474
|
continue;
|
|
18457
19475
|
}
|
|
18458
19476
|
if (c !== void 0 && /\S/.test(c)) sawNonWs = true;
|
|
18459
19477
|
}
|
|
18460
|
-
if (sawNonWs)
|
|
18461
|
-
return
|
|
19478
|
+
if (sawNonWs) count2++;
|
|
19479
|
+
return count2;
|
|
18462
19480
|
}
|
|
18463
19481
|
function formatCell(v) {
|
|
18464
19482
|
if (v === null || v === void 0) return pc.dim("null");
|
|
@@ -19202,75 +20220,6 @@ async function cmdWorkstreamExport(db, opts) {
|
|
|
19202
20220
|
);
|
|
19203
20221
|
printNextSteps(nextSteps);
|
|
19204
20222
|
}
|
|
19205
|
-
async function cmdWorkstreamImport(db, bucketDir, opts) {
|
|
19206
|
-
let sourceWs;
|
|
19207
|
-
if (opts.sourceWs !== void 0) {
|
|
19208
|
-
const canonical = parseCsvFlag(opts.sourceWs);
|
|
19209
|
-
if (canonical.length === 0) {
|
|
19210
|
-
throw new UsageError(
|
|
19211
|
-
"--source-ws was passed but resolved to zero names (empty strings / commas only); pass at least one source-ws name or drop the flag"
|
|
19212
|
-
);
|
|
19213
|
-
}
|
|
19214
|
-
sourceWs = canonical;
|
|
19215
|
-
}
|
|
19216
|
-
const result = importBucket(db, {
|
|
19217
|
-
bucketDir,
|
|
19218
|
-
workstreamOverride: opts.workstream,
|
|
19219
|
-
sourceWs,
|
|
19220
|
-
dryRun: opts.dryRun
|
|
19221
|
-
});
|
|
19222
|
-
const totalTasks = result.sources.reduce((acc, s) => acc + s.tasksImported, 0);
|
|
19223
|
-
const totalEdges = result.sources.reduce((acc, s) => acc + s.edgesImported, 0);
|
|
19224
|
-
const totalNotes = result.sources.reduce((acc, s) => acc + s.notesImported, 0);
|
|
19225
|
-
const totalTombstones = result.sources.reduce((acc, s) => acc + s.tombstonesSkipped, 0);
|
|
19226
|
-
const importedNames = result.sources.map((s) => s.workstreamName);
|
|
19227
|
-
const nextSteps = [];
|
|
19228
|
-
if (!opts.dryRun) {
|
|
19229
|
-
for (const name of importedNames) {
|
|
19230
|
-
nextSteps.push({
|
|
19231
|
-
intent: `Inspect ${name}`,
|
|
19232
|
-
command: `mu task tree -w ${name}`
|
|
19233
|
-
});
|
|
19234
|
-
}
|
|
19235
|
-
nextSteps.push({
|
|
19236
|
-
intent: "Re-export to verify the round trip is byte-stable",
|
|
19237
|
-
command: `mu workstream export -w ${importedNames[0] ?? "<ws>"} --out <new-dir>`
|
|
19238
|
-
});
|
|
19239
|
-
} else {
|
|
19240
|
-
const sourceWsFlag = sourceWs !== void 0 && sourceWs.length > 0 ? ` --source-ws ${sourceWs.join(",")}` : "";
|
|
19241
|
-
nextSteps.push({
|
|
19242
|
-
intent: "Run the import for real",
|
|
19243
|
-
command: `mu workstream import ${bucketDir}${opts.workstream ? ` --workstream ${opts.workstream}` : ""}${sourceWsFlag}`
|
|
19244
|
-
});
|
|
19245
|
-
}
|
|
19246
|
-
if (opts.json) {
|
|
19247
|
-
emitJson({
|
|
19248
|
-
...result,
|
|
19249
|
-
bucketDir,
|
|
19250
|
-
dryRun: opts.dryRun === true,
|
|
19251
|
-
totals: {
|
|
19252
|
-
tasks: totalTasks,
|
|
19253
|
-
edges: totalEdges,
|
|
19254
|
-
notes: totalNotes,
|
|
19255
|
-
tombstones: totalTombstones
|
|
19256
|
-
},
|
|
19257
|
-
nextSteps
|
|
19258
|
-
});
|
|
19259
|
-
return;
|
|
19260
|
-
}
|
|
19261
|
-
const verb = opts.dryRun ? "Would import" : "Imported";
|
|
19262
|
-
console.log(
|
|
19263
|
-
`${verb} ${pc.bold(String(result.sources.length))} source-ws from ${pc.bold(bucketDir)} ${pc.dim(
|
|
19264
|
-
`(bucketVersion=${result.bucketVersion}${result.bucketLabel ? `, label=${result.bucketLabel}` : ""}; tasks=${totalTasks}, edges=${totalEdges}, notes=${totalNotes}, tombstones_skipped=${totalTombstones})`
|
|
19265
|
-
)}`
|
|
19266
|
-
);
|
|
19267
|
-
for (const s of result.sources) {
|
|
19268
|
-
console.log(
|
|
19269
|
-
` ${pc.bold(s.workstreamName)}: tasks=${s.tasksImported}, edges=${s.edgesImported}, notes=${s.notesImported}, tombstones=${s.tombstonesSkipped}`
|
|
19270
|
-
);
|
|
19271
|
-
}
|
|
19272
|
-
printNextSteps(nextSteps);
|
|
19273
|
-
}
|
|
19274
20223
|
function autoExportDir(workstream) {
|
|
19275
20224
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
19276
20225
|
return join9(defaultStateDir(), "exports", `${workstream}-${ts}`);
|
|
@@ -19595,18 +20544,6 @@ function wireWorkstreamCommands(program) {
|
|
|
19595
20544
|
const opts = this.opts();
|
|
19596
20545
|
return handle((db) => cmdDestroy(db, opts), this)();
|
|
19597
20546
|
});
|
|
19598
|
-
workstream.command("import <bucket-dir>").description(
|
|
19599
|
-
"Rebuild a workstream (or a multi-source bucket of workstreams) from a v0.3 markdown export. Accepts EITHER a bucket directory (top-level manifest.json + per-source-ws subdirs) OR a single per-source-ws subdir (auto-detected via README.md + INDEX.md + tasks/, validated against the parent bucket's manifest). Per source-ws transactional: each source-ws is imported in its own SQLite transaction; siblings are unaffected by a sibling's failure. Refuses to merge silently into an existing workstream \u2014 pass --workstream <name> (single-source after any --source-ws filter only) or destroy the existing one first."
|
|
19600
|
-
).option(
|
|
19601
|
-
"--workstream <name>",
|
|
19602
|
-
"override the imported workstream's name (single-source after any --source-ws filter only)"
|
|
19603
|
-
).option(
|
|
19604
|
-
"--source-ws <names...>",
|
|
19605
|
-
"restrict the import to a subset of source-ws subdirs (repeat or comma-separate; or both)"
|
|
19606
|
-
).option("--dry-run", "walk + parse + validate; report what WOULD be created; no DB writes").option(...JSON_OPT).action(function(bucketDir) {
|
|
19607
|
-
const opts = this.opts();
|
|
19608
|
-
return handle((db) => cmdWorkstreamImport(db, bucketDir, opts), this)();
|
|
19609
|
-
});
|
|
19610
20547
|
workstream.command("export").description(
|
|
19611
20548
|
"Render a workstream's task graph + notes to a bucket directory of markdown. Bucket layout: <out>/README.md + INDEX.md + manifest.json (bucketVersion 2) + <ws>/{README.md,INDEX.md,tasks/<id>.md}. Idempotent + additive: re-export refreshes only changed task files; passing -w with a different workstream into the same --out appends a sibling source-ws subdir; deleted tasks are preserved with a banner. Pre-0.3 export dirs are not migrated in place."
|
|
19612
20549
|
).option(...WORKSTREAM_OPT).option("--out <dir>", "output directory (the bucket; defaults to ./<workstream>/)").option(...JSON_OPT).action(function() {
|
|
@@ -19857,9 +20794,9 @@ async function cmdBareTui(db, program, requestedWorkstreams) {
|
|
|
19857
20794
|
throw err;
|
|
19858
20795
|
}
|
|
19859
20796
|
}
|
|
19860
|
-
function
|
|
20797
|
+
function readPackageVersion2() {
|
|
19861
20798
|
try {
|
|
19862
|
-
const here = dirname7(
|
|
20799
|
+
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
19863
20800
|
const pkgPath = join10(here, "..", "package.json");
|
|
19864
20801
|
const raw = readFileSync3(pkgPath, "utf8");
|
|
19865
20802
|
const parsed = JSON.parse(raw);
|
|
@@ -19872,7 +20809,7 @@ function buildProgram() {
|
|
|
19872
20809
|
const program = new Command();
|
|
19873
20810
|
program.name("mu").description(
|
|
19874
20811
|
"Persistent crew of AI agents in tmux panes coordinated through a built-in task DAG."
|
|
19875
|
-
).version(
|
|
20812
|
+
).version(readPackageVersion2()).helpOption("-h, --help").configureHelp({ sortSubcommands: true }).enablePositionalOptions().option(
|
|
19876
20813
|
"-w, --workstream <names...>",
|
|
19877
20814
|
"workstream(s) to render (repeat or comma-separate; or both; defaults to $MU_SESSION or current tmux session)"
|
|
19878
20815
|
).option(...JSON_OPT).action(function() {
|
|
@@ -19894,13 +20831,15 @@ function buildProgram() {
|
|
|
19894
20831
|
wireStateCommands(program);
|
|
19895
20832
|
wireSqlCommand(program);
|
|
19896
20833
|
wireSnapshotCommands(program);
|
|
20834
|
+
wireDbCommands(program);
|
|
19897
20835
|
wireDoctorCommand(program);
|
|
19898
20836
|
applyAlphabeticalHelpSort(program);
|
|
19899
20837
|
applyExitOverride(program);
|
|
19900
20838
|
return program;
|
|
19901
20839
|
}
|
|
19902
20840
|
function applyAlphabeticalHelpSort(cmd) {
|
|
19903
|
-
|
|
20841
|
+
const keepSemanticOrder = cmd.name() === "archive";
|
|
20842
|
+
cmd.configureHelp({ ...cmd.configureHelp(), sortSubcommands: !keepSemanticOrder });
|
|
19904
20843
|
for (const sub of cmd.commands) {
|
|
19905
20844
|
applyAlphabeticalHelpSort(sub);
|
|
19906
20845
|
}
|