@martintrojer/mu 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 = 7;
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,13 +635,13 @@ 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).
@@ -2329,6 +2357,10 @@ ${scrollback.trim()}
2329
2357
  intent: "Or close + discard the workspace in one shot (lossy)",
2330
2358
  command: `mu agent close ${this.agentName} --discard-workspace`
2331
2359
  },
2360
+ {
2361
+ intent: "If the workstream was archived, restore task memory under a fresh name",
2362
+ command: "mu archive restore <label> --as <new-workstream> --source <workstream>"
2363
+ },
2332
2364
  {
2333
2365
  intent: "Or just inspect what's in the workspace",
2334
2366
  command: `cd ${this.workspacePath}`
@@ -4889,6 +4921,117 @@ var init_query = __esm({
4889
4921
  }
4890
4922
  });
4891
4923
 
4924
+ // src/archives/restore.ts
4925
+ function restoreArchive(db, label2, asWorkstream, opts = {}) {
4926
+ const archiveId = resolveArchiveId(db, label2);
4927
+ const sources = listSources(db, archiveId);
4928
+ if (!isValidWorkstreamName(asWorkstream)) throw new WorkstreamNameInvalidError(asWorkstream);
4929
+ const sourceWorkstream = opts.sourceWorkstream ?? sources[0];
4930
+ if (sourceWorkstream === void 0) throw new ArchiveSourceAmbiguousError(label2, sources);
4931
+ if (opts.sourceWorkstream === void 0 && sources.length > 1) {
4932
+ throw new ArchiveSourceAmbiguousError(label2, sources);
4933
+ }
4934
+ if (opts.sourceWorkstream !== void 0 && !sources.includes(opts.sourceWorkstream)) {
4935
+ throw new ArchiveSourceAmbiguousError(label2, sources);
4936
+ }
4937
+ if (tryResolveWorkstreamId(db, asWorkstream) !== null) {
4938
+ throw new WorkstreamExistsError(asWorkstream);
4939
+ }
4940
+ captureSnapshot(db, `archive restore ${label2} as ${asWorkstream}`, null);
4941
+ return db.transaction(() => {
4942
+ ensureWorkstream(db, asWorkstream);
4943
+ const wsId = resolveWorkstreamId(db, asWorkstream);
4944
+ const restoredTasks = db.prepare(
4945
+ `INSERT INTO tasks
4946
+ (workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
4947
+ SELECT ?, original_local_id, title, status, impact, effort_days, NULL,
4948
+ original_created_at, original_updated_at
4949
+ FROM archived_tasks
4950
+ WHERE archive_id = ? AND source_workstream = ?
4951
+ ORDER BY id`
4952
+ ).run(wsId, archiveId, sourceWorkstream).changes;
4953
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4954
+ const restoredEdges = db.prepare(
4955
+ `INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
4956
+ SELECT live_from.id, live_to.id, ?
4957
+ FROM archived_edges e
4958
+ JOIN archived_tasks arch_from ON arch_from.id = e.from_archived_id
4959
+ JOIN archived_tasks arch_to ON arch_to.id = e.to_archived_id
4960
+ JOIN tasks live_from ON live_from.workstream_id = ?
4961
+ AND live_from.local_id = arch_from.original_local_id
4962
+ JOIN tasks live_to ON live_to.workstream_id = ?
4963
+ AND live_to.local_id = arch_to.original_local_id
4964
+ WHERE e.archive_id = ?
4965
+ AND arch_from.source_workstream = ?
4966
+ AND arch_to.source_workstream = ?`
4967
+ ).run(now, wsId, wsId, archiveId, sourceWorkstream, sourceWorkstream).changes;
4968
+ const restoredNotes = db.prepare(
4969
+ `INSERT INTO task_notes (task_id, author, content, created_at)
4970
+ SELECT live.id, n.author, n.content, n.created_at
4971
+ FROM archived_notes n
4972
+ JOIN archived_tasks arch ON arch.id = n.archived_task_id
4973
+ JOIN tasks live ON live.workstream_id = ?
4974
+ AND live.local_id = arch.original_local_id
4975
+ WHERE n.archive_id = ? AND arch.source_workstream = ?
4976
+ ORDER BY n.id`
4977
+ ).run(wsId, archiveId, sourceWorkstream).changes;
4978
+ emitEvent(
4979
+ db,
4980
+ asWorkstream,
4981
+ `archive restore ${label2} source=${sourceWorkstream} as ${asWorkstream} (tasks=${restoredTasks}, edges=${restoredEdges}, notes=${restoredNotes})`
4982
+ );
4983
+ return {
4984
+ archiveLabel: label2,
4985
+ sourceWorkstream,
4986
+ workstreamName: asWorkstream,
4987
+ restoredTasks,
4988
+ restoredEdges,
4989
+ restoredNotes
4990
+ };
4991
+ })();
4992
+ }
4993
+ function listSources(db, archiveId) {
4994
+ return db.prepare(
4995
+ `SELECT source_workstream AS name
4996
+ FROM archived_tasks
4997
+ WHERE archive_id = ?
4998
+ GROUP BY source_workstream
4999
+ ORDER BY source_workstream`
5000
+ ).all(archiveId).map((row2) => row2.name);
5001
+ }
5002
+ var ArchiveSourceAmbiguousError;
5003
+ var init_restore2 = __esm({
5004
+ "src/archives/restore.ts"() {
5005
+ "use strict";
5006
+ init_db();
5007
+ init_logs();
5008
+ init_snapshots();
5009
+ init_workstream();
5010
+ init_core4();
5011
+ ArchiveSourceAmbiguousError = class extends Error {
5012
+ constructor(label2, sources) {
5013
+ super(
5014
+ sources.length === 0 ? `archive ${label2} contains no source workstreams` : `archive ${label2} requires --source <orig-ws-name>. Available: ${sources.join(", ")}`
5015
+ );
5016
+ this.label = label2;
5017
+ this.sources = sources;
5018
+ }
5019
+ label;
5020
+ sources;
5021
+ name = "ArchiveSourceAmbiguousError";
5022
+ errorNextSteps() {
5023
+ return [
5024
+ { intent: "Inspect archive sources", command: `mu archive show ${this.label}` },
5025
+ ...this.sources.map((source) => ({
5026
+ intent: `Restore source workstream ${source}`,
5027
+ command: `mu archive restore ${this.label} --source ${source} --as <new-workstream>`
5028
+ }))
5029
+ ];
5030
+ }
5031
+ };
5032
+ }
5033
+ });
5034
+
4892
5035
  // src/archives.ts
4893
5036
  var init_archives = __esm({
4894
5037
  "src/archives.ts"() {
@@ -4897,6 +5040,7 @@ var init_archives = __esm({
4897
5040
  init_core4();
4898
5041
  init_delete();
4899
5042
  init_query();
5043
+ init_restore2();
4900
5044
  }
4901
5045
  });
4902
5046
 
@@ -5717,7 +5861,7 @@ function exportWorkstream(db, opts) {
5717
5861
  source: sourceManifest
5718
5862
  };
5719
5863
  }
5720
- var WORKSTREAM_NAME_RE, RESERVED_WORKSTREAM_PREFIX, WorkstreamNameInvalidError;
5864
+ var WORKSTREAM_NAME_RE, RESERVED_WORKSTREAM_PREFIX, WorkstreamExistsError, WorkstreamNameInvalidError;
5721
5865
  var init_workstream = __esm({
5722
5866
  "src/workstream.ts"() {
5723
5867
  "use strict";
@@ -5730,6 +5874,27 @@ var init_workstream = __esm({
5730
5874
  init_workspace();
5731
5875
  WORKSTREAM_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
5732
5876
  RESERVED_WORKSTREAM_PREFIX = "mu-";
5877
+ WorkstreamExistsError = class extends Error {
5878
+ constructor(workstream) {
5879
+ super(`workstream already exists: ${workstream}`);
5880
+ this.workstream = workstream;
5881
+ }
5882
+ workstream;
5883
+ name = "WorkstreamExistsError";
5884
+ errorNextSteps() {
5885
+ return [
5886
+ {
5887
+ intent: "Pick a different workstream name",
5888
+ command: "mu archive restore <label> --as <new-name>"
5889
+ },
5890
+ { intent: "List existing workstreams", command: "mu workstream list" },
5891
+ {
5892
+ intent: "Destroy the existing workstream first",
5893
+ command: `mu workstream destroy -w ${this.workstream} --yes`
5894
+ }
5895
+ ];
5896
+ }
5897
+ };
5733
5898
  WorkstreamNameInvalidError = class extends Error {
5734
5899
  constructor(attempted) {
5735
5900
  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.`;
@@ -6882,6 +7047,7 @@ var init_agents = __esm({
6882
7047
  });
6883
7048
 
6884
7049
  // src/dag.ts
7050
+ import pc2 from "picocolors";
6885
7051
  function loadFullDag(db, workstream, opts = {}) {
6886
7052
  const tasks = listTasks(db, workstream).filter(
6887
7053
  (t) => opts.statuses === void 0 || opts.statuses.has(t.status)
@@ -6920,7 +7086,7 @@ function renderForest(roots, edges, statusFn, tasksByName, opts = {}) {
6920
7086
  if (!byName.has(root.name)) byName.set(root.name, root);
6921
7087
  const lines = [formatTreeNodeLabel(root, statusFn, opts)];
6922
7088
  if (seen.has(root.name)) {
6923
- lines[0] = `${lines[0]} (\u21BB already shown above)`;
7089
+ lines[0] = `${lines[0]}${RECURRENCE_MARKER}`;
6924
7090
  } else {
6925
7091
  seen.add(root.name);
6926
7092
  renderForestChildren(root.name, "", edges, byName, statusFn, seen, lines, opts);
@@ -6979,7 +7145,7 @@ function renderForestChildren(taskName, prefix, edges, byName, statusFn, seen, l
6979
7145
  }
6980
7146
  if (seen.has(childName)) {
6981
7147
  lines.push(
6982
- `${prefix}${branch}${formatTreeNodeLabel(child, statusFn, opts)} (\u21BB already shown above)`
7148
+ `${prefix}${branch}${formatTreeNodeLabel(child, statusFn, opts)}${RECURRENCE_MARKER}`
6983
7149
  );
6984
7150
  continue;
6985
7151
  }
@@ -6993,10 +7159,12 @@ function formatTreeNodeLabel(t, statusFn, opts = {}) {
6993
7159
  if (opts.includeTitle === false) return base;
6994
7160
  return `${base} ${t.title}`;
6995
7161
  }
7162
+ var RECURRENCE_MARKER;
6996
7163
  var init_dag = __esm({
6997
7164
  "src/dag.ts"() {
6998
7165
  "use strict";
6999
7166
  init_tasks();
7167
+ RECURRENCE_MARKER = ` ${pc2.dim("(\u21BB)")}`;
7000
7168
  }
7001
7169
  });
7002
7170
 
@@ -7450,14 +7618,14 @@ function agentStatusHistogram(agents) {
7450
7618
  return out;
7451
7619
  }
7452
7620
  function summarizeOwnedTasks(owned) {
7453
- const count = owned.length;
7454
- if (count === 0) return { bit: "\u2014", count: 0 };
7455
- if (count === 1) {
7621
+ const count2 = owned.length;
7622
+ if (count2 === 0) return { bit: "\u2014", count: 0 };
7623
+ if (count2 === 1) {
7456
7624
  const only = owned[0];
7457
7625
  if (!only) return { bit: "\u2014", count: 0 };
7458
7626
  return { bit: only.name, count: 1, onlyTaskId: only.name };
7459
7627
  }
7460
- return { bit: `\u2295${count}`, count };
7628
+ return { bit: `\u2295${count2}`, count: count2 };
7461
7629
  }
7462
7630
  var init_state = __esm({
7463
7631
  "src/state.ts"() {
@@ -9622,6 +9790,9 @@ var init_keymap_spec = __esm({
9622
9790
  row("/", "filter/search rows", ["/"]),
9623
9791
  row("Enter", "drill into focused row", ["Enter"]),
9624
9792
  row("y", "yank action for focused row", ["y"]),
9793
+ row("l", "launch lazygit in the project root (Commits popup only; user-driven TUI escape)", [
9794
+ "l"
9795
+ ]),
9625
9796
  row("Shift 0-9", "switch numbered popup", ["Shift 0-9"]),
9626
9797
  row("Esc/q", "back to dashboard", ["Esc", "q"]),
9627
9798
  row("?", "toggle this overlay", ["?"])
@@ -11362,13 +11533,59 @@ var init_blocked2 = __esm({
11362
11533
  }
11363
11534
  });
11364
11535
 
11365
- // src/cli/tui/tuicr.ts
11536
+ // src/cli/tui/lazygit.ts
11366
11537
  import { spawnSync } from "child_process";
11367
- function runTuicrInteractive(opts, deps = {}) {
11538
+ function runLazygitInteractive(opts, deps = {}) {
11368
11539
  const run3 = deps.spawn ?? spawnSync;
11369
11540
  const write = deps.write ?? ((text2) => process.stdout.write(text2));
11370
11541
  const env = deps.env ?? process.env;
11371
11542
  let result = { ok: true };
11543
+ try {
11544
+ write(ALT_SCREEN_EXIT);
11545
+ const r = run3("lazygit", [], {
11546
+ cwd: opts.cwd,
11547
+ stdio: "inherit",
11548
+ env
11549
+ });
11550
+ if (r.error !== void 0) {
11551
+ result = { ok: false, error: lazygitErrorMessage(r.error) };
11552
+ } else if (typeof r.status === "number" && r.status !== 0) {
11553
+ result = { ok: false, error: `lazygit exited ${r.status}` };
11554
+ }
11555
+ } catch (err) {
11556
+ result = { ok: false, error: lazygitErrorMessage(err) };
11557
+ } finally {
11558
+ try {
11559
+ write(ALT_SCREEN_ENTER);
11560
+ } catch {
11561
+ }
11562
+ }
11563
+ return result;
11564
+ }
11565
+ function lazygitErrorMessage(err) {
11566
+ if (err instanceof Error) {
11567
+ const code = err.code;
11568
+ if (code === "ENOENT") {
11569
+ return "lazygit not found \xB7 install from https://github.com/jesseduffield/lazygit";
11570
+ }
11571
+ return err.message.length > 0 ? err.message : String(err);
11572
+ }
11573
+ return String(err);
11574
+ }
11575
+ var init_lazygit = __esm({
11576
+ "src/cli/tui/lazygit.ts"() {
11577
+ "use strict";
11578
+ init_escapes();
11579
+ }
11580
+ });
11581
+
11582
+ // src/cli/tui/tuicr.ts
11583
+ import { spawnSync as spawnSync2 } from "child_process";
11584
+ function runTuicrInteractive(opts, deps = {}) {
11585
+ const run3 = deps.spawn ?? spawnSync2;
11586
+ const write = deps.write ?? ((text2) => process.stdout.write(text2));
11587
+ const env = deps.env ?? process.env;
11588
+ let result = { ok: true };
11372
11589
  try {
11373
11590
  write(ALT_SCREEN_EXIT);
11374
11591
  const r = run3("tuicr", ["-r", opts.rev], {
@@ -11544,6 +11761,13 @@ function CommitsPopup({
11544
11761
  void yank2(showCommandForBackend(backendName, c.sha));
11545
11762
  return;
11546
11763
  }
11764
+ case "verb":
11765
+ if (action.key === "l") {
11766
+ const r = runLazygitInteractive({ cwd: projectRoot });
11767
+ if (!r.ok) onFooter?.(r.error ?? "lazygit failed", false, "error");
11768
+ else onFooter?.("lazygit", true, "info");
11769
+ }
11770
+ return;
11547
11771
  }
11548
11772
  };
11549
11773
  usePopupActionQueue(popupActions, dispatchListAction);
@@ -11589,7 +11813,7 @@ function CommitsPopup({
11589
11813
  PopupShell,
11590
11814
  {
11591
11815
  title: `Commits \xB7 ${formatBackend2(backendName)} (${safeCursor + 1}/${commits.length})`,
11592
- hint: "y yanks VCS show command",
11816
+ hint: "y yanks VCS show command \xB7 l lazygit",
11593
11817
  children: [
11594
11818
  /* @__PURE__ */ jsx25(Box11, { flexDirection: "column", flexGrow: 1, children: visible.map((c, i) => {
11595
11819
  const row2 = rows[i];
@@ -11646,6 +11870,7 @@ var init_commits2 = __esm({
11646
11870
  init_vcs2();
11647
11871
  init_columns();
11648
11872
  init_keys();
11873
+ init_lazygit();
11649
11874
  init_list_row();
11650
11875
  init_popup_shell();
11651
11876
  init_tuicr();
@@ -13928,8 +14153,8 @@ function rowWidth(workstreams, active, indexes, leftHidden, rightHidden, showHin
13928
14153
  function chromeWidth(leftHidden, rightHidden, showHint) {
13929
14154
  return stringWidth4(PREFIX) + counterWidth("\u2039", leftHidden) + counterWidth("\u203A", rightHidden) + (showHint ? stringWidth4(leftHidden > 0 || rightHidden > 0 ? NARROW_HINT : WIDE_HINT) : 0);
13930
14155
  }
13931
- function counterWidth(prefix, count) {
13932
- return count > 0 ? stringWidth4(`${prefix}${count} `) : 0;
14156
+ function counterWidth(prefix, count2) {
14157
+ return count2 > 0 ? stringWidth4(`${prefix}${count2} `) : 0;
13933
14158
  }
13934
14159
  function truncateToWidth(text2, width) {
13935
14160
  if (width <= 0) return "";
@@ -14564,7 +14789,7 @@ var init_tui = __esm({
14564
14789
  init_agents();
14565
14790
  import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "fs";
14566
14791
  import { dirname as dirname7, join as join10 } from "path";
14567
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
14792
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
14568
14793
  import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commander";
14569
14794
 
14570
14795
  // src/cli/agents.ts
@@ -16443,9 +16668,15 @@ async function cmdArchiveAdd(db, label2, opts) {
16443
16668
  command: `mu archive add ${label2} -w ${workstream}`
16444
16669
  },
16445
16670
  {
16446
- intent: opts.destroy ? "Undo the destroy (DB only; tmux NOT rolled back)" : "Destroy the source workstream now that its memory is preserved",
16447
- command: opts.destroy ? "mu undo --yes" : `mu archive add ${label2} -w ${workstream} --destroy`
16448
- }
16671
+ intent: opts.destroy ? "Restore the archived workstream under a fresh name" : "Destroy the source workstream now that its memory is preserved",
16672
+ command: opts.destroy ? `mu archive restore ${label2} --as <new-workstream> --source ${workstream}` : `mu archive add ${label2} -w ${workstream} --destroy`
16673
+ },
16674
+ ...opts.destroy ? [
16675
+ {
16676
+ intent: "Undo the destroy (DB only; tmux NOT rolled back)",
16677
+ command: "mu undo --yes"
16678
+ }
16679
+ ] : []
16449
16680
  ]);
16450
16681
  }
16451
16682
  async function cmdArchiveRemove(db, label2, opts) {
@@ -16471,6 +16702,35 @@ async function cmdArchiveRemove(db, label2, opts) {
16471
16702
  }
16472
16703
  ]);
16473
16704
  }
16705
+ async function cmdArchiveRestore(db, label2, opts = {}) {
16706
+ if (!opts.as || opts.as.trim().length === 0) {
16707
+ throw new UsageError("--as <new-ws-name> is required for `mu archive restore`");
16708
+ }
16709
+ const result = restoreArchive(db, label2, opts.as, {
16710
+ sourceWorkstream: opts.source
16711
+ });
16712
+ const nextSteps = [
16713
+ { intent: "Inspect restored tasks", command: `mu task list -w ${result.workstreamName}` },
16714
+ { intent: "Undo (a snapshot was taken before the restore)", command: "mu undo --yes" }
16715
+ ];
16716
+ if (opts.json) {
16717
+ emitJson({ ...result, nextSteps });
16718
+ return;
16719
+ }
16720
+ console.log(
16721
+ `Restored archive ${pc.bold(label2)} source ${pc.bold(result.sourceWorkstream)} as workstream ${pc.bold(
16722
+ result.workstreamName
16723
+ )} ${pc.dim(
16724
+ `(tasks=${result.restoredTasks}, edges=${result.restoredEdges}, notes=${result.restoredNotes})`
16725
+ )}`
16726
+ );
16727
+ console.log(
16728
+ pc.dim(
16729
+ "agents, workspace_path, and agent_logs are not restored (archives preserve task graph rows, not live panes or the live event log)."
16730
+ )
16731
+ );
16732
+ printNextSteps(nextSteps);
16733
+ }
16474
16734
  async function cmdArchiveDelete(db, label2, opts = {}) {
16475
16735
  const summary = getArchive(db, label2);
16476
16736
  if (!opts.yes) {
@@ -16484,6 +16744,10 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
16484
16744
  {
16485
16745
  intent: "Confirm and actually delete (a snapshot is taken first)",
16486
16746
  command: `mu archive delete ${label2} --yes`
16747
+ },
16748
+ {
16749
+ intent: "Recover a source before deleting the archive",
16750
+ command: `mu archive restore ${label2} --as <new-workstream> --source <workstream>`
16487
16751
  }
16488
16752
  ]
16489
16753
  });
@@ -16507,6 +16771,10 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
16507
16771
  intent: "Confirm and actually delete",
16508
16772
  command: `mu archive delete ${label2} --yes`
16509
16773
  },
16774
+ {
16775
+ intent: "Recover a source before deleting the archive",
16776
+ command: `mu archive restore ${label2} --as <new-workstream> --source <workstream>`
16777
+ },
16510
16778
  {
16511
16779
  intent: "Surgically remove a single source workstream instead",
16512
16780
  command: `mu archive remove ${label2} -w <workstream>`
@@ -16524,7 +16792,7 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
16524
16792
  removedTasks: summary.totalTasks,
16525
16793
  nextSteps: [
16526
16794
  {
16527
- intent: "Undo (a snapshot was taken before the delete)",
16795
+ intent: "Recover the deleted archive (a snapshot was taken before the delete)",
16528
16796
  command: "mu undo --yes"
16529
16797
  }
16530
16798
  ]
@@ -16538,7 +16806,7 @@ async function cmdArchiveDelete(db, label2, opts = {}) {
16538
16806
  );
16539
16807
  printNextSteps([
16540
16808
  {
16541
- intent: "Undo (a snapshot was taken before the delete)",
16809
+ intent: "Recover the deleted archive (a snapshot was taken before the delete)",
16542
16810
  command: "mu undo --yes"
16543
16811
  }
16544
16812
  ]);
@@ -16605,7 +16873,11 @@ async function cmdArchiveExport(db, label2, opts = {}) {
16605
16873
  0
16606
16874
  );
16607
16875
  const nextSteps = [
16608
- { intent: "Browse the bucket", command: `ls ${result.outDir}` },
16876
+ { intent: "Browse the read-only human/git/docs bucket", command: `ls ${result.outDir}` },
16877
+ {
16878
+ intent: "Restore losslessly from the archive (not from this bucket)",
16879
+ command: `mu archive restore ${label2} --as <new-workstream> --source <workstream>`
16880
+ },
16609
16881
  {
16610
16882
  intent: "Re-export to refresh (additive; existing source-ws subdirs untouched)",
16611
16883
  command: `mu archive export ${label2} --out ${result.outDir}`
@@ -16635,6 +16907,11 @@ async function cmdArchiveExport(db, label2, opts = {}) {
16635
16907
  `(sources=${result.sourceCount}, tasks=${totalTasks}, written=${result.written}, unchanged=${result.unchanged}, preserved=${result.preserved})`
16636
16908
  )}`
16637
16909
  );
16910
+ console.log(
16911
+ pc.dim(
16912
+ "This bucket is a read-only artifact for humans/git/docs; use `mu archive restore` for lossless un-archive."
16913
+ )
16914
+ );
16638
16915
  printNextSteps(nextSteps);
16639
16916
  }
16640
16917
  function wireArchiveCommands(program) {
@@ -16658,14 +16935,23 @@ function wireArchiveCommands(program) {
16658
16935
  return handle((db) => cmdArchiveShow(db, label2, opts), this)();
16659
16936
  });
16660
16937
  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."
16938
+ "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
16939
  ).option(...WORKSTREAM_OPT).option(
16663
16940
  "--destroy",
16664
- "After a successful archive, also destroy the source workstream (kills tmux + frees workspaces + cascade-deletes DB rows)."
16941
+ "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
16942
  ).option(...JSON_OPT).action(function(label2) {
16666
16943
  const opts = this.optsWithGlobals();
16667
16944
  return handle((db) => cmdArchiveAdd(db, label2, opts), this)();
16668
16945
  });
16946
+ archive.command("restore <label>").description(
16947
+ "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)."
16948
+ ).requiredOption("--as <new-ws-name>", "fresh workstream name to create; refuses collisions").option(
16949
+ "--source <orig-ws-name>",
16950
+ "required when the archive contains multiple source workstreams"
16951
+ ).option(...JSON_OPT).action(function(label2) {
16952
+ const opts = this.opts();
16953
+ return handle((db) => cmdArchiveRestore(db, label2, opts), this)();
16954
+ });
16669
16955
  archive.command("remove <label>").description(
16670
16956
  "Surgically remove a single source workstream's contribution from an archive (rare; recovery). Other source workstreams' rows are untouched."
16671
16957
  ).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(label2) {
@@ -16679,7 +16965,7 @@ function wireArchiveCommands(program) {
16679
16965
  return handle((db) => cmdArchiveSearch(db, pattern, opts), this)();
16680
16966
  });
16681
16967
  archive.command("export <label>").description(
16682
- "Render every source workstream in an archive to a bucket directory of markdown (one subdir per source-ws + bucket-level README/INDEX/manifest). Idempotent + additive: re-running refreshes only changed task files."
16968
+ "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
16969
  ).option("--out <dir>", "output directory (the bucket); required").option(...JSON_OPT).action(function(label2) {
16684
16970
  const opts = this.opts();
16685
16971
  return handle((db) => cmdArchiveExport(db, label2, opts), this)();
@@ -16692,81 +16978,1098 @@ function wireArchiveCommands(program) {
16692
16978
  });
16693
16979
  }
16694
16980
 
16695
- // src/cli/doctor.ts
16696
- init_agents();
16981
+ // src/db-sync.ts
16697
16982
  init_db();
16698
- init_output();
16699
- init_tmux();
16700
- init_workstream();
16701
- async function cmdDoctor(db, opts = {}) {
16702
- if (opts.json) {
16703
- return cmdDoctorJson(db);
16983
+ init_logs();
16984
+ init_snapshots();
16985
+ import { randomUUID as randomUUID2 } from "crypto";
16986
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync2, unlinkSync as unlinkSync4, writeFileSync as writeFileSync2 } from "fs";
16987
+ import { hostname as hostname2 } from "os";
16988
+ import { dirname as dirname5, join as join8 } from "path";
16989
+ import { fileURLToPath as fileURLToPath2 } from "url";
16990
+
16991
+ // src/db-sync-replay.ts
16992
+ init_db();
16993
+ init_snapshots();
16994
+ import { createHash as createHash2 } from "crypto";
16995
+ var DbReplayWorkstreamMissingError = class extends Error {
16996
+ constructor(workstream) {
16997
+ super(
16998
+ `replay sidecar is for workstream "${workstream}", which does not exist locally; restore it first via mu db import or mu archive restore`
16999
+ );
17000
+ this.workstream = workstream;
16704
17001
  }
16705
- console.log(pc.bold("mu doctor"));
16706
- console.log(pc.bold("\nenvironment"));
17002
+ workstream;
17003
+ name = "DbReplayWorkstreamMissingError";
17004
+ errorNextSteps() {
17005
+ return [
17006
+ {
17007
+ intent: "Restore this workstream from a DB export",
17008
+ command: "mu db import <file> --apply"
17009
+ },
17010
+ {
17011
+ intent: "Or restore it from an archive",
17012
+ command: `mu archive restore <label> --as ${this.workstream}`
17013
+ }
17014
+ ];
17015
+ }
17016
+ };
17017
+ var DbReplayLocalIdConflictError = class extends Error {
17018
+ constructor(workstream, conflicts) {
17019
+ super(
17020
+ `sidecar task id collides with different local content in ${workstream}: ${conflicts.map(
17021
+ (c) => `${c.localId} (local: ${c.local.status} ${JSON.stringify(c.local.title)}; sidecar: ${c.sidecar.status} ${JSON.stringify(c.sidecar.title)})`
17022
+ ).join(", ")}`
17023
+ );
17024
+ this.workstream = workstream;
17025
+ this.conflicts = conflicts;
17026
+ }
17027
+ workstream;
17028
+ conflicts;
17029
+ name = "DbReplayLocalIdConflictError";
17030
+ errorNextSteps() {
17031
+ const first = this.conflicts[0];
17032
+ return [
17033
+ {
17034
+ intent: "Create a renamed local task manually, then replay notes if desired",
17035
+ 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>`
17036
+ },
17037
+ {
17038
+ intent: "Skip the colliding id and replay another task",
17039
+ command: "mu db replay <sidecar> --task <other-id> --apply"
17040
+ }
17041
+ ];
17042
+ }
17043
+ };
17044
+ function replayDb(db, file, opts = {}) {
17045
+ const sidecarDb = openDb({ path: file, readonly: true });
16707
17046
  try {
16708
- const version = (await tmux(["-V"])).trim();
16709
- console.log(` tmux : ${pc.green("ok")} (${version})`);
16710
- } catch {
16711
- console.log(` tmux : ${pc.red("NOT FOUND")} \u2014 install tmux \u2265 3.0`);
17047
+ const plan = buildReplayPlan(db, sidecarDb, file);
17048
+ const taskFilter = new Set(opts.tasks ?? []);
17049
+ const noteFilter = new Set(opts.notes ?? []);
17050
+ const selectedTaskIds = opts.all === true ? new Set(plan.tasks.map((t) => t.localId)) : taskFilter;
17051
+ const selectedNoteIds = opts.all === true ? new Set(plan.notes.map((n) => n.taskLocalId)) : noteFilter;
17052
+ const hasSelectors = opts.all === true || selectedTaskIds.size > 0 || selectedNoteIds.size > 0;
17053
+ const noteTaskIds = /* @__PURE__ */ new Set([...selectedNoteIds, ...selectedTaskIds]);
17054
+ const hasWrites = plan.tasks.some((t) => selectedTaskIds.has(t.localId)) || plan.notes.some((n) => noteTaskIds.has(n.taskLocalId)) || plan.edges.some(
17055
+ (e) => opts.all === true || selectedTaskIds.has(e.fromLocalId) || selectedTaskIds.has(e.toLocalId)
17056
+ );
17057
+ const relevantConflicts = opts.all === true ? plan.conflicts : plan.conflicts.filter((c) => selectedTaskIds.has(c.localId));
17058
+ if (relevantConflicts.length > 0) {
17059
+ throw new DbReplayLocalIdConflictError(plan.workstream, relevantConflicts);
17060
+ }
17061
+ if (opts.apply !== true || !hasSelectors) return replayResult(plan, true, false);
17062
+ if (!hasWrites) return replayResult(plan, false, true);
17063
+ const snapshot = captureSnapshot(db, `db replay ${file}`, null);
17064
+ const applied = applyReplayPlan(db, plan, selectedTaskIds, selectedNoteIds, opts.all === true);
17065
+ return { ...replayResult(plan, false, true), snapshotId: snapshot.id, ...applied };
17066
+ } finally {
17067
+ sidecarDb.close();
16712
17068
  }
16713
- console.log(` $TMUX : ${process.env.TMUX ? pc.green("set") : pc.yellow("not set")}`);
16714
- console.log(
16715
- ` $TMUX_PANE : ${process.env.TMUX_PANE ? pc.green(process.env.TMUX_PANE) : pc.dim("not set")}`
16716
- );
16717
- console.log(
16718
- ` $MU_SESSION : ${process.env.MU_SESSION ? pc.green(process.env.MU_SESSION) : pc.dim("not set")}`
17069
+ }
17070
+ function buildReplayPlan(localDb, sidecarDb, sourceFile) {
17071
+ const sidecarWorkstreams = listLocalWorkstreams(sidecarDb);
17072
+ const sidecarWs = sidecarWorkstreams[0];
17073
+ if (sidecarWorkstreams.length !== 1 || !sidecarWs) {
17074
+ throw new Error(
17075
+ `replay sidecar must contain exactly one workstream; found ${sidecarWorkstreams.length}`
17076
+ );
17077
+ }
17078
+ const localWs = listLocalWorkstreams(localDb).find((w) => w.name === sidecarWs.name);
17079
+ if (!localWs) throw new DbReplayWorkstreamMissingError(sidecarWs.name);
17080
+ const localTasks = new Map(
17081
+ localDb.prepare("SELECT local_id, title, status FROM tasks WHERE workstream_id = ?").all(localWs.id).map((t) => [t.local_id, t])
16719
17082
  );
16720
- console.log(pc.bold("\ndb"));
16721
- console.log(` path : ${pc.dim(defaultDbPath())}`);
16722
- try {
16723
- const tables = db.prepare(
16724
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
16725
- ).all().map((r) => r.name);
16726
- const missing = EXPECTED_TABLES.filter((t) => !tables.includes(t));
16727
- if (missing.length === 0) {
16728
- console.log(` schema : ${pc.green("ok")} (${EXPECTED_TABLES.length} tables)`);
16729
- } else {
16730
- console.log(` schema : ${pc.red("missing")} \u2014 ${missing.join(", ")}`);
17083
+ const tasks = [];
17084
+ const conflicts = [];
17085
+ for (const task of listReplayTasks(sidecarDb, sidecarWs.id)) {
17086
+ const local = localTasks.get(task.localId);
17087
+ if (!local) tasks.push(task);
17088
+ else if (local.title !== task.title || local.status !== task.status) {
17089
+ conflicts.push({
17090
+ localId: task.localId,
17091
+ local: { title: local.title, status: local.status },
17092
+ sidecar: { title: task.title, status: task.status }
17093
+ });
16731
17094
  }
16732
- try {
16733
- const row2 = db.prepare("SELECT version FROM schema_version WHERE id = 1").get();
16734
- const v = row2?.version;
16735
- if (v === void 0) {
16736
- console.log(
16737
- ` schema_version : ${pc.red("missing row")} (expected ${CURRENT_SCHEMA_VERSION})`
16738
- );
16739
- } else if (v === CURRENT_SCHEMA_VERSION) {
16740
- console.log(` schema_version : ${pc.green(String(v))}`);
16741
- } else if (v < CURRENT_SCHEMA_VERSION) {
16742
- console.log(
16743
- ` schema_version : ${pc.yellow(String(v))} (code expects ${CURRENT_SCHEMA_VERSION}; openDb should have migrated)`
16744
- );
16745
- } else {
16746
- console.log(
16747
- ` schema_version : ${pc.red(String(v))} (code expects ${CURRENT_SCHEMA_VERSION}; possible downgrade or future-version DB)`
16748
- );
16749
- }
16750
- } catch {
16751
- console.log(
16752
- ` schema_version : ${pc.red("unreadable")} (schema_version table missing or wrong shape)`
17095
+ }
17096
+ const localNoteHashes = new Set(listReplayNotes(localDb, localWs.id).map((n) => n.hash));
17097
+ const localEdges = new Set(listReplayEdges(localDb, localWs.id).map(edgeKey));
17098
+ return {
17099
+ sourceFile,
17100
+ workstream: sidecarWs.name,
17101
+ tasks,
17102
+ notes: listReplayNotes(sidecarDb, sidecarWs.id).filter((n) => !localNoteHashes.has(n.hash)),
17103
+ edges: listReplayEdges(sidecarDb, sidecarWs.id).filter((e) => !localEdges.has(edgeKey(e))),
17104
+ conflicts
17105
+ };
17106
+ }
17107
+ function applyReplayPlan(db, plan, selectedTaskIds, selectedNoteIds, replayAllEdges) {
17108
+ const warnings = [];
17109
+ const added = db.transaction(() => {
17110
+ const wsId = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(plan.workstream)?.id;
17111
+ if (wsId === void 0) throw new DbReplayWorkstreamMissingError(plan.workstream);
17112
+ const taskIds = new Set(selectedTaskIds);
17113
+ const noteTaskIds = /* @__PURE__ */ new Set([...selectedNoteIds, ...taskIds]);
17114
+ let tasks = 0;
17115
+ let notes = 0;
17116
+ let edges = 0;
17117
+ const insertTask = db.prepare(
17118
+ `INSERT OR IGNORE INTO tasks (workstream_id, local_id, title, status, impact, effort_days, created_at, updated_at)
17119
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
17120
+ );
17121
+ for (const task of plan.tasks) {
17122
+ if (!taskIds.has(task.localId)) continue;
17123
+ const result = insertTask.run(
17124
+ wsId,
17125
+ task.localId,
17126
+ task.title,
17127
+ task.status,
17128
+ task.impact,
17129
+ task.effortDays,
17130
+ task.createdAt,
17131
+ task.updatedAt
16753
17132
  );
17133
+ if (result.changes > 0) tasks += 1;
16754
17134
  }
16755
- const journal = db.pragma("journal_mode", { simple: true });
16756
- console.log(
16757
- ` journal_mode : ${journal === "wal" ? pc.green(String(journal)) : pc.yellow(String(journal))}`
17135
+ const existingNoteHashes = new Set(listReplayNotes(db, wsId).map((n) => n.hash));
17136
+ const insertNote = db.prepare(
17137
+ `INSERT INTO task_notes (task_id, author, content, created_at)
17138
+ SELECT id, ?, ?, ? FROM tasks WHERE workstream_id = ? AND local_id = ?`
16758
17139
  );
16759
- const fk = db.pragma("foreign_keys", { simple: true });
16760
- console.log(` foreign_keys : ${fk === 1 ? pc.green("on") : pc.red(`off (${fk})`)}`);
16761
- } catch (err) {
16762
- console.log(
16763
- ` schema : ${pc.red("FAIL")} \u2014 ${err instanceof Error ? err.message : err}`
17140
+ for (const note of plan.notes) {
17141
+ if (!noteTaskIds.has(note.taskLocalId) || existingNoteHashes.has(note.hash)) continue;
17142
+ const result = insertNote.run(
17143
+ note.author,
17144
+ note.content,
17145
+ note.createdAt,
17146
+ wsId,
17147
+ note.taskLocalId
17148
+ );
17149
+ if (result.changes > 0) {
17150
+ notes += 1;
17151
+ existingNoteHashes.add(note.hash);
17152
+ }
17153
+ }
17154
+ const insertEdge = db.prepare(
17155
+ `INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
17156
+ SELECT f.id, t.id, ?
17157
+ FROM tasks f, tasks t
17158
+ WHERE f.workstream_id = ? AND f.local_id = ?
17159
+ AND t.workstream_id = ? AND t.local_id = ?`
16764
17160
  );
16765
- }
16766
- console.log(pc.bold("\nworkstream"));
16767
- let currentWorkstream = null;
16768
- try {
16769
- currentWorkstream = await resolveWorkstream();
17161
+ for (const edge of plan.edges) {
17162
+ if (!replayAllEdges && !taskIds.has(edge.fromLocalId) && !taskIds.has(edge.toLocalId)) {
17163
+ continue;
17164
+ }
17165
+ if (!hasTask(db, wsId, edge.fromLocalId) || !hasTask(db, wsId, edge.toLocalId)) {
17166
+ warnings.push(
17167
+ `skipped edge ${edge.fromLocalId} -> ${edge.toLocalId}: one endpoint is missing locally`
17168
+ );
17169
+ continue;
17170
+ }
17171
+ const result = insertEdge.run(edge.createdAt, wsId, edge.fromLocalId, wsId, edge.toLocalId);
17172
+ if (result.changes > 0) edges += 1;
17173
+ }
17174
+ return { tasks, notes, edges };
17175
+ })();
17176
+ return { added, warnings };
17177
+ }
17178
+ function replayResult(plan, dryRun, applied) {
17179
+ return { ...plan, dryRun, applied, added: { tasks: 0, notes: 0, edges: 0 }, warnings: [] };
17180
+ }
17181
+ function listLocalWorkstreams(db) {
17182
+ return db.prepare("SELECT id, name FROM workstreams ORDER BY name").all();
17183
+ }
17184
+ function listReplayTasks(db, wsId) {
17185
+ return db.prepare(
17186
+ `SELECT local_id, title, status, impact, effort_days, created_at, updated_at
17187
+ FROM tasks
17188
+ WHERE workstream_id = ?
17189
+ ORDER BY local_id`
17190
+ ).all(wsId).map((row2) => ({
17191
+ localId: row2.local_id,
17192
+ title: row2.title,
17193
+ status: row2.status,
17194
+ impact: row2.impact,
17195
+ effortDays: row2.effort_days,
17196
+ createdAt: row2.created_at,
17197
+ updatedAt: row2.updated_at
17198
+ }));
17199
+ }
17200
+ function listReplayNotes(db, wsId) {
17201
+ const rows = db.prepare(
17202
+ `SELECT t.local_id AS taskLocalId, n.author, n.content, n.created_at AS createdAt
17203
+ FROM task_notes n
17204
+ JOIN tasks t ON t.id = n.task_id
17205
+ WHERE t.workstream_id = ?
17206
+ ORDER BY n.created_at, n.id`
17207
+ ).all(wsId);
17208
+ return rows.map((row2) => ({ ...row2, hash: noteHash(row2) }));
17209
+ }
17210
+ function listReplayEdges(db, wsId) {
17211
+ return db.prepare(
17212
+ `SELECT f.local_id AS fromLocalId, t.local_id AS toLocalId, e.created_at AS createdAt
17213
+ FROM task_edges e
17214
+ JOIN tasks f ON f.id = e.from_task_id
17215
+ JOIN tasks t ON t.id = e.to_task_id
17216
+ WHERE f.workstream_id = ? AND t.workstream_id = ?
17217
+ ORDER BY f.local_id, t.local_id`
17218
+ ).all(wsId, wsId);
17219
+ }
17220
+ function noteHash(note) {
17221
+ return createHash2("sha256").update(`${note.taskLocalId}\0${note.content}\0${note.createdAt}`).digest("hex");
17222
+ }
17223
+ function edgeKey(edge) {
17224
+ return `${edge.fromLocalId}\0${edge.toLocalId}`;
17225
+ }
17226
+ function hasTask(db, wsId, localId) {
17227
+ return db.prepare("SELECT 1 FROM tasks WHERE workstream_id = ? AND local_id = ?").get(wsId, localId) !== void 0;
17228
+ }
17229
+ function shellQuote2(s) {
17230
+ return `'${s.replace(/'/g, `'"'"'`)}'`;
17231
+ }
17232
+
17233
+ // src/db-sync.ts
17234
+ var DbExportTargetExistsError = class extends Error {
17235
+ constructor(file) {
17236
+ super(`DB export target already exists: ${file}`);
17237
+ this.file = file;
17238
+ }
17239
+ file;
17240
+ name = "DbExportTargetExistsError";
17241
+ errorNextSteps() {
17242
+ return [
17243
+ { intent: "Choose a different target", command: "mu db export <new-file>" },
17244
+ { intent: "Overwrite this target", command: `mu db export ${shellQuote3(this.file)} --force` }
17245
+ ];
17246
+ }
17247
+ };
17248
+ var DbImportManifestMissingError = class extends Error {
17249
+ constructor(manifestPath) {
17250
+ super(`DB import manifest not found: ${manifestPath}`);
17251
+ this.manifestPath = manifestPath;
17252
+ }
17253
+ manifestPath;
17254
+ name = "DbImportManifestMissingError";
17255
+ errorNextSteps() {
17256
+ return [
17257
+ { intent: "Export the DB with its sidecar", command: "mu db export /tmp/mu.db --force" },
17258
+ { intent: "Copy the sidecar too", command: `scp <host>:${shellQuote3(this.manifestPath)} .` }
17259
+ ];
17260
+ }
17261
+ };
17262
+ var DbImportSchemaTooOldError = class extends Error {
17263
+ constructor(sourceVersion) {
17264
+ super(
17265
+ `source DB schema v${sourceVersion} is older than local mu requires (v${CURRENT_SCHEMA_VERSION})`
17266
+ );
17267
+ this.sourceVersion = sourceVersion;
17268
+ }
17269
+ sourceVersion;
17270
+ name = "DbImportSchemaTooOldError";
17271
+ errorNextSteps() {
17272
+ return [
17273
+ {
17274
+ intent: "Upgrade mu on the source machine",
17275
+ command: "npm run build && mu db export <file> --force"
17276
+ },
17277
+ { intent: "Then retry this import", command: "mu db import <file> --apply" }
17278
+ ];
17279
+ }
17280
+ };
17281
+ var DbImportSchemaTooNewError = class extends Error {
17282
+ constructor(sourceVersion) {
17283
+ super(
17284
+ `source DB schema v${sourceVersion} is newer than this mu supports (v${CURRENT_SCHEMA_VERSION}); upgrade local mu`
17285
+ );
17286
+ this.sourceVersion = sourceVersion;
17287
+ }
17288
+ sourceVersion;
17289
+ name = "DbImportSchemaTooNewError";
17290
+ errorNextSteps() {
17291
+ return [
17292
+ { intent: "Upgrade local mu", command: "git pull && npm install && npm run build" },
17293
+ { intent: "Then retry this import", command: "mu db import <file> --apply" }
17294
+ ];
17295
+ }
17296
+ };
17297
+ var DbImportSourceStaleError = class extends Error {
17298
+ constructor(workstreams) {
17299
+ super(`source DB is stale for local-ahead workstream(s): ${workstreams.join(", ")}`);
17300
+ this.workstreams = workstreams;
17301
+ }
17302
+ workstreams;
17303
+ name = "DbImportSourceStaleError";
17304
+ errorNextSteps() {
17305
+ return [
17306
+ { intent: "Re-export from this machine", command: "mu db export /tmp/mu-fresh.db --force" },
17307
+ { intent: "Dry-run the incoming file first", command: "mu db import <file>" }
17308
+ ];
17309
+ }
17310
+ };
17311
+ var DbImportConflictError = class extends Error {
17312
+ constructor(workstreams) {
17313
+ super(`source and local both advanced for workstream(s): ${workstreams.join(", ")}`);
17314
+ this.workstreams = workstreams;
17315
+ }
17316
+ workstreams;
17317
+ name = "DbImportConflictError";
17318
+ errorNextSteps() {
17319
+ return [
17320
+ { intent: "Preview the conflicting workstreams", command: "mu db import <file> --json" },
17321
+ {
17322
+ intent: "Clobber from source after parking local divergence",
17323
+ command: "mu db import <file> --apply --force-source"
17324
+ }
17325
+ ];
17326
+ }
17327
+ };
17328
+ function exportDb(db, file, opts = {}) {
17329
+ const target = file;
17330
+ const manifestPath = `${target}.manifest.json`;
17331
+ const targetExists = existsSync12(target);
17332
+ if (targetExists && opts.force !== true) throw new DbExportTargetExistsError(target);
17333
+ const manifest = buildExportManifest(db);
17334
+ mkdirSync4(dirname5(target), { recursive: true });
17335
+ try {
17336
+ if (targetExists) unlinkSync4(target);
17337
+ db.exec(`VACUUM INTO ${quoteSqlString2(target)}`);
17338
+ writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
17339
+ `, "utf8");
17340
+ } catch (err) {
17341
+ try {
17342
+ if (existsSync12(target)) unlinkSync4(target);
17343
+ } catch {
17344
+ }
17345
+ throw err;
17346
+ }
17347
+ return { file: target, manifestPath, manifest, overwritten: targetExists };
17348
+ }
17349
+ function importDb(db, file, opts = {}) {
17350
+ const manifest = readImportManifest(file);
17351
+ assertImportSchemaCompatible(manifest.schemaVersion);
17352
+ const sourceDb = openDb({ path: file, readonly: true });
17353
+ try {
17354
+ const summary = buildImportPlan(db, manifest, file, opts.onlyWorkstreams);
17355
+ if (opts.apply !== true) {
17356
+ return {
17357
+ machineId: manifest.machineId,
17358
+ sourceFile: file,
17359
+ dryRun: true,
17360
+ applied: false,
17361
+ summary
17362
+ };
17363
+ }
17364
+ const stale = summary.filter((s) => s.decision === "LOCAL_AHEAD").map((s) => s.workstream);
17365
+ if (stale.length > 0) throw new DbImportSourceStaleError(stale);
17366
+ const conflicts = summary.filter((s) => s.decision === "CONFLICT").map((s) => s.workstream);
17367
+ if (conflicts.length > 0 && opts.forceSource !== true)
17368
+ throw new DbImportConflictError(conflicts);
17369
+ const mutating = summary.some((s) => shouldReplace(s.decision, opts.forceSource === true));
17370
+ const snapshot = mutating ? captureSnapshot(db, `db import ${file}`, null) : void 0;
17371
+ for (const item of summary) {
17372
+ if (!shouldReplace(item.decision, opts.forceSource === true)) continue;
17373
+ if (item.decision === "CONFLICT") {
17374
+ item.parkPath = parkLocalWorkstream(db, item.workstream);
17375
+ }
17376
+ const sourceWs = manifest.workstreams.find((w) => w.name === item.workstream);
17377
+ const sourceSeq = sourceWs?.latestSeq ?? 0;
17378
+ replaceWorkstreamFromSource(db, sourceDb, item.workstream, manifest.machineId, sourceSeq);
17379
+ }
17380
+ return {
17381
+ machineId: manifest.machineId,
17382
+ sourceFile: file,
17383
+ dryRun: false,
17384
+ applied: true,
17385
+ ...snapshot ? { snapshotId: snapshot.id } : {},
17386
+ summary
17387
+ };
17388
+ } finally {
17389
+ sourceDb.close();
17390
+ }
17391
+ }
17392
+ function buildImportPlan(localDb, manifest, sourceFile, onlyWorkstreams) {
17393
+ const sourceByName = new Map(manifest.workstreams.map((w) => [w.name, w]));
17394
+ const localByName = new Map(listLocalWorkstreams2(localDb).map((w) => [w.name, w]));
17395
+ const localMachineId = getMachineIdentity(localDb)?.machine_id ?? "";
17396
+ const only = normaliseOnlyWorkstreams(onlyWorkstreams);
17397
+ const names = Array.from(/* @__PURE__ */ new Set([...sourceByName.keys(), ...localByName.keys()])).filter((name) => only.size === 0 || only.has(name)).sort();
17398
+ return names.map((name) => {
17399
+ const source = sourceByName.get(name);
17400
+ const local = localByName.get(name);
17401
+ const sourceSeq = source?.latestSeq ?? 0;
17402
+ const localSeq = local ? latestSeq(localDb, local.id) : 0;
17403
+ 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 };
17404
+ const decision = classifyWorkstream({
17405
+ hasSource: source !== void 0,
17406
+ hasLocal: local !== void 0,
17407
+ sourceSeq,
17408
+ localSeq,
17409
+ syncedSourceSeq: synced.sourceSeq,
17410
+ syncedLocalSeq: synced.localSeq
17411
+ });
17412
+ return {
17413
+ workstream: name,
17414
+ decision,
17415
+ delta: {
17416
+ sourceFile,
17417
+ sourceSeq,
17418
+ localSeq,
17419
+ lastSynced: synced.sourceSeq,
17420
+ localSynced: synced.localSeq,
17421
+ source: source ? countsFromManifest(source) : null,
17422
+ local: local ? countWorkstream(localDb, local.id) : null
17423
+ },
17424
+ ...decision === "LOCAL_AHEAD" ? { needs: "re-export from this machine" } : {},
17425
+ ...decision === "CONFLICT" ? { needs: "--force-source" } : {}
17426
+ };
17427
+ });
17428
+ }
17429
+ function classifyWorkstream(opts) {
17430
+ if (opts.hasSource && !opts.hasLocal) return "IMPORT";
17431
+ if (!opts.hasSource && opts.hasLocal)
17432
+ return opts.syncedSourceSeq > 0 || opts.syncedLocalSeq > 0 ? "LOCAL_AHEAD" : "LEAVE_ALONE";
17433
+ if (!opts.hasSource && !opts.hasLocal) return "IDENTICAL";
17434
+ const sourceAdvanced = opts.sourceSeq > opts.syncedSourceSeq;
17435
+ const localAdvanced = opts.localSeq > opts.syncedLocalSeq;
17436
+ if (!sourceAdvanced && !localAdvanced) return "IDENTICAL";
17437
+ if (sourceAdvanced && !localAdvanced) return "FAST_FORWARD";
17438
+ if (!sourceAdvanced && localAdvanced) return "LOCAL_AHEAD";
17439
+ return "CONFLICT";
17440
+ }
17441
+ function shouldReplace(decision, forceSource) {
17442
+ return decision === "FAST_FORWARD" || decision === "IMPORT" || decision === "CONFLICT" && forceSource;
17443
+ }
17444
+ function replaceWorkstreamFromSource(localDb, sourceDb, workstream, sourceMachineId, sourceSeq) {
17445
+ localDb.transaction(() => {
17446
+ const existing = localDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream);
17447
+ if (existing) {
17448
+ localDb.prepare("DELETE FROM vcs_workspaces WHERE workstream_id = ?").run(existing.id);
17449
+ localDb.prepare("DELETE FROM agents WHERE workstream_id = ?").run(existing.id);
17450
+ localDb.prepare("DELETE FROM workstreams WHERE id = ?").run(existing.id);
17451
+ }
17452
+ copyWorkstreamRows(sourceDb, localDb, workstream, {
17453
+ includeMachineLocalRows: false,
17454
+ preserveLogSeq: false,
17455
+ includeSync: false
17456
+ });
17457
+ const wsId = localDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream)?.id;
17458
+ if (wsId === void 0) throw new Error(`importDb: failed to import workstream ${workstream}`);
17459
+ writeSyncState(localDb, wsId, sourceMachineId, sourceSeq);
17460
+ })();
17461
+ }
17462
+ function parkLocalWorkstream(db, workstream) {
17463
+ const dir = join8(defaultStateDir(), "divergence");
17464
+ mkdirSync4(dir, { recursive: true });
17465
+ const path2 = join8(
17466
+ dir,
17467
+ `${workstream}-${(/* @__PURE__ */ new Date()).toISOString()}-${randomUUID2().slice(0, 8)}.db`
17468
+ );
17469
+ const parkDb = openDb({ path: path2 });
17470
+ try {
17471
+ const identity = getMachineIdentity(db);
17472
+ if (identity) {
17473
+ parkDb.prepare(
17474
+ `UPDATE machine_identity
17475
+ SET machine_id = ?, hostname = ?, created_at = ?
17476
+ WHERE id = 1`
17477
+ ).run(
17478
+ identity.machine_id,
17479
+ identity.hostname,
17480
+ identity.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
17481
+ );
17482
+ }
17483
+ copyWorkstreamRows(db, parkDb, workstream, {
17484
+ includeMachineLocalRows: true,
17485
+ preserveLogSeq: true,
17486
+ includeSync: true
17487
+ });
17488
+ } catch (err) {
17489
+ try {
17490
+ parkDb.close();
17491
+ } catch {
17492
+ }
17493
+ try {
17494
+ if (existsSync12(path2)) unlinkSync4(path2);
17495
+ } catch {
17496
+ }
17497
+ throw err;
17498
+ }
17499
+ parkDb.close();
17500
+ return path2;
17501
+ }
17502
+ function copyWorkstreamRows(sourceDb, targetDb, workstream, opts) {
17503
+ const sourceWs = sourceDb.prepare("SELECT id, name, created_at FROM workstreams WHERE name = ?").get(workstream);
17504
+ if (!sourceWs) throw new Error(`copyWorkstreamRows: no such workstream ${workstream}`);
17505
+ targetDb.prepare("INSERT INTO workstreams (name, created_at) VALUES (?, ?)").run(sourceWs.name, sourceWs.created_at);
17506
+ const targetWsId = targetDb.prepare("SELECT id FROM workstreams WHERE name = ?").get(workstream).id;
17507
+ if (opts.includeMachineLocalRows) copyAgents(sourceDb, targetDb, sourceWs.id, targetWsId);
17508
+ copyTasks(sourceDb, targetDb, sourceWs.id, targetWsId, opts.includeMachineLocalRows);
17509
+ copyEdges(sourceDb, targetDb, sourceWs.id, targetWsId);
17510
+ copyNotes(sourceDb, targetDb, sourceWs.id, targetWsId);
17511
+ copyLogs(sourceDb, targetDb, sourceWs.id, targetWsId, opts.preserveLogSeq);
17512
+ if (opts.includeMachineLocalRows) copyWorkspaces(sourceDb, targetDb, sourceWs.id, targetWsId);
17513
+ if (opts.includeSync) copySync(sourceDb, targetDb, sourceWs.id, targetWsId);
17514
+ }
17515
+ function copyAgents(sourceDb, targetDb, sourceWsId, targetWsId) {
17516
+ const rows = sourceDb.prepare(
17517
+ `SELECT name, cli, pane_id, status, role, tab, created_at, updated_at
17518
+ FROM agents
17519
+ WHERE workstream_id = ?
17520
+ ORDER BY id`
17521
+ ).all(sourceWsId);
17522
+ const insert = targetDb.prepare(
17523
+ `INSERT INTO agents (workstream_id, name, cli, pane_id, status, role, tab, created_at, updated_at)
17524
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
17525
+ );
17526
+ for (const row2 of rows) {
17527
+ insert.run(
17528
+ targetWsId,
17529
+ row2.name,
17530
+ row2.cli,
17531
+ row2.pane_id,
17532
+ row2.status,
17533
+ row2.role,
17534
+ row2.tab,
17535
+ row2.created_at,
17536
+ row2.updated_at
17537
+ );
17538
+ }
17539
+ }
17540
+ function copyTasks(sourceDb, targetDb, sourceWsId, targetWsId, includeOwners) {
17541
+ const rows = sourceDb.prepare(
17542
+ `SELECT t.local_id, t.title, t.status, t.impact, t.effort_days, a.name AS owner_name,
17543
+ t.created_at, t.updated_at
17544
+ FROM tasks t
17545
+ LEFT JOIN agents a ON a.id = t.owner_id
17546
+ WHERE t.workstream_id = ?
17547
+ ORDER BY t.id`
17548
+ ).all(sourceWsId);
17549
+ const ownerLookup = targetDb.prepare(
17550
+ "SELECT id FROM agents WHERE workstream_id = ? AND name = ?"
17551
+ );
17552
+ const insert = targetDb.prepare(
17553
+ `INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days, owner_id, created_at, updated_at)
17554
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
17555
+ );
17556
+ for (const row2 of rows) {
17557
+ const ownerId = includeOwners && row2.owner_name !== null ? ownerLookup.get(targetWsId, row2.owner_name)?.id ?? null : null;
17558
+ insert.run(
17559
+ targetWsId,
17560
+ row2.local_id,
17561
+ row2.title,
17562
+ row2.status,
17563
+ row2.impact,
17564
+ row2.effort_days,
17565
+ ownerId,
17566
+ row2.created_at,
17567
+ row2.updated_at
17568
+ );
17569
+ }
17570
+ }
17571
+ function copyEdges(sourceDb, targetDb, sourceWsId, targetWsId) {
17572
+ const rows = sourceDb.prepare(
17573
+ `SELECT f.local_id AS from_local_id, t.local_id AS to_local_id, e.created_at
17574
+ FROM task_edges e
17575
+ JOIN tasks f ON f.id = e.from_task_id
17576
+ JOIN tasks t ON t.id = e.to_task_id
17577
+ WHERE f.workstream_id = ? AND t.workstream_id = ?
17578
+ ORDER BY e.created_at, f.local_id, t.local_id`
17579
+ ).all(sourceWsId, sourceWsId);
17580
+ const insert = targetDb.prepare(
17581
+ `INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, created_at)
17582
+ SELECT f.id, t.id, ?
17583
+ FROM tasks f, tasks t
17584
+ WHERE f.workstream_id = ? AND f.local_id = ?
17585
+ AND t.workstream_id = ? AND t.local_id = ?`
17586
+ );
17587
+ for (const row2 of rows) {
17588
+ insert.run(row2.created_at, targetWsId, row2.from_local_id, targetWsId, row2.to_local_id);
17589
+ }
17590
+ }
17591
+ function copyNotes(sourceDb, targetDb, sourceWsId, targetWsId) {
17592
+ const rows = sourceDb.prepare(
17593
+ `SELECT t.local_id AS task_local_id, n.author, n.content, n.created_at
17594
+ FROM task_notes n
17595
+ JOIN tasks t ON t.id = n.task_id
17596
+ WHERE t.workstream_id = ?
17597
+ ORDER BY n.id`
17598
+ ).all(sourceWsId);
17599
+ const insert = targetDb.prepare(
17600
+ `INSERT INTO task_notes (task_id, author, content, created_at)
17601
+ SELECT id, ?, ?, ? FROM tasks WHERE workstream_id = ? AND local_id = ?`
17602
+ );
17603
+ for (const row2 of rows) {
17604
+ insert.run(row2.author, row2.content, row2.created_at, targetWsId, row2.task_local_id);
17605
+ }
17606
+ }
17607
+ function copyLogs(sourceDb, targetDb, sourceWsId, targetWsId, preserveSeq) {
17608
+ const rows = sourceDb.prepare(
17609
+ `SELECT seq, source, kind, payload, created_at
17610
+ FROM agent_logs
17611
+ WHERE workstream_id = ?
17612
+ ORDER BY seq`
17613
+ ).all(sourceWsId);
17614
+ const insertPreserve = targetDb.prepare(
17615
+ "INSERT INTO agent_logs (seq, workstream_id, source, kind, payload, created_at) VALUES (?, ?, ?, ?, ?, ?)"
17616
+ );
17617
+ const insertRenumber = targetDb.prepare(
17618
+ "INSERT INTO agent_logs (workstream_id, source, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)"
17619
+ );
17620
+ for (const row2 of rows) {
17621
+ if (preserveSeq) {
17622
+ insertPreserve.run(row2.seq, targetWsId, row2.source, row2.kind, row2.payload, row2.created_at);
17623
+ } else {
17624
+ insertRenumber.run(targetWsId, row2.source, row2.kind, row2.payload, row2.created_at);
17625
+ }
17626
+ }
17627
+ }
17628
+ function copyWorkspaces(sourceDb, targetDb, sourceWsId, targetWsId) {
17629
+ const rows = sourceDb.prepare(
17630
+ `SELECT a.name AS agent_name, v.backend, v.path, v.parent_ref, v.created_at
17631
+ FROM vcs_workspaces v
17632
+ JOIN agents a ON a.id = v.agent_id
17633
+ WHERE v.workstream_id = ?
17634
+ ORDER BY v.id`
17635
+ ).all(sourceWsId);
17636
+ const agentLookup = targetDb.prepare(
17637
+ "SELECT id FROM agents WHERE workstream_id = ? AND name = ?"
17638
+ );
17639
+ const insert = targetDb.prepare(
17640
+ `INSERT INTO vcs_workspaces (agent_id, workstream_id, backend, path, parent_ref, created_at)
17641
+ VALUES (?, ?, ?, ?, ?, ?)`
17642
+ );
17643
+ for (const row2 of rows) {
17644
+ const agentId = agentLookup.get(targetWsId, row2.agent_name)?.id;
17645
+ if (agentId === void 0) continue;
17646
+ insert.run(agentId, targetWsId, row2.backend, row2.path, row2.parent_ref, row2.created_at);
17647
+ }
17648
+ }
17649
+ function copySync(sourceDb, targetDb, sourceWsId, targetWsId) {
17650
+ const row2 = sourceDb.prepare("SELECT last_known_peer_seqs FROM workstream_sync WHERE workstream_id = ?").get(sourceWsId);
17651
+ if (!row2) return;
17652
+ targetDb.prepare("INSERT INTO workstream_sync (workstream_id, last_known_peer_seqs) VALUES (?, ?)").run(targetWsId, row2.last_known_peer_seqs);
17653
+ }
17654
+ function writeSyncState(db, workstreamId, sourceMachineId, sourceSeq) {
17655
+ const localSeq = latestSeq(db, workstreamId);
17656
+ const peers = {
17657
+ [sourceMachineId]: sourceSeq,
17658
+ [localSeqKey(sourceMachineId)]: localSeq
17659
+ };
17660
+ db.prepare(
17661
+ `INSERT OR REPLACE INTO workstream_sync (workstream_id, last_known_peer_seqs)
17662
+ VALUES (?, ?)`
17663
+ ).run(workstreamId, JSON.stringify(peers));
17664
+ }
17665
+ function lastKnownPeerSync(db, workstreamId, machineId) {
17666
+ const row2 = db.prepare("SELECT last_known_peer_seqs FROM workstream_sync WHERE workstream_id = ?").get(workstreamId);
17667
+ if (!row2) return { sourceSeq: 0, localSeq: 0 };
17668
+ const parsed = parsePeerSeqs(row2.last_known_peer_seqs);
17669
+ const sourceSeq = parsed[machineId] ?? 0;
17670
+ return { sourceSeq, localSeq: parsed[localSeqKey(machineId)] ?? sourceSeq };
17671
+ }
17672
+ function localSeqKey(machineId) {
17673
+ return `${machineId}:local`;
17674
+ }
17675
+ function parsePeerSeqs(raw) {
17676
+ try {
17677
+ const parsed = JSON.parse(raw);
17678
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
17679
+ const result = {};
17680
+ for (const [key, value] of Object.entries(parsed)) {
17681
+ if (typeof value === "number" && Number.isFinite(value)) result[key] = value;
17682
+ }
17683
+ return result;
17684
+ } catch {
17685
+ return {};
17686
+ }
17687
+ }
17688
+ function readImportManifest(file) {
17689
+ const manifestPath = `${file}.manifest.json`;
17690
+ if (!existsSync12(manifestPath)) throw new DbImportManifestMissingError(manifestPath);
17691
+ return JSON.parse(readFileSync2(manifestPath, "utf8"));
17692
+ }
17693
+ function assertImportSchemaCompatible(sourceVersion) {
17694
+ if (sourceVersion < CURRENT_SCHEMA_VERSION) throw new DbImportSchemaTooOldError(sourceVersion);
17695
+ if (sourceVersion > CURRENT_SCHEMA_VERSION) throw new DbImportSchemaTooNewError(sourceVersion);
17696
+ }
17697
+ function buildExportManifest(db) {
17698
+ const identity = getMachineIdentity(db);
17699
+ const schemaRow = db.prepare("SELECT version FROM schema_version WHERE id = 1").get();
17700
+ const workstreams = listLocalWorkstreams2(db);
17701
+ return {
17702
+ muVersion: readPackageVersion(),
17703
+ schemaVersion: schemaRow?.version ?? CURRENT_SCHEMA_VERSION,
17704
+ machineId: identity?.machine_id ?? "",
17705
+ hostname: identity?.hostname ?? hostname2(),
17706
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
17707
+ workstreams: workstreams.map((ws) => ({
17708
+ name: ws.name,
17709
+ tasks: count(db, "SELECT COUNT(*) AS n FROM tasks WHERE workstream_id = ?", ws.id),
17710
+ edges: count(
17711
+ db,
17712
+ `SELECT COUNT(*) AS n
17713
+ FROM task_edges e
17714
+ JOIN tasks f ON f.id = e.from_task_id
17715
+ JOIN tasks t ON t.id = e.to_task_id
17716
+ WHERE f.workstream_id = ? AND t.workstream_id = ?`,
17717
+ ws.id,
17718
+ ws.id
17719
+ ),
17720
+ notes: count(
17721
+ db,
17722
+ `SELECT COUNT(*) AS n
17723
+ FROM task_notes n
17724
+ JOIN tasks t ON t.id = n.task_id
17725
+ WHERE t.workstream_id = ?`,
17726
+ ws.id
17727
+ ),
17728
+ latestSeq: latestSeq(db, ws.id)
17729
+ }))
17730
+ };
17731
+ }
17732
+ function listLocalWorkstreams2(db) {
17733
+ return db.prepare("SELECT id, name FROM workstreams ORDER BY name").all();
17734
+ }
17735
+ function getMachineIdentity(db) {
17736
+ return db.prepare("SELECT machine_id, hostname, created_at FROM machine_identity WHERE id = 1").get();
17737
+ }
17738
+ function countWorkstream(db, wsId) {
17739
+ return {
17740
+ tasks: count(db, "SELECT COUNT(*) AS n FROM tasks WHERE workstream_id = ?", wsId),
17741
+ edges: count(
17742
+ db,
17743
+ `SELECT COUNT(*) AS n
17744
+ FROM task_edges e
17745
+ JOIN tasks f ON f.id = e.from_task_id
17746
+ JOIN tasks t ON t.id = e.to_task_id
17747
+ WHERE f.workstream_id = ? AND t.workstream_id = ?`,
17748
+ wsId,
17749
+ wsId
17750
+ ),
17751
+ notes: count(
17752
+ db,
17753
+ `SELECT COUNT(*) AS n
17754
+ FROM task_notes n
17755
+ JOIN tasks t ON t.id = n.task_id
17756
+ WHERE t.workstream_id = ?`,
17757
+ wsId
17758
+ )
17759
+ };
17760
+ }
17761
+ function countsFromManifest(ws) {
17762
+ return { tasks: ws.tasks, edges: ws.edges, notes: ws.notes };
17763
+ }
17764
+ function normaliseOnlyWorkstreams(input) {
17765
+ if (!input || input.length === 0) return /* @__PURE__ */ new Set();
17766
+ return new Set(
17767
+ input.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0)
17768
+ );
17769
+ }
17770
+ function count(db, sql, ...params) {
17771
+ const row2 = db.prepare(sql).get(...params);
17772
+ return row2?.n ?? 0;
17773
+ }
17774
+ function quoteSqlString2(s) {
17775
+ return `'${s.replace(/'/g, "''")}'`;
17776
+ }
17777
+ function readPackageVersion() {
17778
+ try {
17779
+ const here = dirname5(fileURLToPath2(import.meta.url));
17780
+ const raw = readFileSync2(join8(here, "..", "package.json"), "utf8");
17781
+ const parsed = JSON.parse(raw);
17782
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
17783
+ } catch {
17784
+ return "unknown";
17785
+ }
17786
+ }
17787
+ function shellQuote3(s) {
17788
+ return `'${s.replace(/'/g, `'"'"'`)}'`;
17789
+ }
17790
+
17791
+ // src/cli/db.ts
17792
+ init_output();
17793
+ function exportNextSteps(file) {
17794
+ return [
17795
+ { intent: "Ship the DB copy", command: `scp ${file} <other-machine>:/tmp/mu.db` },
17796
+ { intent: "Ship the manifest too", command: `scp ${file}.manifest.json <other-machine>:/tmp/` },
17797
+ { intent: "Import on the other side", command: "mu db import /tmp/mu.db" }
17798
+ ];
17799
+ }
17800
+ function importNextSteps(result) {
17801
+ if (result.dryRun) {
17802
+ const hasConflict = result.summary.some((s) => s.decision === "CONFLICT");
17803
+ const hasStale = result.summary.some((s) => s.decision === "LOCAL_AHEAD");
17804
+ return [
17805
+ ...hasStale ? [{ intent: "Source is stale", command: "re-export from this machine before importing" }] : [],
17806
+ ...hasConflict ? [
17807
+ {
17808
+ intent: "Clobber source after parking local",
17809
+ command: `mu db import ${result.sourceFile} --apply --force-source`
17810
+ }
17811
+ ] : [],
17812
+ ...!hasConflict && !hasStale ? [{ intent: "Apply this plan", command: `mu db import ${result.sourceFile} --apply` }] : []
17813
+ ];
17814
+ }
17815
+ return [
17816
+ { intent: "Undo if needed", command: "mu undo --yes" },
17817
+ { intent: "Inspect workstreams", command: "mu state --json" }
17818
+ ];
17819
+ }
17820
+ function replayNextSteps(result) {
17821
+ if (result.dryRun) {
17822
+ const firstTask = result.tasks[0]?.localId;
17823
+ return [
17824
+ ...firstTask ? [
17825
+ {
17826
+ intent: "Replay one parked task",
17827
+ command: `mu db replay ${result.sourceFile} --task ${firstTask} --apply`
17828
+ }
17829
+ ] : [],
17830
+ {
17831
+ intent: "Replay all parked rows",
17832
+ command: `mu db replay ${result.sourceFile} --all --apply`
17833
+ }
17834
+ ];
17835
+ }
17836
+ return [
17837
+ { intent: "Undo if needed", command: "mu undo --yes" },
17838
+ { intent: "Inspect the workstream", command: `mu task list -w ${result.workstream}` }
17839
+ ];
17840
+ }
17841
+ async function cmdDbExport(db, file, opts = {}) {
17842
+ const result = exportDb(db, file, opts);
17843
+ const nextSteps = exportNextSteps(result.file);
17844
+ if (opts.json) {
17845
+ emitJson({ ...result, nextSteps });
17846
+ return;
17847
+ }
17848
+ console.log(
17849
+ `Exported whole mu DB \u2192 ${pc.bold(result.file)} ${pc.dim(
17850
+ `(schema=v${result.manifest.schemaVersion}, workstreams=${result.manifest.workstreams.length}, manifest=${result.manifestPath})`
17851
+ )}`
17852
+ );
17853
+ if (result.overwritten) console.log(pc.dim("Overwrote existing target due to --force."));
17854
+ printNextSteps(nextSteps);
17855
+ }
17856
+ async function cmdDbImport(db, file, opts = {}) {
17857
+ const result = importDb(db, file, {
17858
+ apply: opts.apply,
17859
+ forceSource: opts.forceSource,
17860
+ onlyWorkstreams: opts.onlyWs
17861
+ });
17862
+ const nextSteps = importNextSteps(result);
17863
+ if (opts.json) {
17864
+ emitJson({ ...result, nextSteps });
17865
+ return;
17866
+ }
17867
+ console.log(
17868
+ `${result.dryRun ? "Dry-run" : "Applied"} DB import from ${pc.bold(result.sourceFile)} ${pc.dim(
17869
+ `(source machine=${result.machineId})`
17870
+ )}`
17871
+ );
17872
+ if (result.snapshotId !== void 0) {
17873
+ console.log(pc.dim(`Safety snapshot #${result.snapshotId} captured before import.`));
17874
+ }
17875
+ console.log(renderImportSummary(result.summary));
17876
+ printNextSteps(nextSteps);
17877
+ }
17878
+ function renderImportSummary(summary) {
17879
+ const table = muTable({
17880
+ head: ["workstream", "decision", "source_seq", "local_seq", "last_synced", "needs"]
17881
+ });
17882
+ for (const item of summary) {
17883
+ const delta = item.delta;
17884
+ table.push([
17885
+ item.workstream,
17886
+ item.decision,
17887
+ String(delta.sourceSeq ?? ""),
17888
+ String(delta.localSeq ?? ""),
17889
+ String(delta.lastSynced ?? ""),
17890
+ item.parkPath ?? item.needs ?? ""
17891
+ ]);
17892
+ }
17893
+ return table.toString();
17894
+ }
17895
+ async function cmdDbReplay(db, file, opts = {}) {
17896
+ const result = replayDb(db, file, {
17897
+ apply: opts.apply,
17898
+ tasks: opts.task,
17899
+ notes: opts.note,
17900
+ all: opts.all
17901
+ });
17902
+ const nextSteps = replayNextSteps(result);
17903
+ if (opts.json) {
17904
+ emitJson({ ...result, nextSteps });
17905
+ return;
17906
+ }
17907
+ console.log(
17908
+ `${result.dryRun ? "Dry-run" : "Applied"} DB replay from ${pc.bold(result.sourceFile)} ${pc.dim(
17909
+ `(workstream=${result.workstream})`
17910
+ )}`
17911
+ );
17912
+ if (result.snapshotId !== void 0) {
17913
+ console.log(pc.dim(`Safety snapshot #${result.snapshotId} captured before replay.`));
17914
+ }
17915
+ console.log(renderReplaySummary(result));
17916
+ for (const warning of result.warnings) console.warn(pc.yellow(`warning: ${warning}`));
17917
+ printNextSteps(nextSteps);
17918
+ }
17919
+ function renderReplaySummary(result) {
17920
+ const table = muTable({ head: ["kind", "count", "details"] });
17921
+ table.push([
17922
+ "tasks",
17923
+ String(result.tasks.length),
17924
+ result.tasks.map((t) => `${t.localId} (${t.status})`).join(", ")
17925
+ ]);
17926
+ table.push([
17927
+ "notes",
17928
+ String(result.notes.length),
17929
+ result.notes.map((n) => `${n.taskLocalId}@${n.createdAt}`).join(", ")
17930
+ ]);
17931
+ table.push([
17932
+ "edges",
17933
+ String(result.edges.length),
17934
+ result.edges.map((e) => `${e.fromLocalId}->${e.toLocalId}`).join(", ")
17935
+ ]);
17936
+ table.push([
17937
+ "conflicts",
17938
+ String(result.conflicts.length),
17939
+ result.conflicts.map(
17940
+ (c) => `${c.localId}: local=${c.local.status}/${c.local.title}; sidecar=${c.sidecar.status}/${c.sidecar.title}`
17941
+ ).join(", ")
17942
+ ]);
17943
+ if (!result.dryRun) {
17944
+ table.push([
17945
+ "added",
17946
+ String(result.added.tasks + result.added.notes + result.added.edges),
17947
+ `tasks=${result.added.tasks}, notes=${result.added.notes}, edges=${result.added.edges}`
17948
+ ]);
17949
+ }
17950
+ return table.toString();
17951
+ }
17952
+ function collectOnlyWs(value, previous = []) {
17953
+ return collectRepeatedCsv(value, previous);
17954
+ }
17955
+ function collectRepeatedCsv(value, previous = []) {
17956
+ return [
17957
+ ...previous,
17958
+ ...value.split(",").map((v) => v.trim()).filter((v) => v.length > 0)
17959
+ ];
17960
+ }
17961
+ function wireDbCommands(program) {
17962
+ const db = program.command("db").description("Whole-machine DB sync commands");
17963
+ db.command("export <file>").description(
17964
+ "Export the entire mu SQLite DB to <file> via VACUUM INTO and write <file>.manifest.json. Whole-machine by design; no --workstream flag."
17965
+ ).option("--force", "overwrite an existing target file").option(...JSON_OPT).action(function(file) {
17966
+ const opts = this.opts();
17967
+ return handle((dbHandle) => cmdDbExport(dbHandle, file, opts), this)();
17968
+ });
17969
+ db.command("import <file>").description(
17970
+ "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."
17971
+ ).option("--apply", "actually apply the import plan (default is dry-run)").option(
17972
+ "--only-ws <names>",
17973
+ "restrict to workstream names; repeat or comma-separate",
17974
+ collectOnlyWs,
17975
+ []
17976
+ ).option("--force-source", "on conflict, park local divergence then replace from source").option(...JSON_OPT).action(function(file) {
17977
+ const opts = this.opts();
17978
+ return handle((dbHandle) => cmdDbImport(dbHandle, file, opts), this)();
17979
+ });
17980
+ db.command("replay <sidecar-file>").description(
17981
+ "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."
17982
+ ).option("--apply", "actually apply the replay selection (default is dry-run)").option(
17983
+ "--task <id>",
17984
+ "replay a missing task plus its notes and eligible edges; repeat or comma-separate",
17985
+ collectRepeatedCsv,
17986
+ []
17987
+ ).option(
17988
+ "--note <task-id>",
17989
+ "replay missing notes for a task; repeat or comma-separate",
17990
+ collectRepeatedCsv,
17991
+ []
17992
+ ).option("--all", "replay every missing local-only item from the sidecar").option(...JSON_OPT).action(function(file) {
17993
+ const opts = this.opts();
17994
+ return handle((dbHandle) => cmdDbReplay(dbHandle, file, opts), this)();
17995
+ });
17996
+ }
17997
+
17998
+ // src/cli/doctor.ts
17999
+ init_agents();
18000
+ init_db();
18001
+ init_output();
18002
+ init_tmux();
18003
+ init_workstream();
18004
+ async function cmdDoctor(db, opts = {}) {
18005
+ if (opts.json) {
18006
+ return cmdDoctorJson(db);
18007
+ }
18008
+ console.log(pc.bold("mu doctor"));
18009
+ console.log(pc.bold("\nenvironment"));
18010
+ try {
18011
+ const version = (await tmux(["-V"])).trim();
18012
+ console.log(` tmux : ${pc.green("ok")} (${version})`);
18013
+ } catch {
18014
+ console.log(` tmux : ${pc.red("NOT FOUND")} \u2014 install tmux \u2265 3.0`);
18015
+ }
18016
+ console.log(` $TMUX : ${process.env.TMUX ? pc.green("set") : pc.yellow("not set")}`);
18017
+ console.log(
18018
+ ` $TMUX_PANE : ${process.env.TMUX_PANE ? pc.green(process.env.TMUX_PANE) : pc.dim("not set")}`
18019
+ );
18020
+ console.log(
18021
+ ` $MU_SESSION : ${process.env.MU_SESSION ? pc.green(process.env.MU_SESSION) : pc.dim("not set")}`
18022
+ );
18023
+ console.log(pc.bold("\ndb"));
18024
+ console.log(` path : ${pc.dim(defaultDbPath())}`);
18025
+ try {
18026
+ const tables = db.prepare(
18027
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
18028
+ ).all().map((r) => r.name);
18029
+ const missing = EXPECTED_TABLES.filter((t) => !tables.includes(t));
18030
+ if (missing.length === 0) {
18031
+ console.log(` schema : ${pc.green("ok")} (${EXPECTED_TABLES.length} tables)`);
18032
+ } else {
18033
+ console.log(` schema : ${pc.red("missing")} \u2014 ${missing.join(", ")}`);
18034
+ }
18035
+ try {
18036
+ const row2 = db.prepare("SELECT version FROM schema_version WHERE id = 1").get();
18037
+ const v = row2?.version;
18038
+ if (v === void 0) {
18039
+ console.log(
18040
+ ` schema_version : ${pc.red("missing row")} (expected ${CURRENT_SCHEMA_VERSION})`
18041
+ );
18042
+ } else if (v === CURRENT_SCHEMA_VERSION) {
18043
+ console.log(` schema_version : ${pc.green(String(v))}`);
18044
+ } else if (v < CURRENT_SCHEMA_VERSION) {
18045
+ console.log(
18046
+ ` schema_version : ${pc.yellow(String(v))} (code expects ${CURRENT_SCHEMA_VERSION}; openDb should have migrated)`
18047
+ );
18048
+ } else {
18049
+ console.log(
18050
+ ` schema_version : ${pc.red(String(v))} (code expects ${CURRENT_SCHEMA_VERSION}; possible downgrade or future-version DB)`
18051
+ );
18052
+ }
18053
+ } catch {
18054
+ console.log(
18055
+ ` schema_version : ${pc.red("unreadable")} (schema_version table missing or wrong shape)`
18056
+ );
18057
+ }
18058
+ const journal = db.pragma("journal_mode", { simple: true });
18059
+ console.log(
18060
+ ` journal_mode : ${journal === "wal" ? pc.green(String(journal)) : pc.yellow(String(journal))}`
18061
+ );
18062
+ const fk = db.pragma("foreign_keys", { simple: true });
18063
+ console.log(` foreign_keys : ${fk === 1 ? pc.green("on") : pc.red(`off (${fk})`)}`);
18064
+ } catch (err) {
18065
+ console.log(
18066
+ ` schema : ${pc.red("FAIL")} \u2014 ${err instanceof Error ? err.message : err}`
18067
+ );
18068
+ }
18069
+ console.log(pc.bold("\nworkstream"));
18070
+ let currentWorkstream = null;
18071
+ try {
18072
+ currentWorkstream = await resolveWorkstream();
16770
18073
  console.log(` current : ${pc.green(currentWorkstream)}`);
16771
18074
  } catch {
16772
18075
  console.log(
@@ -16933,553 +18236,8 @@ function wireDoctorCommand(program) {
16933
18236
  // src/cli/handle.ts
16934
18237
  init_agents();
16935
18238
  init_archives();
16936
- init_db();
16937
18239
  import { CommanderError } from "commander";
16938
-
16939
- // src/importing.ts
16940
18240
  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
18241
  init_output();
17484
18242
  init_snapshots();
17485
18243
  init_tasks();
@@ -17534,13 +18292,34 @@ var NameAmbiguousError = class extends Error {
17534
18292
  }
17535
18293
  };
17536
18294
  function classifyError(err) {
17537
- if (err instanceof UsageError || err instanceof WorkstreamNameInvalidError || err instanceof ArchiveLabelInvalidError || err instanceof ImportBucketInvalidError || err instanceof ImportFrontmatterParseError || err instanceof ImportEdgeRefMissingError || err instanceof PruneOptionsInvalidError) {
18295
+ if (err instanceof UsageError || err instanceof WorkstreamNameInvalidError || err instanceof ArchiveLabelInvalidError || err instanceof PruneOptionsInvalidError) {
17538
18296
  return { label: "error", exitCode: 2 };
17539
18297
  }
17540
18298
  if (err instanceof AgentNotFoundError || err instanceof TaskNotFoundError || err instanceof WorkstreamNotFoundError || err instanceof WorkspaceNotFoundError || err instanceof SnapshotNotFoundError || err instanceof ArchiveNotFoundError) {
17541
18299
  return { label: "not found", exitCode: 3 };
17542
18300
  }
17543
- 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 ImportSourceNotInBucketError || err instanceof WorkstreamAlreadyExistsError) {
18301
+ if (err instanceof DbImportManifestMissingError) {
18302
+ return { label: "db import manifest missing", exitCode: 8 };
18303
+ }
18304
+ if (err instanceof DbImportSchemaTooOldError) {
18305
+ return { label: "db import schema too old", exitCode: 9 };
18306
+ }
18307
+ if (err instanceof DbImportSchemaTooNewError) {
18308
+ return { label: "db import schema too new", exitCode: 10 };
18309
+ }
18310
+ if (err instanceof DbImportSourceStaleError) {
18311
+ return { label: "db import source stale", exitCode: 11 };
18312
+ }
18313
+ if (err instanceof DbImportConflictError) {
18314
+ return { label: "db import conflict", exitCode: 12 };
18315
+ }
18316
+ if (err instanceof DbReplayWorkstreamMissingError) {
18317
+ return { label: "db replay workstream missing", exitCode: 13 };
18318
+ }
18319
+ if (err instanceof DbReplayLocalIdConflictError) {
18320
+ return { label: "db replay local-id conflict", exitCode: 14 };
18321
+ }
18322
+ 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
18323
  return { label: "conflict", exitCode: 4 };
17545
18324
  }
17546
18325
  if (err instanceof AgentSpawnCliNotFoundError) {
@@ -18394,7 +19173,7 @@ async function cmdSql(db, query, opts = {}) {
18394
19173
  }
18395
19174
  }
18396
19175
  function countTopLevelStatements(sql) {
18397
- let count = 0;
19176
+ let count2 = 0;
18398
19177
  let inSingle = false;
18399
19178
  let inDouble = false;
18400
19179
  let inLineComment = false;
@@ -18450,15 +19229,15 @@ function countTopLevelStatements(sql) {
18450
19229
  }
18451
19230
  if (c === ";") {
18452
19231
  if (sawNonWs) {
18453
- count++;
19232
+ count2++;
18454
19233
  sawNonWs = false;
18455
19234
  }
18456
19235
  continue;
18457
19236
  }
18458
19237
  if (c !== void 0 && /\S/.test(c)) sawNonWs = true;
18459
19238
  }
18460
- if (sawNonWs) count++;
18461
- return count;
19239
+ if (sawNonWs) count2++;
19240
+ return count2;
18462
19241
  }
18463
19242
  function formatCell(v) {
18464
19243
  if (v === null || v === void 0) return pc.dim("null");
@@ -19202,75 +19981,6 @@ async function cmdWorkstreamExport(db, opts) {
19202
19981
  );
19203
19982
  printNextSteps(nextSteps);
19204
19983
  }
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
19984
  function autoExportDir(workstream) {
19275
19985
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19276
19986
  return join9(defaultStateDir(), "exports", `${workstream}-${ts}`);
@@ -19595,18 +20305,6 @@ function wireWorkstreamCommands(program) {
19595
20305
  const opts = this.opts();
19596
20306
  return handle((db) => cmdDestroy(db, opts), this)();
19597
20307
  });
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
20308
  workstream.command("export").description(
19611
20309
  "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
20310
  ).option(...WORKSTREAM_OPT).option("--out <dir>", "output directory (the bucket; defaults to ./<workstream>/)").option(...JSON_OPT).action(function() {
@@ -19857,9 +20555,9 @@ async function cmdBareTui(db, program, requestedWorkstreams) {
19857
20555
  throw err;
19858
20556
  }
19859
20557
  }
19860
- function readPackageVersion() {
20558
+ function readPackageVersion2() {
19861
20559
  try {
19862
- const here = dirname7(fileURLToPath2(import.meta.url));
20560
+ const here = dirname7(fileURLToPath3(import.meta.url));
19863
20561
  const pkgPath = join10(here, "..", "package.json");
19864
20562
  const raw = readFileSync3(pkgPath, "utf8");
19865
20563
  const parsed = JSON.parse(raw);
@@ -19872,7 +20570,7 @@ function buildProgram() {
19872
20570
  const program = new Command();
19873
20571
  program.name("mu").description(
19874
20572
  "Persistent crew of AI agents in tmux panes coordinated through a built-in task DAG."
19875
- ).version(readPackageVersion()).helpOption("-h, --help").configureHelp({ sortSubcommands: true }).enablePositionalOptions().option(
20573
+ ).version(readPackageVersion2()).helpOption("-h, --help").configureHelp({ sortSubcommands: true }).enablePositionalOptions().option(
19876
20574
  "-w, --workstream <names...>",
19877
20575
  "workstream(s) to render (repeat or comma-separate; or both; defaults to $MU_SESSION or current tmux session)"
19878
20576
  ).option(...JSON_OPT).action(function() {
@@ -19894,13 +20592,15 @@ function buildProgram() {
19894
20592
  wireStateCommands(program);
19895
20593
  wireSqlCommand(program);
19896
20594
  wireSnapshotCommands(program);
20595
+ wireDbCommands(program);
19897
20596
  wireDoctorCommand(program);
19898
20597
  applyAlphabeticalHelpSort(program);
19899
20598
  applyExitOverride(program);
19900
20599
  return program;
19901
20600
  }
19902
20601
  function applyAlphabeticalHelpSort(cmd) {
19903
- cmd.configureHelp({ ...cmd.configureHelp(), sortSubcommands: true });
20602
+ const keepSemanticOrder = cmd.name() === "archive";
20603
+ cmd.configureHelp({ ...cmd.configureHelp(), sortSubcommands: !keepSemanticOrder });
19904
20604
  for (const sub of cmd.commands) {
19905
20605
  applyAlphabeticalHelpSort(sub);
19906
20606
  }