@mu-crew/mu 3.13.3 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/AGENTS.md +73 -35
  2. package/README.md +71 -57
  3. package/dist/chunk-4ULY3ZKO.js +4482 -0
  4. package/dist/chunk-4ULY3ZKO.js.map +1 -0
  5. package/dist/chunk-7M634JMD.js +375 -0
  6. package/dist/chunk-7M634JMD.js.map +1 -0
  7. package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
  8. package/dist/chunk-PZ5GOCWH.js.map +1 -0
  9. package/dist/chunk-SS7NQSSZ.js +3660 -0
  10. package/dist/chunk-SS7NQSSZ.js.map +1 -0
  11. package/dist/chunk-UCZPJX7K.js +14253 -0
  12. package/dist/chunk-UCZPJX7K.js.map +1 -0
  13. package/dist/{cli-CKOMHBXV.js → cli-TV3N47AZ.js} +7317 -4593
  14. package/dist/cli-TV3N47AZ.js.map +1 -0
  15. package/dist/cli.js +2 -3
  16. package/dist/cli.js.map +1 -1
  17. package/dist/errors-GRONKWFS.js +65 -0
  18. package/dist/extension/mu-pi.js +1632 -837
  19. package/dist/extension/mu-pi.js.map +1 -1
  20. package/dist/index.d.ts +4067 -2561
  21. package/dist/index.js +614 -297
  22. package/dist/index.js.map +1 -1
  23. package/dist/tui-FUBQ326D.js +1848 -0
  24. package/dist/tui-FUBQ326D.js.map +1 -0
  25. package/docs/ARCHITECTURE.md +118 -66
  26. package/docs/VOCABULARY.md +113 -49
  27. package/docs/guide/README.md +3 -3
  28. package/docs/guide/backends.md +8 -4
  29. package/docs/guide/cleanup.md +69 -33
  30. package/docs/guide/delegate.md +149 -66
  31. package/docs/guide/dispatch.md +148 -53
  32. package/docs/guide/getting-started.md +37 -44
  33. package/docs/guide/other-agent-clis.md +102 -0
  34. package/docs/guide/recovery.md +31 -50
  35. package/docs/guide/remote.md +122 -45
  36. package/docs/guide/sql.md +104 -24
  37. package/docs/guide/stop-a-worker.md +30 -37
  38. package/docs/guide/sync.md +11 -3
  39. package/docs/guide/upgrade.md +93 -43
  40. package/docs/reference/env.md +24 -32
  41. package/docs/reference/naming.md +14 -7
  42. package/docs/reference/surface-contract.md +153 -0
  43. package/package.json +33 -11
  44. package/prompts/mu-debug.md +3 -1
  45. package/prompts/mu-refute.md +2 -1
  46. package/prompts/mu-research.md +3 -1
  47. package/prompts/mu-review.md +2 -1
  48. package/prompts/mu-rules-audit.md +5 -4
  49. package/prompts/mu-sweep.md +2 -1
  50. package/prompts/mu-tournament.md +4 -2
  51. package/prompts/mu-until.md +3 -1
  52. package/prompts/ultrathink.md +1 -1
  53. package/skills/mu/SKILL.md +60 -291
  54. package/skills/mu/recipes/adversarial-review.md +12 -103
  55. package/skills/mu/recipes/brief.md +14 -61
  56. package/skills/mu/recipes/course-correct.md +23 -0
  57. package/skills/mu/recipes/findings.md +17 -111
  58. package/skills/mu/recipes/models.md +17 -92
  59. package/skills/mu/recipes/move.md +14 -0
  60. package/skills/mu/recipes/orchestrator-loop.md +20 -119
  61. package/skills/mu/recipes/recovery.md +8 -29
  62. package/skills/mu/recipes/remote-workers.md +17 -318
  63. package/skills/mu/recipes/tasks-or-calls.md +15 -78
  64. package/skills/mu/recipes/test-health.md +19 -77
  65. package/skills/mu/recipes/worker.md +17 -24
  66. package/dist/chunk-45KQLMEL.js +0 -2908
  67. package/dist/chunk-45KQLMEL.js.map +0 -1
  68. package/dist/chunk-G434JKV2.js +0 -641
  69. package/dist/chunk-G434JKV2.js.map +0 -1
  70. package/dist/chunk-KUGUFBWH.js +0 -12527
  71. package/dist/chunk-KUGUFBWH.js.map +0 -1
  72. package/dist/chunk-QITLNZEM.js +0 -87
  73. package/dist/chunk-QITLNZEM.js.map +0 -1
  74. package/dist/chunk-VIEYQMQA.js +0 -393
  75. package/dist/chunk-VIEYQMQA.js.map +0 -1
  76. package/dist/chunk-WI6AWCMK.js +0 -19
  77. package/dist/chunk-WI6AWCMK.js.map +0 -1
  78. package/dist/cli-CKOMHBXV.js.map +0 -1
  79. package/dist/devtools-LS4ZEPWM.js +0 -3772
  80. package/dist/devtools-LS4ZEPWM.js.map +0 -1
  81. package/dist/tui-LFJ5P4Y2.js +0 -40956
  82. package/dist/tui-LFJ5P4Y2.js.map +0 -1
  83. package/dist/tui-sync-worker.js +0 -17
  84. package/dist/tui-sync-worker.js.map +0 -1
  85. package/dist/yank-XDOONQSL.js +0 -13
  86. package/dist/yank-XDOONQSL.js.map +0 -1
  87. package/docs/ROADMAP.md +0 -123
  88. package/docs/VISION.md +0 -298
  89. package/docs/architecture/control-socket.md +0 -142
  90. package/docs/architecture/dag.md +0 -117
  91. package/docs/architecture/mux.md +0 -167
  92. package/docs/architecture/ops-log.md +0 -160
  93. package/docs/architecture/sdk.md +0 -187
  94. package/docs/architecture/sync.md +0 -159
  95. package/docs/architecture/tui.md +0 -165
  96. package/docs/guide/tui.md +0 -98
  97. package/docs/history/CHANGELOG-pre-3.md +0 -4866
  98. package/docs/img/tui-dashboard.png +0 -0
  99. package/docs/specs/2026-09-28-agent-state-from-murmur.md +0 -333
  100. package/docs/specs/2026-09-29-task-substates.md +0 -247
  101. package/docs/specs/2026-10-03-keep-driving-nudge.md +0 -163
  102. package/docs/specs/2026-10-21-fast-and-slow-gates.md +0 -229
  103. package/skills/mu/recipes/backlog-triage.md +0 -45
  104. package/skills/mu/recipes/codemode-driver.md +0 -118
  105. package/skills/mu/recipes/deep-research.md +0 -69
  106. package/skills/mu/recipes/drift-audit.md +0 -75
  107. package/skills/mu/recipes/fan-out.md +0 -42
  108. package/skills/mu/recipes/hypothesis-panel.md +0 -48
  109. package/skills/mu/recipes/long-run.md +0 -32
  110. package/skills/mu/recipes/loop-until-done.md +0 -45
  111. package/skills/mu/recipes/plan.md +0 -67
  112. package/skills/mu/recipes/refute.md +0 -55
  113. package/skills/mu/recipes/review-panel.md +0 -65
  114. package/skills/mu/recipes/rules-audit.md +0 -89
  115. package/skills/mu/recipes/tournament.md +0 -44
  116. package/skills/mu/recipes/ultrathink.md +0 -86
  117. package/skills/mu/recipes/watcher.md +0 -28
  118. package/skills/mu/recipes/waves.md +0 -21
  119. /package/dist/{chunk-3BARPJLY.js.map → errors-GRONKWFS.js.map} +0 -0
@@ -1,641 +0,0 @@
1
- import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
2
- import {
3
- groupIdFromPrefix
4
- } from "./chunk-KUGUFBWH.js";
5
- import {
6
- LEGACY_LOG_ONLY_SQL_EXCLUSION,
7
- localMachineId,
8
- mapLegacyStatus,
9
- repairTaskPair,
10
- resolvePair,
11
- resolveWorkstreamId,
12
- withOpContext
13
- } from "./chunk-45KQLMEL.js";
14
-
15
- // src/undo.ts
16
- var UndoGroupNotFoundError = class extends Error {
17
- constructor(groupId) {
18
- super(`no ops found for group ${JSON.stringify(groupId)}`);
19
- this.groupId = groupId;
20
- this.name = "UndoGroupNotFoundError";
21
- }
22
- groupId;
23
- errorNextSteps() {
24
- return [
25
- { intent: "List recent undoable groups", command: "mu undo" },
26
- { intent: "Inspect a group's ops", command: "mu log --group <id>" }
27
- ];
28
- }
29
- };
30
- var NothingToUndoError = class extends Error {
31
- constructor() {
32
- super("the ops log is empty; nothing to undo");
33
- this.name = "NothingToUndoError";
34
- }
35
- errorNextSteps() {
36
- return [{ intent: "Make a change first", command: "mu task add <id> -t <title>" }];
37
- }
38
- };
39
- var UndoSupersededError = class extends Error {
40
- constructor(groupId, conflicts) {
41
- const shown = conflicts.slice(0, 5).map((c) => `${c.key}.${c.field} (changed by ${c.groupId.slice(0, 8)})`).join(", ");
42
- super(
43
- `group ${groupId.slice(0, 8)} has been superseded: ${conflicts.length} field(s) were changed by a later action \u2014 ${shown}. Undoing would discard that newer work.`
44
- );
45
- this.groupId = groupId;
46
- this.conflicts = conflicts;
47
- this.name = "UndoSupersededError";
48
- }
49
- groupId;
50
- conflicts;
51
- errorNextSteps() {
52
- return [
53
- {
54
- intent: "Inspect what changed since",
55
- command: `mu log --group ${this.groupId.slice(0, 8)}`
56
- },
57
- {
58
- intent: "Undo anyway, discarding the newer edits",
59
- command: `mu undo ${this.groupId.slice(0, 8)} --yes --force`
60
- }
61
- ];
62
- }
63
- };
64
- var ENTITY_RESTORE_ORDER = {
65
- workstream: 0,
66
- task: 1,
67
- note: 2,
68
- edge: 2
69
- };
70
- function restoreRank(entity) {
71
- return ENTITY_RESTORE_ORDER[entity] ?? 3;
72
- }
73
- function priorFieldValue(db, entity, key, hlc, field, machineId) {
74
- const row = db.prepare(
75
- `SELECT json_extract(payload, '$.' || @field) AS value
76
- FROM ops
77
- WHERE entity = @entity
78
- AND key = @key
79
- AND op = 'put'
80
- AND hlc < @hlc
81
- AND (@machineId IS NULL OR machine_id = @machineId)
82
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
83
- AND json_type(payload, '$.' || @field) IS NOT NULL
84
- ORDER BY hlc DESC
85
- LIMIT 1`
86
- ).get({ entity, key, hlc, field, machineId: machineId ?? null });
87
- if (row === void 0) return { found: false };
88
- return { found: true, value: row.value };
89
- }
90
- function laterWriters(db, entity, key, hlc, field, excludeGroup, machineId) {
91
- return db.prepare(
92
- `SELECT DISTINCT group_id AS groupId, intent
93
- FROM ops
94
- WHERE entity = @entity
95
- AND key = @key
96
- AND op = 'put'
97
- AND hlc > @hlc
98
- AND group_id <> @excludeGroup
99
- AND (@machineId IS NULL OR machine_id = @machineId)
100
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
101
- AND json_type(payload, '$.' || @field) IS NOT NULL`
102
- ).all({ entity, key, hlc, field, excludeGroup, machineId: machineId ?? null });
103
- }
104
- function laterRowWriters(db, entity, key, hlc, excludeGroup) {
105
- return db.prepare(
106
- `SELECT DISTINCT group_id AS groupId, intent
107
- FROM ops
108
- WHERE entity = @entity
109
- AND key = @key
110
- AND op = 'put'
111
- AND hlc > @hlc
112
- AND group_id <> @excludeGroup
113
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}`
114
- ).all({ entity, key, hlc, excludeGroup });
115
- }
116
- function isDeletedByLaterGroup(db, entity, key, groupId) {
117
- const row = db.prepare(
118
- `SELECT op, group_id AS groupId FROM ops
119
- WHERE entity = @entity AND key = @key
120
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
121
- ORDER BY hlc DESC LIMIT 1`
122
- ).get({ entity, key });
123
- return row?.op === "del" && row.groupId !== groupId;
124
- }
125
- function laterDeleters(db, entity, key, hlc, excludeGroup) {
126
- return db.prepare(
127
- `SELECT DISTINCT group_id AS groupId, intent
128
- FROM ops
129
- WHERE entity = @entity
130
- AND key = @key
131
- AND op = 'del'
132
- AND hlc > @hlc
133
- AND group_id <> @excludeGroup`
134
- ).all({ entity, key, hlc, excludeGroup });
135
- }
136
- function listRecentGroups(db, limit = 10) {
137
- const rows = db.prepare(
138
- `SELECT group_id AS groupId,
139
- MAX(hlc) AS hlc,
140
- MIN(created_at) AS when_,
141
- COUNT(*) AS ops,
142
- MAX(actor) AS actor
143
- FROM ops
144
- WHERE entity IN ('workstream','task','note','edge')
145
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
146
- GROUP BY group_id
147
- ORDER BY MAX(hlc) DESC
148
- LIMIT @limit`
149
- ).all({ limit });
150
- return rows.map((row) => {
151
- const intents = db.prepare(
152
- `SELECT DISTINCT intent FROM ops
153
- WHERE group_id = @groupId AND intent IS NOT NULL
154
- ORDER BY seq`
155
- ).all({ groupId: row.groupId }).map((r) => r.intent);
156
- return {
157
- groupId: row.groupId,
158
- intents,
159
- actor: row.actor,
160
- ops: row.ops,
161
- when: row.when_,
162
- hlc: row.hlc
163
- };
164
- });
165
- }
166
- function mostRecentGroup(db) {
167
- return listRecentGroups(db, 1)[0] ?? null;
168
- }
169
- function resolveGroupId(db, prefix) {
170
- const resolved = groupIdFromPrefix(db, prefix);
171
- if (resolved === null) throw new UndoGroupNotFoundError(prefix);
172
- return resolved;
173
- }
174
- var CAPTURED_TABLE_FOR = {
175
- workstream: "workstreams",
176
- task: "tasks",
177
- note: "task_notes",
178
- edge: "task_edges"
179
- };
180
- var NEVER_RESTORE = /* @__PURE__ */ new Set(["id", "local_id", "name", "workstream_id", "task_id"]);
181
- var OWNER_FIELD = "owner_id";
182
- var NEVER_RECONSTRUCT = /* @__PURE__ */ new Set([...NEVER_RESTORE, OWNER_FIELD]);
183
- var TOUCH_FIELD = "updated_at";
184
- function planUndo(db, groupId) {
185
- const rows = db.prepare(
186
- `SELECT o.seq, o.hlc, o.group_id, o.machine_id, o.intent, o.actor, o.entity, o.key, o.op, o.payload,
187
- (SELECT COUNT(*) FROM ops p
188
- WHERE p.entity = o.entity AND p.key = o.key AND p.hlc < o.hlc
189
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}) AS prior,
190
- (SELECT p.op FROM ops p
191
- WHERE p.entity = o.entity AND p.key = o.key AND p.hlc < o.hlc
192
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
193
- ORDER BY p.hlc DESC LIMIT 1) AS priorKind
194
- FROM ops o
195
- WHERE o.group_id = @groupId
196
- AND o.entity IN ('workstream','task','note','edge')
197
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
198
- ORDER BY o.hlc`
199
- ).all({ groupId });
200
- if (rows.length === 0) throw new UndoGroupNotFoundError(groupId);
201
- const inverses = [];
202
- let skipped = 0;
203
- const seenLegacyNoteTasks = /* @__PURE__ */ new Set();
204
- const restoredFields = /* @__PURE__ */ new Map();
205
- let machine;
206
- const localMachine = () => {
207
- machine ??= localMachineId(db);
208
- return machine;
209
- };
210
- for (const row of rows) {
211
- if (row.op === "del") {
212
- let fields2 = reconstructRow(db, row.entity, row.key, row.hlc);
213
- if (Object.keys(fields2).length === 0) fields2 = payloadFields(row.payload);
214
- if (Object.keys(fields2).length === 0 && row.entity === "note") {
215
- const recovered = recoverLegacyNoteContents(db, row.key, row.hlc, seenLegacyNoteTasks);
216
- for (const legacy2 of recovered) {
217
- inverses.push({
218
- entity: "note",
219
- key: legacy2.key,
220
- op: "put",
221
- fields: legacy2.fields,
222
- summary: `note ${legacy2.key} (recovered from legacy history)`,
223
- supersededBy: []
224
- });
225
- }
226
- skipped += 1;
227
- continue;
228
- }
229
- const status = fields2.status;
230
- const legacy = typeof status === "string" ? mapLegacyStatus(status) : null;
231
- if (legacy) {
232
- fields2.status = legacy.status;
233
- fields2.substate = legacy.substate;
234
- }
235
- const conflicts = laterRowWriters(db, row.entity, row.key, row.hlc, groupId).map((w) => ({
236
- field: "<row>",
237
- groupId: w.groupId,
238
- intent: w.intent
239
- }));
240
- inverses.push({
241
- entity: row.entity,
242
- key: row.key,
243
- op: "put",
244
- fields: fields2,
245
- summary: `${row.entity} ${row.key} (from tombstone)`,
246
- supersededBy: conflicts
247
- });
248
- continue;
249
- }
250
- if (row.prior === 0 || row.priorKind === "del") {
251
- if (isDeletedByLaterGroup(db, row.entity, row.key, groupId)) {
252
- skipped += 1;
253
- continue;
254
- }
255
- const later = [...laterRowWriters(db, row.entity, row.key, row.hlc, groupId)];
256
- inverses.push({
257
- entity: row.entity,
258
- key: row.key,
259
- op: "del",
260
- fields: {},
261
- summary: `${row.entity} ${row.key} (it was created by this group)`,
262
- supersededBy: later.map((w) => ({ field: "<row>", groupId: w.groupId, intent: w.intent }))
263
- });
264
- continue;
265
- }
266
- const payload = JSON.parse(row.payload);
267
- const fields = {};
268
- const supersededBy = [];
269
- const rowKey = `${row.entity}\0${row.key}`;
270
- const restored = restoredFields.get(rowKey) ?? /* @__PURE__ */ new Set();
271
- restoredFields.set(rowKey, restored);
272
- for (const field of Object.keys(payload)) {
273
- if (NEVER_RESTORE.has(field) || restored.has(field)) continue;
274
- restored.add(field);
275
- let machineId;
276
- if (field === OWNER_FIELD) {
277
- if (row.machine_id !== localMachine()) continue;
278
- machineId = localMachine();
279
- }
280
- const prior = field === OWNER_FIELD ? priorOwner(db, row.key, row.hlc, localMachine()) : priorFieldValue(db, row.entity, row.key, row.hlc, field, machineId);
281
- if (!prior.found) {
282
- continue;
283
- }
284
- if (field === OWNER_FIELD && prior.value !== null && !agentExists(db, prior.value)) continue;
285
- const writers = laterWriters(db, row.entity, row.key, row.hlc, field, groupId, machineId);
286
- if (field === TOUCH_FIELD && writers.length > 0) continue;
287
- fields[field] = prior.value;
288
- for (const writer of writers) {
289
- supersededBy.push({ field, groupId: writer.groupId, intent: writer.intent });
290
- }
291
- }
292
- for (const deleter of laterDeleters(db, row.entity, row.key, row.hlc, groupId)) {
293
- supersededBy.push({ field: "<row>", groupId: deleter.groupId, intent: deleter.intent });
294
- }
295
- if (Object.keys(fields).length === 0) {
296
- skipped += 1;
297
- continue;
298
- }
299
- inverses.push({
300
- entity: row.entity,
301
- key: row.key,
302
- op: "put",
303
- fields,
304
- summary: `${row.entity} ${row.key} ${Object.entries(fields).map(([f, v]) => `${f}=${v === null ? "NULL" : String(v)}`).join(" ")}`,
305
- supersededBy
306
- });
307
- }
308
- const ordered = [...inverses].sort((a, b) => {
309
- if (a.op !== b.op) return a.op === "put" ? -1 : 1;
310
- const rank = a.op === "put" ? restoreRank(a.entity) - restoreRank(b.entity) : restoreRank(b.entity) - restoreRank(a.entity);
311
- return rank;
312
- });
313
- const intents = [...new Set(rows.map((r) => r.intent).filter((i) => i !== null))];
314
- return {
315
- groupId,
316
- intents,
317
- when: groupWhen(db, groupId),
318
- inverses: ordered,
319
- superseded: ordered.some((i) => i.supersededBy.length > 0),
320
- skipped
321
- };
322
- }
323
- function priorOwner(db, key, hlc, machineId) {
324
- const local = priorFieldValue(db, "task", key, hlc, OWNER_FIELD, machineId);
325
- if (!local.found) return { found: true, value: null };
326
- const tomb = db.prepare(
327
- `SELECT 1 FROM ops d
328
- WHERE d.entity = 'task' AND d.key = @key AND d.op = 'del' AND d.hlc < @hlc
329
- AND d.hlc > (SELECT MAX(hlc) FROM ops
330
- WHERE entity = 'task' AND key = @key AND op = 'put'
331
- AND hlc < @hlc AND machine_id = @machineId
332
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
333
- AND json_type(payload, '$.owner_id') IS NOT NULL)
334
- AND NOT EXISTS (SELECT 1 FROM ops p
335
- WHERE p.entity = 'task' AND p.key = @key AND p.op = 'put'
336
- AND p.hlc > d.hlc AND p.seq < d.seq)
337
- LIMIT 1`
338
- ).get({ key, hlc, machineId });
339
- return tomb === void 0 ? local : { found: true, value: null };
340
- }
341
- function agentExists(db, id) {
342
- return db.prepare("SELECT 1 FROM agents WHERE id = ?").get(id) !== void 0;
343
- }
344
- function groupWhen(db, groupId) {
345
- const row = db.prepare("SELECT MIN(created_at) AS when_ FROM ops WHERE group_id = ?").get(groupId);
346
- return row?.when_ ?? "";
347
- }
348
- function recoverLegacyNoteContents(db, tombstoneKey, beforeHlc, seen) {
349
- const parsed = parseNoteKey(tombstoneKey);
350
- if (parsed === null) return [];
351
- if (seen.has(parsed.taskKey)) return [];
352
- seen.add(parsed.taskKey);
353
- const rows = db.prepare(
354
- `SELECT key, payload FROM ops
355
- WHERE entity = 'note'
356
- AND op = 'put'
357
- AND hlc < @hlc
358
- AND key LIKE @prefix
359
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
360
- ORDER BY hlc`
361
- ).all({ hlc: beforeHlc, prefix: `${parsed.taskKey}#%` });
362
- const out = [];
363
- const contents = /* @__PURE__ */ new Set();
364
- for (const row of rows) {
365
- const fields = payloadFields(row.payload);
366
- const content = fields.content;
367
- if (typeof content !== "string" || content === "") continue;
368
- if (contents.has(content)) continue;
369
- contents.add(content);
370
- out.push({ key: row.key, fields });
371
- }
372
- return out;
373
- }
374
- function payloadFields(payload) {
375
- const fields = {};
376
- const parsed = JSON.parse(payload);
377
- for (const [field, value] of Object.entries(parsed)) {
378
- if (NEVER_RECONSTRUCT.has(field)) continue;
379
- if (value === null || typeof value === "string" || typeof value === "number") {
380
- fields[field] = value;
381
- }
382
- }
383
- return fields;
384
- }
385
- function reconstructRow(db, entity, key, beforeHlc) {
386
- const puts = db.prepare(
387
- `SELECT payload FROM ops
388
- WHERE entity = @entity AND key = @key AND op = 'put' AND hlc < @hlc
389
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
390
- ORDER BY hlc`
391
- ).all({ entity, key, hlc: beforeHlc });
392
- const fields = {};
393
- for (const put of puts) {
394
- const parsed = JSON.parse(put.payload);
395
- for (const [field, value] of Object.entries(parsed)) {
396
- if (NEVER_RECONSTRUCT.has(field)) continue;
397
- if (value === null || typeof value === "string" || typeof value === "number") {
398
- fields[field] = value;
399
- }
400
- }
401
- }
402
- return fields;
403
- }
404
- function undoGroup(db, groupId, opts = {}) {
405
- const plan = planUndo(db, groupId);
406
- if (plan.superseded && opts.force !== true) {
407
- const conflicts = plan.inverses.flatMap(
408
- (inv) => inv.supersededBy.map((s) => ({ key: inv.key, field: s.field, groupId: s.groupId }))
409
- );
410
- throw new UndoSupersededError(groupId, conflicts);
411
- }
412
- let applied = 0;
413
- let undoGroupId = "";
414
- const run = db.transaction(() => {
415
- withOpContext(
416
- db,
417
- { intent: "undo", group: "new", ...opts.actor !== void 0 ? { actor: opts.actor } : {} },
418
- () => {
419
- const ctx = db.prepare("SELECT group_id FROM _op_ctx").get();
420
- undoGroupId = ctx?.group_id ?? "";
421
- for (const inverse of plan.inverses) {
422
- if (applyInverse(db, inverse)) applied += 1;
423
- }
424
- }
425
- );
426
- });
427
- run.immediate();
428
- return { plan, undoGroupId: applied > 0 && undoGroupId !== "" ? undoGroupId : null, applied };
429
- }
430
- function applyInverse(db, inverse) {
431
- const table = CAPTURED_TABLE_FOR[inverse.entity];
432
- if (table === void 0) return false;
433
- if (inverse.op === "del") return deleteRow(db, inverse);
434
- return restoreRow(db, inverse, table);
435
- }
436
- function deleteRow(db, inverse) {
437
- switch (inverse.entity) {
438
- case "workstream":
439
- return db.prepare("DELETE FROM workstreams WHERE name = ?").run(inverse.key).changes > 0;
440
- case "task": {
441
- const id = taskIdForKey(db, inverse.key);
442
- if (id === null) return false;
443
- return db.prepare("DELETE FROM tasks WHERE id = ?").run(id).changes > 0;
444
- }
445
- case "note": {
446
- const parsed = parseNoteKey(inverse.key);
447
- if (parsed === null) return false;
448
- const taskId = taskIdForKey(db, parsed.taskKey);
449
- if (taskId === null) return false;
450
- const note = db.prepare(
451
- `SELECT json_extract(payload, '$.content') AS content,
452
- json_extract(payload, '$.author') AS author,
453
- json_extract(payload, '$.created_at') AS created_at FROM ops
454
- WHERE entity = 'note' AND key = ? AND op = 'put'
455
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
456
- ORDER BY hlc DESC LIMIT 1`
457
- ).get(inverse.key);
458
- if (note?.content === void 0 || note.content === null) return false;
459
- return db.prepare(
460
- `DELETE FROM task_notes
461
- WHERE task_id = @taskId AND content = @content
462
- AND COALESCE(author, '') = COALESCE(@author, '')
463
- AND (@createdAt IS NULL OR created_at = @createdAt)`
464
- ).run({
465
- taskId,
466
- content: note.content,
467
- author: note.author,
468
- createdAt: note.created_at
469
- }).changes > 0;
470
- }
471
- case "edge": {
472
- const parsed = parseEdgeKey(inverse.key);
473
- if (parsed === null) return false;
474
- const from = taskIdForKey(db, parsed.blocker);
475
- const to = taskIdForKey(db, parsed.blocked);
476
- if (from === null || to === null) return false;
477
- return db.prepare("DELETE FROM task_edges WHERE from_task_id = ? AND to_task_id = ?").run(from, to).changes > 0;
478
- }
479
- default:
480
- return false;
481
- }
482
- }
483
- function restoreRow(db, inverse, table) {
484
- switch (inverse.entity) {
485
- case "workstream": {
486
- const exists = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(inverse.key);
487
- if (!exists) {
488
- db.prepare("INSERT INTO workstreams (name, created_at) VALUES (?, ?)").run(
489
- inverse.key,
490
- String(inverse.fields.created_at ?? (/* @__PURE__ */ new Date()).toISOString())
491
- );
492
- return true;
493
- }
494
- return updateFields(db, "workstreams", "id", exists.id, inverse.fields);
495
- }
496
- case "task": {
497
- const parsed = parseTaskKey(inverse.key);
498
- if (parsed === null) return false;
499
- const id = taskIdForKey(db, inverse.key);
500
- if (id === null) {
501
- const wsId = ensureWorkstream(db, parsed.workstream);
502
- const now = (/* @__PURE__ */ new Date()).toISOString();
503
- const pair = resolvePair(
504
- String(inverse.fields.status ?? "OPEN"),
505
- inverse.fields.substate
506
- ) ?? {
507
- status: "OPEN",
508
- substate: "todo"
509
- };
510
- db.prepare(
511
- `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact,
512
- effort_days, owner_id, created_at, updated_at)
513
- VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`
514
- ).run(
515
- wsId,
516
- parsed.localId,
517
- String(inverse.fields.title ?? parsed.localId),
518
- pair.status,
519
- pair.substate,
520
- Number(inverse.fields.impact ?? 50),
521
- Number(inverse.fields.effort_days ?? 1),
522
- String(inverse.fields.created_at ?? now),
523
- String(inverse.fields.updated_at ?? now)
524
- );
525
- return true;
526
- }
527
- const updated = updateFields(db, "tasks", "id", id, inverse.fields);
528
- return repairTaskPair(db, id) || updated;
529
- }
530
- case "note": {
531
- const parsed = parseNoteKey(inverse.key);
532
- if (parsed === null) return false;
533
- const taskId = taskIdForKey(db, parsed.taskKey);
534
- if (taskId === null) return false;
535
- const content = inverse.fields.content;
536
- if (content === void 0 || content === null) return false;
537
- const author = inverse.fields.author;
538
- const createdAt = inverse.fields.created_at;
539
- const exists = db.prepare(
540
- `SELECT id FROM task_notes
541
- WHERE task_id = @taskId AND content = @content
542
- AND COALESCE(author, '') = COALESCE(@author, '')
543
- AND (@createdAt IS NULL OR created_at = @createdAt)`
544
- ).get({
545
- taskId,
546
- content: String(content),
547
- author: author === void 0 || author === null ? null : String(author),
548
- createdAt: createdAt === void 0 || createdAt === null ? null : String(createdAt)
549
- });
550
- if (exists) return false;
551
- db.prepare(
552
- "INSERT INTO task_notes (task_id, author, content, created_at) VALUES (?, ?, ?, ?)"
553
- ).run(
554
- taskId,
555
- inverse.fields.author === void 0 || inverse.fields.author === null ? null : String(inverse.fields.author),
556
- String(content),
557
- String(inverse.fields.created_at ?? (/* @__PURE__ */ new Date()).toISOString())
558
- );
559
- return true;
560
- }
561
- case "edge": {
562
- const parsed = parseEdgeKey(inverse.key);
563
- if (parsed === null) return false;
564
- const from = taskIdForKey(db, parsed.blocker);
565
- const to = taskIdForKey(db, parsed.blocked);
566
- if (from === null || to === null) return false;
567
- const exists = db.prepare("SELECT 1 AS x FROM task_edges WHERE from_task_id = ? AND to_task_id = ?").get(from, to);
568
- if (exists) return false;
569
- db.prepare(
570
- "INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
571
- ).run(from, to, String(inverse.fields.created_at ?? (/* @__PURE__ */ new Date()).toISOString()));
572
- return true;
573
- }
574
- default:
575
- void table;
576
- return false;
577
- }
578
- }
579
- function updateFields(db, table, idColumn, id, fields) {
580
- const names = Object.keys(fields).filter((f) => !NEVER_RESTORE.has(f));
581
- if (names.length === 0) return false;
582
- const current = db.prepare(`SELECT * FROM ${table} WHERE ${idColumn} = ?`).get(id);
583
- const pending = names.filter((name) => {
584
- const target = fields[name] ?? null;
585
- if (current === void 0) return true;
586
- return current[name] !== target;
587
- });
588
- if (pending.length === 0) return false;
589
- const setClause = pending.map((name) => `${name} = @${name}`).join(", ");
590
- const params = { id };
591
- for (const name of pending) params[name] = fields[name] ?? null;
592
- db.prepare(`UPDATE ${table} SET ${setClause} WHERE ${idColumn} = @id`).run(params);
593
- return true;
594
- }
595
- function ensureWorkstream(db, name) {
596
- const existing = db.prepare("SELECT id FROM workstreams WHERE name = ?").get(name);
597
- if (existing) return existing.id;
598
- db.prepare("INSERT INTO workstreams (name, created_at) VALUES (?, ?)").run(
599
- name,
600
- (/* @__PURE__ */ new Date()).toISOString()
601
- );
602
- return resolveWorkstreamId(db, name);
603
- }
604
- function parseTaskKey(key) {
605
- const slash = key.indexOf("/");
606
- if (slash <= 0 || slash === key.length - 1) return null;
607
- return { workstream: key.slice(0, slash), localId: key.slice(slash + 1) };
608
- }
609
- function parseNoteKey(key) {
610
- const hash = key.lastIndexOf("#");
611
- if (hash <= 0) return null;
612
- return { taskKey: key.slice(0, hash) };
613
- }
614
- function parseEdgeKey(key) {
615
- const arrow = key.indexOf("->");
616
- if (arrow <= 0) return null;
617
- return { blocker: key.slice(0, arrow), blocked: key.slice(arrow + 2) };
618
- }
619
- function taskIdForKey(db, key) {
620
- const parsed = parseTaskKey(key);
621
- if (parsed === null) return null;
622
- const row = db.prepare(
623
- `SELECT t.id AS id FROM tasks t
624
- JOIN workstreams w ON w.id = t.workstream_id
625
- WHERE w.name = ? AND t.local_id = ?`
626
- ).get(parsed.workstream, parsed.localId);
627
- return row?.id ?? null;
628
- }
629
-
630
- export {
631
- UndoGroupNotFoundError,
632
- NothingToUndoError,
633
- UndoSupersededError,
634
- priorFieldValue,
635
- listRecentGroups,
636
- mostRecentGroup,
637
- resolveGroupId,
638
- planUndo,
639
- undoGroup
640
- };
641
- //# sourceMappingURL=chunk-G434JKV2.js.map