@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.
- package/AGENTS.md +73 -35
- package/README.md +71 -57
- package/dist/chunk-4ULY3ZKO.js +4482 -0
- package/dist/chunk-4ULY3ZKO.js.map +1 -0
- package/dist/chunk-7M634JMD.js +375 -0
- package/dist/chunk-7M634JMD.js.map +1 -0
- package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
- package/dist/chunk-PZ5GOCWH.js.map +1 -0
- package/dist/chunk-SS7NQSSZ.js +3660 -0
- package/dist/chunk-SS7NQSSZ.js.map +1 -0
- package/dist/chunk-UCZPJX7K.js +14253 -0
- package/dist/chunk-UCZPJX7K.js.map +1 -0
- package/dist/{cli-CKOMHBXV.js → cli-TV3N47AZ.js} +7317 -4593
- package/dist/cli-TV3N47AZ.js.map +1 -0
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/errors-GRONKWFS.js +65 -0
- package/dist/extension/mu-pi.js +1632 -837
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +4067 -2561
- package/dist/index.js +614 -297
- package/dist/index.js.map +1 -1
- package/dist/tui-FUBQ326D.js +1848 -0
- package/dist/tui-FUBQ326D.js.map +1 -0
- package/docs/ARCHITECTURE.md +118 -66
- package/docs/VOCABULARY.md +113 -49
- package/docs/guide/README.md +3 -3
- package/docs/guide/backends.md +8 -4
- package/docs/guide/cleanup.md +69 -33
- package/docs/guide/delegate.md +149 -66
- package/docs/guide/dispatch.md +148 -53
- package/docs/guide/getting-started.md +37 -44
- package/docs/guide/other-agent-clis.md +102 -0
- package/docs/guide/recovery.md +31 -50
- package/docs/guide/remote.md +122 -45
- package/docs/guide/sql.md +104 -24
- package/docs/guide/stop-a-worker.md +30 -37
- package/docs/guide/sync.md +11 -3
- package/docs/guide/upgrade.md +93 -43
- package/docs/reference/env.md +24 -32
- package/docs/reference/naming.md +14 -7
- package/docs/reference/surface-contract.md +153 -0
- package/package.json +33 -11
- package/prompts/mu-debug.md +3 -1
- package/prompts/mu-refute.md +2 -1
- package/prompts/mu-research.md +3 -1
- package/prompts/mu-review.md +2 -1
- package/prompts/mu-rules-audit.md +5 -4
- package/prompts/mu-sweep.md +2 -1
- package/prompts/mu-tournament.md +4 -2
- package/prompts/mu-until.md +3 -1
- package/prompts/ultrathink.md +1 -1
- package/skills/mu/SKILL.md +60 -291
- package/skills/mu/recipes/adversarial-review.md +12 -103
- package/skills/mu/recipes/brief.md +14 -61
- package/skills/mu/recipes/course-correct.md +23 -0
- package/skills/mu/recipes/findings.md +17 -111
- package/skills/mu/recipes/models.md +17 -92
- package/skills/mu/recipes/move.md +14 -0
- package/skills/mu/recipes/orchestrator-loop.md +20 -119
- package/skills/mu/recipes/recovery.md +8 -29
- package/skills/mu/recipes/remote-workers.md +17 -318
- package/skills/mu/recipes/tasks-or-calls.md +15 -78
- package/skills/mu/recipes/test-health.md +19 -77
- package/skills/mu/recipes/worker.md +17 -24
- package/dist/chunk-45KQLMEL.js +0 -2908
- package/dist/chunk-45KQLMEL.js.map +0 -1
- package/dist/chunk-G434JKV2.js +0 -641
- package/dist/chunk-G434JKV2.js.map +0 -1
- package/dist/chunk-KUGUFBWH.js +0 -12527
- package/dist/chunk-KUGUFBWH.js.map +0 -1
- package/dist/chunk-QITLNZEM.js +0 -87
- package/dist/chunk-QITLNZEM.js.map +0 -1
- package/dist/chunk-VIEYQMQA.js +0 -393
- package/dist/chunk-VIEYQMQA.js.map +0 -1
- package/dist/chunk-WI6AWCMK.js +0 -19
- package/dist/chunk-WI6AWCMK.js.map +0 -1
- package/dist/cli-CKOMHBXV.js.map +0 -1
- package/dist/devtools-LS4ZEPWM.js +0 -3772
- package/dist/devtools-LS4ZEPWM.js.map +0 -1
- package/dist/tui-LFJ5P4Y2.js +0 -40956
- package/dist/tui-LFJ5P4Y2.js.map +0 -1
- package/dist/tui-sync-worker.js +0 -17
- package/dist/tui-sync-worker.js.map +0 -1
- package/dist/yank-XDOONQSL.js +0 -13
- package/dist/yank-XDOONQSL.js.map +0 -1
- package/docs/ROADMAP.md +0 -123
- package/docs/VISION.md +0 -298
- package/docs/architecture/control-socket.md +0 -142
- package/docs/architecture/dag.md +0 -117
- package/docs/architecture/mux.md +0 -167
- package/docs/architecture/ops-log.md +0 -160
- package/docs/architecture/sdk.md +0 -187
- package/docs/architecture/sync.md +0 -159
- package/docs/architecture/tui.md +0 -165
- package/docs/guide/tui.md +0 -98
- package/docs/history/CHANGELOG-pre-3.md +0 -4866
- package/docs/img/tui-dashboard.png +0 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +0 -333
- package/docs/specs/2026-09-29-task-substates.md +0 -247
- package/docs/specs/2026-10-03-keep-driving-nudge.md +0 -163
- package/docs/specs/2026-10-21-fast-and-slow-gates.md +0 -229
- package/skills/mu/recipes/backlog-triage.md +0 -45
- package/skills/mu/recipes/codemode-driver.md +0 -118
- package/skills/mu/recipes/deep-research.md +0 -69
- package/skills/mu/recipes/drift-audit.md +0 -75
- package/skills/mu/recipes/fan-out.md +0 -42
- package/skills/mu/recipes/hypothesis-panel.md +0 -48
- package/skills/mu/recipes/long-run.md +0 -32
- package/skills/mu/recipes/loop-until-done.md +0 -45
- package/skills/mu/recipes/plan.md +0 -67
- package/skills/mu/recipes/refute.md +0 -55
- package/skills/mu/recipes/review-panel.md +0 -65
- package/skills/mu/recipes/rules-audit.md +0 -89
- package/skills/mu/recipes/tournament.md +0 -44
- package/skills/mu/recipes/ultrathink.md +0 -86
- package/skills/mu/recipes/watcher.md +0 -28
- package/skills/mu/recipes/waves.md +0 -21
- /package/dist/{chunk-3BARPJLY.js.map → errors-GRONKWFS.js.map} +0 -0
package/dist/chunk-G434JKV2.js
DELETED
|
@@ -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
|