@mu-crew/mu 3.3.0 → 3.5.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/README.md +45 -31
- package/dist/cli.js +812 -429
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +221 -55
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +33 -4
- package/dist/index.js +76 -5
- package/dist/index.js.map +1 -1
- package/docs/VISION.md +5 -1
- package/docs/VOCABULARY.md +4 -1
- package/docs/architecture/dag.md +3 -3
- package/docs/architecture/ops-log.md +26 -7
- package/docs/guide/cleanup.md +19 -0
- package/docs/guide/delegate.md +43 -4
- package/docs/reference/env.md +1 -0
- package/docs/reference/naming.md +18 -0
- package/docs/specs/2026-09-29-task-substates.md +1 -1
- package/package.json +1 -1
- package/prompts/mu-debug.md +8 -0
- package/prompts/mu-sweep.md +8 -0
- package/prompts/mu-until.md +8 -0
- package/prompts/ultrathink.md +2 -1
- package/skills/mu/SKILL.md +70 -31
- package/skills/mu/recipes/adversarial-review.md +27 -19
- package/skills/mu/recipes/{triage.md → backlog-triage.md} +9 -4
- package/skills/mu/recipes/codemode-driver.md +68 -20
- package/skills/mu/recipes/deep-research.md +23 -8
- package/skills/mu/recipes/fan-out.md +15 -11
- package/skills/mu/recipes/findings.md +103 -0
- package/skills/mu/recipes/hypothesis-panel.md +18 -13
- package/skills/mu/recipes/long-run.md +4 -4
- package/skills/mu/recipes/loop-until-done.md +7 -3
- package/skills/mu/recipes/orchestrator-loop.md +44 -27
- package/skills/mu/recipes/plan.md +8 -3
- package/skills/mu/recipes/recovery.md +5 -0
- package/skills/mu/recipes/refute.md +38 -36
- package/skills/mu/recipes/remote-workers.md +7 -2
- package/skills/mu/recipes/review-panel.md +36 -31
- package/skills/mu/recipes/rules-audit.md +30 -17
- package/skills/mu/recipes/tasks-or-calls.md +71 -0
- package/skills/mu/recipes/tournament.md +13 -10
- package/skills/mu/recipes/ultrathink.md +20 -11
- package/skills/mu/recipes/watcher.md +4 -1
- package/skills/mu/recipes/waves.md +6 -5
- package/skills/mu/recipes/worker.md +3 -1
package/dist/cli.js
CHANGED
|
@@ -175,6 +175,11 @@ function fullOldPayload(cols) {
|
|
|
175
175
|
const pairs = cols.map((c) => `'${c}', OLD.${c}`).join(", ");
|
|
176
176
|
return `json_object(${pairs})`;
|
|
177
177
|
}
|
|
178
|
+
function noteTombstonePayload(keyExpr, cols) {
|
|
179
|
+
return `CASE WHEN EXISTS (
|
|
180
|
+
SELECT 1 FROM ops WHERE entity = 'note' AND key = ${keyExpr} AND op = 'put'
|
|
181
|
+
) THEN '{}' ELSE ${fullOldPayload(cols)} END`;
|
|
182
|
+
}
|
|
178
183
|
function anyChanged(cols) {
|
|
179
184
|
return cols.map((c) => `NEW.${c} IS NOT OLD.${c}`).join("\n OR ");
|
|
180
185
|
}
|
|
@@ -258,7 +263,7 @@ END;
|
|
|
258
263
|
CREATE TEMP TRIGGER IF NOT EXISTS _cap_task_notes_del
|
|
259
264
|
BEFORE DELETE ON task_notes WHEN ${NOT_APPLYING}
|
|
260
265
|
BEGIN
|
|
261
|
-
${emitOp("note", "del", `${taskKey("OLD.task_id")} || '#' || OLD.id`,
|
|
266
|
+
${emitOp("note", "del", `${taskKey("OLD.task_id")} || '#' || OLD.id`, noteTombstonePayload(`${taskKey("OLD.task_id")} || '#' || OLD.id`, noteCols))}
|
|
262
267
|
END;
|
|
263
268
|
|
|
264
269
|
-- \u2500\u2500\u2500 task_edges \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
|
|
@@ -393,7 +398,7 @@ var init_status = __esm({
|
|
|
393
398
|
TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
|
|
394
399
|
TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
|
|
395
400
|
TASK_SUBSTATES = {
|
|
396
|
-
OPEN: ["todo", "parked"],
|
|
401
|
+
OPEN: ["todo", "parked", "triage"],
|
|
397
402
|
IN_PROGRESS: ["active"],
|
|
398
403
|
CLOSED: ["done", "rejected", "wontfix", "duplicate", "superseded"]
|
|
399
404
|
};
|
|
@@ -607,7 +612,7 @@ CREATE VIEW ready AS
|
|
|
607
612
|
SELECT t.*
|
|
608
613
|
FROM tasks t
|
|
609
614
|
WHERE t.status = 'OPEN'
|
|
610
|
-
AND t.substate
|
|
615
|
+
AND t.substate NOT IN ('parked', 'triage')
|
|
611
616
|
AND NOT EXISTS (
|
|
612
617
|
SELECT 1
|
|
613
618
|
FROM task_edges e
|
|
@@ -5850,7 +5855,7 @@ var init_id = __esm({
|
|
|
5850
5855
|
});
|
|
5851
5856
|
|
|
5852
5857
|
// src/tasks/errors.ts
|
|
5853
|
-
var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskParkedError, InvalidSubstateError;
|
|
5858
|
+
var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskInTriageError, TaskParkedError, InvalidSubstateError;
|
|
5854
5859
|
var init_errors2 = __esm({
|
|
5855
5860
|
"src/tasks/errors.ts"() {
|
|
5856
5861
|
"use strict";
|
|
@@ -6198,6 +6203,30 @@ var init_errors2 = __esm({
|
|
|
6198
6203
|
];
|
|
6199
6204
|
}
|
|
6200
6205
|
};
|
|
6206
|
+
TaskInTriageError = class extends Error {
|
|
6207
|
+
constructor(taskId, workstream) {
|
|
6208
|
+
super(`task ${taskId} is in triage (OPEN/triage); accept it or claim with --force`);
|
|
6209
|
+
this.taskId = taskId;
|
|
6210
|
+
this.workstream = workstream;
|
|
6211
|
+
}
|
|
6212
|
+
taskId;
|
|
6213
|
+
workstream;
|
|
6214
|
+
name = "TaskInTriageError";
|
|
6215
|
+
errorNextSteps() {
|
|
6216
|
+
const ws = this.workstream;
|
|
6217
|
+
return [
|
|
6218
|
+
{
|
|
6219
|
+
intent: "Accept it as work, then claim",
|
|
6220
|
+
command: `mu task accept ${this.taskId} -w ${ws}`
|
|
6221
|
+
},
|
|
6222
|
+
{
|
|
6223
|
+
intent: "Decline it",
|
|
6224
|
+
command: `mu task close ${this.taskId} --as rejected --why "<reason>" -w ${ws}`
|
|
6225
|
+
},
|
|
6226
|
+
{ intent: "Claim it anyway", command: `mu task claim ${this.taskId} --force -w ${ws}` }
|
|
6227
|
+
];
|
|
6228
|
+
}
|
|
6229
|
+
};
|
|
6201
6230
|
TaskParkedError = class extends Error {
|
|
6202
6231
|
constructor(taskId, workstream) {
|
|
6203
6232
|
super(`task ${taskId} is parked (OPEN/parked); unpark it or claim with --force`);
|
|
@@ -6760,8 +6789,17 @@ function addTaskImpl(db, opts) {
|
|
|
6760
6789
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6761
6790
|
const insertResult = db.prepare(
|
|
6762
6791
|
`INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days, created_at, updated_at)
|
|
6763
|
-
VALUES (?, ?, ?, 'OPEN',
|
|
6764
|
-
).run(
|
|
6792
|
+
VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?, ?)`
|
|
6793
|
+
).run(
|
|
6794
|
+
wsId,
|
|
6795
|
+
opts.localId,
|
|
6796
|
+
opts.title,
|
|
6797
|
+
opts.triage === true ? "triage" : "todo",
|
|
6798
|
+
opts.impact,
|
|
6799
|
+
opts.effortDays,
|
|
6800
|
+
now,
|
|
6801
|
+
now
|
|
6802
|
+
);
|
|
6765
6803
|
const newTaskId = Number(insertResult.lastInsertRowid);
|
|
6766
6804
|
if (opts.blockedBy && opts.blockedBy.length > 0) {
|
|
6767
6805
|
const blockerLookupSameWs = db.prepare(
|
|
@@ -7049,6 +7087,28 @@ function parkTask(db, localId, opts) {
|
|
|
7049
7087
|
})()
|
|
7050
7088
|
);
|
|
7051
7089
|
}
|
|
7090
|
+
function acceptTask(db, localId, opts) {
|
|
7091
|
+
return withOpContext(
|
|
7092
|
+
db,
|
|
7093
|
+
{ intent: "task.accept", group: "new" },
|
|
7094
|
+
() => db.transaction(() => {
|
|
7095
|
+
const before = getTask(db, localId, opts.workstream);
|
|
7096
|
+
if (!before) throw new TaskNotFoundError(localId);
|
|
7097
|
+
if (before.status !== "OPEN" || before.substate !== "triage") {
|
|
7098
|
+
return {
|
|
7099
|
+
previousStatus: before.status,
|
|
7100
|
+
status: before.status,
|
|
7101
|
+
previousSubstate: before.substate,
|
|
7102
|
+
substate: before.substate,
|
|
7103
|
+
changed: false
|
|
7104
|
+
};
|
|
7105
|
+
}
|
|
7106
|
+
const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
|
|
7107
|
+
recordEvidenceNote(db, localId, before.workstreamName, "ACCEPT", opts);
|
|
7108
|
+
return r;
|
|
7109
|
+
})()
|
|
7110
|
+
);
|
|
7111
|
+
}
|
|
7052
7112
|
function unparkTask(db, localId, opts) {
|
|
7053
7113
|
return withOpContext(
|
|
7054
7114
|
db,
|
|
@@ -7191,6 +7251,9 @@ function assertNotParked(task, opts) {
|
|
|
7191
7251
|
if (task.status === "OPEN" && task.substate === "parked" && opts.force !== true) {
|
|
7192
7252
|
throw new TaskParkedError(task.name, task.workstreamName);
|
|
7193
7253
|
}
|
|
7254
|
+
if (task.status === "OPEN" && task.substate === "triage" && opts.force !== true) {
|
|
7255
|
+
throw new TaskInTriageError(task.name, task.workstreamName);
|
|
7256
|
+
}
|
|
7194
7257
|
}
|
|
7195
7258
|
async function resolveActorIdentity() {
|
|
7196
7259
|
const worker = await resolveWorkerIdentity();
|
|
@@ -7562,7 +7625,10 @@ var init_adopt = __esm({
|
|
|
7562
7625
|
// src/agents/delegate.ts
|
|
7563
7626
|
function delegateOutcome(agent, timedOut) {
|
|
7564
7627
|
if (agent.dead) return "died";
|
|
7565
|
-
if (agent.fired)
|
|
7628
|
+
if (agent.fired) {
|
|
7629
|
+
if (agent.lastError !== void 0) return "error";
|
|
7630
|
+
return agent.lastText ? "done" : "empty";
|
|
7631
|
+
}
|
|
7566
7632
|
return timedOut ? "timeout" : "pending";
|
|
7567
7633
|
}
|
|
7568
7634
|
var init_delegate = __esm({
|
|
@@ -7775,6 +7841,7 @@ async function waitForAgents(db, input, opts) {
|
|
|
7775
7841
|
st.wasBusy = true;
|
|
7776
7842
|
st.fired = true;
|
|
7777
7843
|
if (snap.lastText !== void 0) st.lastText = snap.lastText;
|
|
7844
|
+
if (snap.lastError !== void 0) st.lastError = snap.lastError;
|
|
7778
7845
|
}
|
|
7779
7846
|
wake();
|
|
7780
7847
|
},
|
|
@@ -10083,6 +10150,10 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
|
|
|
10083
10150
|
blocked: listBlocked(db, workstream),
|
|
10084
10151
|
recentClosed: listRecentClosed(db, workstream),
|
|
10085
10152
|
parkedCount: listTasks(db, workstream, { status: "OPEN", substate: "parked" }).length,
|
|
10153
|
+
triage: listTasks(db, workstream, { status: "OPEN", substate: "triage" }),
|
|
10154
|
+
taskCount: db.prepare(
|
|
10155
|
+
"SELECT count(*) AS n FROM tasks t JOIN workstreams w ON w.id = t.workstream_id WHERE w.name = ?"
|
|
10156
|
+
).get(workstream).n,
|
|
10086
10157
|
allTasks: opts.withAllTasks === true ? listTasks(db, workstream) : [],
|
|
10087
10158
|
workspaces: listWorkspaces2(db, workstream),
|
|
10088
10159
|
workspaceOrphans: listWorkspaceOrphans(db, workstream),
|
|
@@ -10156,6 +10227,8 @@ function minimalSnapshot(workstream) {
|
|
|
10156
10227
|
blocked: [],
|
|
10157
10228
|
recentClosed: [],
|
|
10158
10229
|
parkedCount: 0,
|
|
10230
|
+
triage: [],
|
|
10231
|
+
taskCount: 0,
|
|
10159
10232
|
allTasks: [],
|
|
10160
10233
|
workspaces: [],
|
|
10161
10234
|
workspaceOrphans: [],
|
|
@@ -10336,13 +10409,13 @@ import {
|
|
|
10336
10409
|
appendFileSync,
|
|
10337
10410
|
existsSync as existsSync14,
|
|
10338
10411
|
mkdirSync as mkdirSync5,
|
|
10339
|
-
readdirSync as
|
|
10412
|
+
readdirSync as readdirSync4,
|
|
10340
10413
|
readFileSync as readFileSync3,
|
|
10341
|
-
statSync as
|
|
10414
|
+
statSync as statSync3,
|
|
10342
10415
|
writeFileSync as writeFileSync2
|
|
10343
10416
|
} from "fs";
|
|
10344
10417
|
import { tmpdir as tmpdir2 } from "os";
|
|
10345
|
-
import { join as
|
|
10418
|
+
import { join as join13, resolve as resolve6, sep as sep2 } from "path";
|
|
10346
10419
|
import { crc32 } from "zlib";
|
|
10347
10420
|
function syncDir() {
|
|
10348
10421
|
const dir = process.env.MU_SYNC_DIR;
|
|
@@ -10366,7 +10439,7 @@ function localMachineId(db) {
|
|
|
10366
10439
|
return row2.machine_id;
|
|
10367
10440
|
}
|
|
10368
10441
|
function segmentPath(dir, machineId2) {
|
|
10369
|
-
return
|
|
10442
|
+
return join13(dir, `${machineId2}${SEGMENT_EXT}`);
|
|
10370
10443
|
}
|
|
10371
10444
|
function manifestPath(segment) {
|
|
10372
10445
|
return segment.replace(new RegExp(`${SEGMENT_EXT}$`), MANIFEST_EXT);
|
|
@@ -10510,7 +10583,7 @@ async function flushSegment(db, dir = syncDir()) {
|
|
|
10510
10583
|
const path2 = segmentPath(dir, machineId2);
|
|
10511
10584
|
const lockName = createHash2("sha256").update(`${dir}${machineId2}`).digest("hex").slice(0, 16);
|
|
10512
10585
|
return withFileLock(
|
|
10513
|
-
|
|
10586
|
+
join13(locksDir(), `segment-${lockName}.lock`),
|
|
10514
10587
|
`segment:${machineId2}`,
|
|
10515
10588
|
() => Promise.resolve(flushLocked(db, path2, machineId2))
|
|
10516
10589
|
);
|
|
@@ -10632,16 +10705,16 @@ function verifyAgainstManifest(segment) {
|
|
|
10632
10705
|
function discoverPeers(dir, selfMachineId) {
|
|
10633
10706
|
if (!existsSync14(dir)) return [];
|
|
10634
10707
|
const peers = [];
|
|
10635
|
-
for (const name of
|
|
10708
|
+
for (const name of readdirSync4(dir)) {
|
|
10636
10709
|
if (!name.endsWith(SEGMENT_EXT)) continue;
|
|
10637
10710
|
const stem = name.slice(0, -SEGMENT_EXT.length);
|
|
10638
10711
|
const conflictAt = stem.indexOf(".sync-conflict-");
|
|
10639
10712
|
const conflictCopy = conflictAt > 0;
|
|
10640
10713
|
const machineId2 = conflictCopy ? stem.slice(0, conflictAt) : stem;
|
|
10641
10714
|
if (machineId2 === selfMachineId && !conflictCopy) continue;
|
|
10642
|
-
const path2 =
|
|
10715
|
+
const path2 = join13(dir, name);
|
|
10643
10716
|
try {
|
|
10644
|
-
if (!
|
|
10717
|
+
if (!statSync3(path2).isFile()) continue;
|
|
10645
10718
|
} catch {
|
|
10646
10719
|
continue;
|
|
10647
10720
|
}
|
|
@@ -10846,7 +10919,7 @@ var init_segments = __esm({
|
|
|
10846
10919
|
});
|
|
10847
10920
|
|
|
10848
10921
|
// src/sync.ts
|
|
10849
|
-
import { existsSync as existsSync15, statSync as
|
|
10922
|
+
import { existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
10850
10923
|
import Database2 from "better-sqlite3";
|
|
10851
10924
|
function peerStatuses(db, dir) {
|
|
10852
10925
|
const self = localMachineId(db);
|
|
@@ -10854,7 +10927,7 @@ function peerStatuses(db, dir) {
|
|
|
10854
10927
|
const rows = discoverPeers(dir, self).map((peer) => {
|
|
10855
10928
|
let lastSeenMs = null;
|
|
10856
10929
|
try {
|
|
10857
|
-
lastSeenMs =
|
|
10930
|
+
lastSeenMs = statSync4(peer.path).mtimeMs;
|
|
10858
10931
|
} catch {
|
|
10859
10932
|
lastSeenMs = null;
|
|
10860
10933
|
}
|
|
@@ -11324,7 +11397,7 @@ function colorPair(pair) {
|
|
|
11324
11397
|
const text2 = formatPair(pair);
|
|
11325
11398
|
switch (pair.status) {
|
|
11326
11399
|
case "OPEN":
|
|
11327
|
-
return pair.substate === "parked" ? pc.dim(text2) : pc.cyan(text2);
|
|
11400
|
+
return pair.substate === "parked" || pair.substate === "triage" ? pc.dim(text2) : pc.cyan(text2);
|
|
11328
11401
|
case "IN_PROGRESS":
|
|
11329
11402
|
return pc.yellow(text2);
|
|
11330
11403
|
case "CLOSED":
|
|
@@ -11344,7 +11417,7 @@ function colorStatus(status) {
|
|
|
11344
11417
|
function inkColorForPair(pair) {
|
|
11345
11418
|
switch (pair.status) {
|
|
11346
11419
|
case "OPEN":
|
|
11347
|
-
return pair.substate === "parked" ? "gray" : "cyan";
|
|
11420
|
+
return pair.substate === "parked" || pair.substate === "triage" ? "gray" : "cyan";
|
|
11348
11421
|
case "IN_PROGRESS":
|
|
11349
11422
|
return "yellow";
|
|
11350
11423
|
case "CLOSED":
|
|
@@ -16537,6 +16610,7 @@ function yankCommandForTask2(t, ws) {
|
|
|
16537
16610
|
switch (t.status) {
|
|
16538
16611
|
case "OPEN":
|
|
16539
16612
|
if (t.substate === "parked") return `mu task unpark ${t.name} -w ${ws}`;
|
|
16613
|
+
if (t.substate === "triage") return `mu task accept ${t.name} -w ${ws}`;
|
|
16540
16614
|
return t.ownerName === null ? `mu task claim ${t.name} -w ${ws}` : `mu task release ${t.name} -w ${ws}`;
|
|
16541
16615
|
case "IN_PROGRESS":
|
|
16542
16616
|
return `mu task close ${t.name} -w ${ws} --evidence "..."`;
|
|
@@ -17596,6 +17670,7 @@ function snapshotKey(s) {
|
|
|
17596
17670
|
blocked: s.blocked.map(taskKey2),
|
|
17597
17671
|
recentClosed: s.recentClosed.map(taskKey2),
|
|
17598
17672
|
parkedCount: s.parkedCount,
|
|
17673
|
+
triage: s.triage.map(taskKey2),
|
|
17599
17674
|
allTasks: s.allTasks.map(taskKey2),
|
|
17600
17675
|
commitsBackend: s.commitsBackend ?? null,
|
|
17601
17676
|
recentCommits: s.recentCommits.map((c) => [c.sha, c.subject, c.author, c.relTime]),
|
|
@@ -18769,7 +18844,8 @@ async function cmdTaskAdd(db, localId, opts) {
|
|
|
18769
18844
|
title: opts.title,
|
|
18770
18845
|
impact: opts.impact,
|
|
18771
18846
|
effortDays: opts.effortDays,
|
|
18772
|
-
...hasBlockers ? { blockedBy } : {}
|
|
18847
|
+
...hasBlockers ? { blockedBy } : {},
|
|
18848
|
+
...opts.triage === true ? { triage: true } : {}
|
|
18773
18849
|
});
|
|
18774
18850
|
const createdNote = opts.note !== void 0 ? addNote(db, createdTask.name, unescapeNoteText(opts.note), {
|
|
18775
18851
|
author: initialNoteAuthor,
|
|
@@ -18793,7 +18869,7 @@ async function cmdTaskAdd(db, localId, opts) {
|
|
|
18793
18869
|
intent: "Add a blocker",
|
|
18794
18870
|
command: `mu task block ${task.name} --by <other-id> -w ${workstream}`
|
|
18795
18871
|
},
|
|
18796
|
-
{
|
|
18872
|
+
opts.triage === true ? { intent: "Accept it as work", command: `mu task accept ${task.name} -w ${workstream}` } : {
|
|
18797
18873
|
intent: "Claim and start",
|
|
18798
18874
|
command: `mu task claim ${task.name} -w ${workstream} --self (or --for <worker>)`
|
|
18799
18875
|
}
|
|
@@ -18916,6 +18992,12 @@ async function cmdTaskShow(db, rawId, opts = {}) {
|
|
|
18916
18992
|
if (task.substate === "parked") {
|
|
18917
18993
|
console.log("");
|
|
18918
18994
|
printNextSteps([{ intent: "Return it to next", command: `mu task unpark ${ref}` }]);
|
|
18995
|
+
} else if (task.substate === "triage") {
|
|
18996
|
+
console.log("");
|
|
18997
|
+
printNextSteps([
|
|
18998
|
+
{ intent: "Accept it as work", command: `mu task accept ${ref}` },
|
|
18999
|
+
{ intent: "Decline it", command: `mu task close ${ref} --as rejected --why "<reason>"` }
|
|
19000
|
+
]);
|
|
18919
19001
|
} else if (task.status === "CLOSED" && task.substate !== "done") {
|
|
18920
19002
|
console.log("");
|
|
18921
19003
|
printNextSteps([{ intent: "Reopen it", command: `mu task open ${ref}` }]);
|
|
@@ -19040,6 +19122,17 @@ async function cmdTaskList(db, opts) {
|
|
|
19040
19122
|
const basis = relTimeBasisForSort(sortKey);
|
|
19041
19123
|
if (basis !== null) tableOpts.relTimeBasis = basis;
|
|
19042
19124
|
console.log(formatTaskListTable(tasks, tableOpts));
|
|
19125
|
+
const first = tasks.find((t) => t.status === "OPEN" && t.substate === "triage");
|
|
19126
|
+
if (first !== void 0) {
|
|
19127
|
+
printNextSteps([
|
|
19128
|
+
{ intent: "Accept as work", command: `mu task accept ${first.name} -w ${workstream}` },
|
|
19129
|
+
{
|
|
19130
|
+
intent: "Decline",
|
|
19131
|
+
command: `mu task close ${first.name} --as rejected|duplicate --why "<reason>" -w ${workstream}`
|
|
19132
|
+
},
|
|
19133
|
+
{ intent: "Read the evidence", command: `mu task notes ${first.name} -w ${workstream}` }
|
|
19134
|
+
]);
|
|
19135
|
+
}
|
|
19043
19136
|
}
|
|
19044
19137
|
async function cmdTaskNext(db, opts) {
|
|
19045
19138
|
const workstream = await resolveWorkstream(opts.workstream);
|
|
@@ -19060,16 +19153,26 @@ async function cmdTaskNext(db, opts) {
|
|
|
19060
19153
|
}
|
|
19061
19154
|
if (tasks.length === 0) {
|
|
19062
19155
|
const parked = listTasks(db, workstream, { substate: "parked" }).length;
|
|
19063
|
-
|
|
19156
|
+
const triage = listTasks(db, workstream, { substate: "triage" }).length;
|
|
19157
|
+
if (parked === 0 && triage === 0) {
|
|
19064
19158
|
console.log(pc.dim("(no ready tasks)"));
|
|
19065
19159
|
return;
|
|
19066
19160
|
}
|
|
19067
|
-
|
|
19161
|
+
const held = [parked > 0 ? `${parked} parked` : "", triage > 0 ? `${triage} in triage` : ""].filter(Boolean).join(", ");
|
|
19162
|
+
console.log(pc.dim(`(no ready tasks; ${held})`));
|
|
19068
19163
|
printNextSteps([
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19164
|
+
...triage > 0 ? [
|
|
19165
|
+
{
|
|
19166
|
+
intent: "Triage findings",
|
|
19167
|
+
command: `mu task list --substate triage -w ${workstream}`
|
|
19168
|
+
}
|
|
19169
|
+
] : [],
|
|
19170
|
+
...parked > 0 ? [
|
|
19171
|
+
{
|
|
19172
|
+
intent: "List parked tasks",
|
|
19173
|
+
command: `mu task list --substate parked -w ${workstream}`
|
|
19174
|
+
}
|
|
19175
|
+
] : []
|
|
19073
19176
|
]);
|
|
19074
19177
|
return;
|
|
19075
19178
|
}
|
|
@@ -19638,7 +19741,14 @@ async function cmdTaskClose(db, rawId, opts = {}) {
|
|
|
19638
19741
|
const taskRow = getTask(db, localId, ws);
|
|
19639
19742
|
const r = closeTask(db, localId, sdkOpts);
|
|
19640
19743
|
if ("skipped" in r) {
|
|
19744
|
+
const inTriage = r.blockingIds.filter((id) => getTask(db, id, ws)?.substate === "triage");
|
|
19641
19745
|
const blockingNextSteps = [
|
|
19746
|
+
...inTriage.length > 0 ? [
|
|
19747
|
+
{
|
|
19748
|
+
intent: `Triage ${inTriage.length} blocking finding(s) (accept, or close --as rejected|duplicate)`,
|
|
19749
|
+
command: `mu task list --substate triage -w ${ws}`
|
|
19750
|
+
}
|
|
19751
|
+
] : [],
|
|
19642
19752
|
{
|
|
19643
19753
|
intent: "Watch the remaining blockers (returns when one closes)",
|
|
19644
19754
|
command: `mu task wait ${r.blockingIds.join(" ")} -w ${ws} --first --any --on-stall exit`
|
|
@@ -19771,6 +19881,32 @@ async function cmdTaskPark(db, rawId, opts) {
|
|
|
19771
19881
|
}
|
|
19772
19882
|
printNextSteps(nextSteps3);
|
|
19773
19883
|
}
|
|
19884
|
+
async function cmdTaskAccept(db, rawId, opts = {}) {
|
|
19885
|
+
const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
|
|
19886
|
+
assertTaskInWorkstream(db, localId, opts.workstream);
|
|
19887
|
+
const ws = await resolveWorkstream(opts.workstream);
|
|
19888
|
+
const sdkOpts = { workstream: ws };
|
|
19889
|
+
if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
|
|
19890
|
+
const r = acceptTask(db, localId, sdkOpts);
|
|
19891
|
+
const nextSteps3 = [
|
|
19892
|
+
{
|
|
19893
|
+
intent: "Claim it",
|
|
19894
|
+
command: `mu task claim ${localId} -w ${ws} (--self / --for <worker>)`
|
|
19895
|
+
},
|
|
19896
|
+
{ intent: "Triage the next one", command: `mu task list --substate triage -w ${ws}` }
|
|
19897
|
+
];
|
|
19898
|
+
if (opts.json) {
|
|
19899
|
+
emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
|
|
19900
|
+
return;
|
|
19901
|
+
}
|
|
19902
|
+
if (!r.changed) {
|
|
19903
|
+
const pair = formatPair({ status: r.status, substate: r.substate });
|
|
19904
|
+
console.log(pc.dim(`${localId} is ${pair}, not in triage (no-op)`));
|
|
19905
|
+
} else {
|
|
19906
|
+
console.log(`Accepted ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
|
|
19907
|
+
}
|
|
19908
|
+
printNextSteps(nextSteps3);
|
|
19909
|
+
}
|
|
19774
19910
|
async function cmdTaskUnpark(db, rawId, opts = {}) {
|
|
19775
19911
|
const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
|
|
19776
19912
|
assertTaskInWorkstream(db, localId, opts.workstream);
|
|
@@ -19849,7 +19985,10 @@ function wireTaskCommands(program) {
|
|
|
19849
19985
|
).requiredOption("-t, --title <title>", "task title").requiredOption("-i, --impact <n>", "impact 1..100", parseImpact).requiredOption("-e, --effort-days <days>", "effort in days (>0)", parsePositiveNumber).option(
|
|
19850
19986
|
"-b, --blocked-by <ids...>",
|
|
19851
19987
|
"task ids that block this one (repeat or comma-separate; or both)"
|
|
19852
|
-
).option("--note <text>", "append an initial note after creating the task").option("--note-author <name>", "author label for --note (default: current actor)").option(
|
|
19988
|
+
).option("--note <text>", "append an initial note after creating the task").option("--note-author <name>", "author label for --note (default: current actor)").option(
|
|
19989
|
+
"--triage",
|
|
19990
|
+
"start as OPEN/triage: a proposal (e.g. a review finding) kept out of next/claim until `mu task accept`"
|
|
19991
|
+
).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(id) {
|
|
19853
19992
|
const opts = this.opts();
|
|
19854
19993
|
return handle((db) => cmdTaskAdd(db, id, opts), this)();
|
|
19855
19994
|
});
|
|
@@ -19954,6 +20093,12 @@ function wireTaskCommands(program) {
|
|
|
19954
20093
|
const opts = this.opts();
|
|
19955
20094
|
return handle((db) => cmdTaskPark(db, id, opts), this)();
|
|
19956
20095
|
});
|
|
20096
|
+
task.command("accept <id>").description(
|
|
20097
|
+
"Accept an OPEN/triage task (a proposed finding) as work: OPEN/todo, back in `next`. No-op on any other state. Decline with `close --as rejected|duplicate --why`."
|
|
20098
|
+
).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
|
|
20099
|
+
const opts = this.opts();
|
|
20100
|
+
return handle((db) => cmdTaskAccept(db, id, opts), this)();
|
|
20101
|
+
});
|
|
19957
20102
|
task.command("unpark <id>").description("Return a parked task to OPEN (back in `next`). No-op on any other state.").option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
|
|
19958
20103
|
const opts = this.opts();
|
|
19959
20104
|
return handle((db) => cmdTaskUnpark(db, id, opts), this)();
|
|
@@ -19978,7 +20123,7 @@ function wireTaskCommands(program) {
|
|
|
19978
20123
|
).option(
|
|
19979
20124
|
"--actor <name>",
|
|
19980
20125
|
"override the actor name used for the log (only valid with --self; defaults to pane title or $USER)"
|
|
19981
|
-
).option("--force", "claim even when the task is parked (OPEN/parked)").option(
|
|
20126
|
+
).option("--force", "claim even when the task is parked or in triage (OPEN/parked, OPEN/triage)").option(
|
|
19982
20127
|
"--strict-staleness",
|
|
19983
20128
|
"refuse --for dispatch when the target agent's workspace is stale (default: warn and proceed)"
|
|
19984
20129
|
).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(taskId) {
|
|
@@ -20541,7 +20686,8 @@ async function cmdAgentWait(db, rawNames, opts) {
|
|
|
20541
20686
|
const settled = ctlRequest(sock, { op: "wait", afterRuns: probe.status.runs }, { signal }).then(
|
|
20542
20687
|
(reply) => reply.ok && reply.state !== void 0 ? {
|
|
20543
20688
|
status: ctlRuntimeState(reply.state),
|
|
20544
|
-
...reply.lastText !== void 0 ? { lastText: reply.lastText } : {}
|
|
20689
|
+
...reply.lastText !== void 0 ? { lastText: reply.lastText } : {},
|
|
20690
|
+
...reply.lastError !== void 0 ? { lastError: reply.lastError } : {}
|
|
20545
20691
|
} : { status: "unknown", unknownReason: reply.ok ? "ctl wait: no state" : reply.error },
|
|
20546
20692
|
// The connection dropped. A socket that still answers (pi's /new
|
|
20547
20693
|
// restarts the session) hands back to polling; one that does not
|
|
@@ -20750,124 +20896,424 @@ function wireSelfCommands(program) {
|
|
|
20750
20896
|
}
|
|
20751
20897
|
|
|
20752
20898
|
// src/cli/db.ts
|
|
20753
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync6 } from "fs";
|
|
20899
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, statSync as statSync5 } from "fs";
|
|
20754
20900
|
import { dirname as dirname13, resolve as resolve7 } from "path";
|
|
20755
|
-
init_output();
|
|
20756
|
-
|
|
20757
|
-
// src/cli/handle.ts
|
|
20758
|
-
init_agents();
|
|
20759
|
-
init_db();
|
|
20760
|
-
init_drift();
|
|
20761
|
-
init_link();
|
|
20762
|
-
init_logs();
|
|
20763
|
-
init_mux2();
|
|
20764
|
-
init_output();
|
|
20765
|
-
init_rebuild();
|
|
20766
|
-
init_sync();
|
|
20767
|
-
init_tasks();
|
|
20768
|
-
import { CommanderError } from "commander";
|
|
20769
20901
|
|
|
20770
|
-
// src/
|
|
20771
|
-
init_apply();
|
|
20772
|
-
init_db();
|
|
20902
|
+
// src/compact.ts
|
|
20773
20903
|
init_legacy_ops();
|
|
20774
|
-
|
|
20775
|
-
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
20780
|
-
|
|
20781
|
-
|
|
20782
|
-
|
|
20783
|
-
|
|
20784
|
-
|
|
20785
|
-
|
|
20786
|
-
|
|
20787
|
-
|
|
20788
|
-
|
|
20789
|
-
|
|
20790
|
-
}
|
|
20791
|
-
|
|
20792
|
-
|
|
20793
|
-
|
|
20794
|
-
|
|
20904
|
+
var REDUNDANT_TOMBSTONES_SQL = `
|
|
20905
|
+
SELECT d.seq AS seq, length(d.payload) AS bytes
|
|
20906
|
+
FROM ops d
|
|
20907
|
+
WHERE d.entity = 'note' AND d.op = 'del' AND d.payload <> '{}'
|
|
20908
|
+
AND EXISTS (SELECT 1 FROM ops p
|
|
20909
|
+
WHERE p.entity = 'note' AND p.key = d.key AND p.op = 'put')`;
|
|
20910
|
+
function planCompact(db) {
|
|
20911
|
+
const r = db.prepare(
|
|
20912
|
+
`SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS b FROM (${REDUNDANT_TOMBSTONES_SQL})`
|
|
20913
|
+
).get();
|
|
20914
|
+
return { tombstones: r.n, bytes: r.b };
|
|
20915
|
+
}
|
|
20916
|
+
function compact(db) {
|
|
20917
|
+
const plan = planCompact(db);
|
|
20918
|
+
db.transaction(() => {
|
|
20919
|
+
db.prepare(
|
|
20920
|
+
`UPDATE ops SET payload = '{}' WHERE seq IN (SELECT seq FROM (${REDUNDANT_TOMBSTONES_SQL}))`
|
|
20921
|
+
).run();
|
|
20922
|
+
})();
|
|
20923
|
+
return plan;
|
|
20924
|
+
}
|
|
20925
|
+
var WORKSTREAM_OPS_WHERE = `(key = @ws OR substr(key, 1, length(@ws) + 1) = @ws || '/')`;
|
|
20926
|
+
function listForgetCandidates(db) {
|
|
20927
|
+
const spellings = intentSpellings("workstream.teardown");
|
|
20928
|
+
const names = db.prepare(
|
|
20929
|
+
`SELECT key AS name, MAX(created_at) AS at
|
|
20930
|
+
FROM ops
|
|
20931
|
+
WHERE entity = 'workstream' AND op = 'del'
|
|
20932
|
+
AND intent IN (${spellings.map(() => "?").join(", ")})
|
|
20933
|
+
AND key NOT IN (SELECT name FROM workstreams)
|
|
20934
|
+
GROUP BY key`
|
|
20935
|
+
).all(...spellings);
|
|
20936
|
+
const cost = db.prepare(
|
|
20937
|
+
`SELECT COUNT(*) AS n, COALESCE(SUM(length(payload)), 0) AS b FROM ops WHERE ${WORKSTREAM_OPS_WHERE}`
|
|
20938
|
+
);
|
|
20939
|
+
return names.map((r) => {
|
|
20940
|
+
const c = cost.get({ ws: r.name });
|
|
20941
|
+
return { name: r.name, tornDownAt: r.at, ops: c.n, bytes: c.b };
|
|
20942
|
+
}).sort((a, b) => b.bytes - a.bytes);
|
|
20943
|
+
}
|
|
20944
|
+
function planForget(db, names) {
|
|
20945
|
+
const all = new Map(listForgetCandidates(db).map((c) => [c.name, c]));
|
|
20946
|
+
const live = new Set(
|
|
20947
|
+
db.prepare("SELECT name FROM workstreams").all().map((r) => r.name)
|
|
20948
|
+
);
|
|
20949
|
+
const plan = { candidates: [], refused: [] };
|
|
20950
|
+
for (const name of new Set(names)) {
|
|
20951
|
+
const c = all.get(name);
|
|
20952
|
+
if (c) plan.candidates.push(c);
|
|
20953
|
+
else plan.refused.push({ name, why: live.has(name) ? "live" : "never-torn-down" });
|
|
20795
20954
|
}
|
|
20796
|
-
|
|
20797
|
-
|
|
20955
|
+
return plan;
|
|
20956
|
+
}
|
|
20957
|
+
function forget(db, plan) {
|
|
20958
|
+
const del = db.prepare(`DELETE FROM ops WHERE ${WORKSTREAM_OPS_WHERE}`);
|
|
20959
|
+
let ops = 0;
|
|
20960
|
+
db.transaction(() => {
|
|
20961
|
+
for (const c of plan.candidates) ops += del.run({ ws: c.name }).changes;
|
|
20962
|
+
})();
|
|
20963
|
+
return { ops };
|
|
20964
|
+
}
|
|
20965
|
+
|
|
20966
|
+
// src/disk-recon.ts
|
|
20967
|
+
init_db();
|
|
20968
|
+
init_workspace();
|
|
20969
|
+
import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
20970
|
+
import { join as join12 } from "path";
|
|
20971
|
+
var STALE_LOCK_REPORT_MS = 60 * 60 * 1e3;
|
|
20972
|
+
function formatBytes(bytes) {
|
|
20973
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
20974
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;
|
|
20975
|
+
if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / 1024 / 1024)}M`;
|
|
20976
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
|
|
20977
|
+
}
|
|
20978
|
+
function dirNames(path2) {
|
|
20979
|
+
try {
|
|
20980
|
+
return readdirSync3(path2, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
20981
|
+
} catch {
|
|
20982
|
+
return [];
|
|
20798
20983
|
}
|
|
20799
|
-
}
|
|
20800
|
-
|
|
20801
|
-
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
this.groupId = groupId;
|
|
20807
|
-
this.conflicts = conflicts;
|
|
20808
|
-
this.name = "UndoSupersededError";
|
|
20984
|
+
}
|
|
20985
|
+
function exists(path2) {
|
|
20986
|
+
try {
|
|
20987
|
+
statSync2(path2);
|
|
20988
|
+
return true;
|
|
20989
|
+
} catch {
|
|
20990
|
+
return false;
|
|
20809
20991
|
}
|
|
20810
|
-
|
|
20811
|
-
|
|
20812
|
-
|
|
20813
|
-
return
|
|
20814
|
-
|
|
20815
|
-
|
|
20816
|
-
command: `mu log --group ${this.groupId.slice(0, 8)}`
|
|
20817
|
-
},
|
|
20818
|
-
{
|
|
20819
|
-
intent: "Undo anyway, discarding the newer edits",
|
|
20820
|
-
command: `mu undo ${this.groupId.slice(0, 8)} --yes --force`
|
|
20821
|
-
}
|
|
20822
|
-
];
|
|
20992
|
+
}
|
|
20993
|
+
function sizeOf(path2) {
|
|
20994
|
+
try {
|
|
20995
|
+
return statSync2(path2).size;
|
|
20996
|
+
} catch {
|
|
20997
|
+
return 0;
|
|
20823
20998
|
}
|
|
20824
|
-
};
|
|
20825
|
-
var ENTITY_RESTORE_ORDER = {
|
|
20826
|
-
workstream: 0,
|
|
20827
|
-
task: 1,
|
|
20828
|
-
note: 2,
|
|
20829
|
-
edge: 2
|
|
20830
|
-
};
|
|
20831
|
-
function restoreRank(entity) {
|
|
20832
|
-
return ENTITY_RESTORE_ORDER[entity] ?? 3;
|
|
20833
20999
|
}
|
|
20834
|
-
function
|
|
20835
|
-
|
|
20836
|
-
|
|
20837
|
-
|
|
20838
|
-
|
|
20839
|
-
|
|
20840
|
-
AND op = 'put'
|
|
20841
|
-
AND hlc < @hlc
|
|
20842
|
-
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
20843
|
-
AND json_type(payload, '$.' || @field) IS NOT NULL
|
|
20844
|
-
ORDER BY hlc DESC
|
|
20845
|
-
LIMIT 1`
|
|
20846
|
-
).get({ entity, key, hlc, field });
|
|
20847
|
-
if (row2 === void 0) return { found: false };
|
|
20848
|
-
return { found: true, value: row2.value };
|
|
21000
|
+
function findMissingWorkspaceDirs(db) {
|
|
21001
|
+
return listWorkspaces2(db).filter((row2) => !exists(row2.path)).map((row2) => ({
|
|
21002
|
+
agentName: row2.agentName,
|
|
21003
|
+
workstreamName: row2.workstreamName,
|
|
21004
|
+
path: row2.path
|
|
21005
|
+
}));
|
|
20849
21006
|
}
|
|
20850
|
-
function
|
|
20851
|
-
|
|
20852
|
-
|
|
20853
|
-
|
|
20854
|
-
|
|
20855
|
-
|
|
20856
|
-
|
|
20857
|
-
|
|
20858
|
-
|
|
20859
|
-
|
|
20860
|
-
|
|
20861
|
-
|
|
21007
|
+
function checkMissingWorkspaceDirs(db) {
|
|
21008
|
+
const missing = findMissingWorkspaceDirs(db);
|
|
21009
|
+
if (missing.length === 0) {
|
|
21010
|
+
return { name: "ws-rows", severity: "ok", detail: "every workspace row has its dir" };
|
|
21011
|
+
}
|
|
21012
|
+
return {
|
|
21013
|
+
name: "ws-rows",
|
|
21014
|
+
severity: "warn",
|
|
21015
|
+
detail: `${missing.length} workspace row(s) point at a path that is gone`,
|
|
21016
|
+
remediation: [
|
|
21017
|
+
"These vcs_workspaces rows name a directory that no longer exists:",
|
|
21018
|
+
...missing.map((m) => ` ${m.workstreamName}/${m.agentName} ${m.path}`),
|
|
21019
|
+
"",
|
|
21020
|
+
"mu reports such a row as a healthy workspace on every read surface, and",
|
|
21021
|
+
"the next send or refresh against it fails inside the VCS backend rather",
|
|
21022
|
+
"than at the row that lied. Unregister them:",
|
|
21023
|
+
...missing.map((m) => ` mu workspace free ${m.agentName} -w ${m.workstreamName}`)
|
|
21024
|
+
]
|
|
21025
|
+
};
|
|
20862
21026
|
}
|
|
20863
|
-
function
|
|
20864
|
-
|
|
20865
|
-
|
|
20866
|
-
|
|
20867
|
-
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
21027
|
+
function checkWorkspaceOrphanDirs(db) {
|
|
21028
|
+
const orphans = listAllOrphanWorkspaces(db);
|
|
21029
|
+
if (orphans.length === 0) {
|
|
21030
|
+
return { name: "ws-dirs", severity: "ok", detail: "no orphan workspace dirs" };
|
|
21031
|
+
}
|
|
21032
|
+
const stranded = orphans.filter((o) => o.stranded);
|
|
21033
|
+
const strandedNote = stranded.length > 0 ? `, ${stranded.length} stranded` : "";
|
|
21034
|
+
return {
|
|
21035
|
+
name: "ws-dirs",
|
|
21036
|
+
severity: "warn",
|
|
21037
|
+
detail: `${orphans.length} workspace dir(s) with no DB row${strandedNote}`,
|
|
21038
|
+
remediation: [
|
|
21039
|
+
"These directories have no row in vcs_workspaces:",
|
|
21040
|
+
...orphans.map((o) => ` ${o.path}${o.stranded ? " (workstream row also gone)" : ""}`),
|
|
21041
|
+
"",
|
|
21042
|
+
"Each one blocks the next `--workspace` spawn for that agent name. mu will",
|
|
21043
|
+
"not remove them: they may hold uncommitted work, and a stranded dir is the",
|
|
21044
|
+
"only remaining copy of it. Inspect, then remove by hand:",
|
|
21045
|
+
" mu workspace orphans --all",
|
|
21046
|
+
...orphans.slice(0, 3).map((o) => ` rm -rf ${o.path}`)
|
|
21047
|
+
]
|
|
21048
|
+
};
|
|
21049
|
+
}
|
|
21050
|
+
function checkEmptyWorkstreamDirs() {
|
|
21051
|
+
const root = join12(defaultStateDir(), "workspaces");
|
|
21052
|
+
const empty = dirNames(root).filter((name) => dirNames(join12(root, name)).length === 0);
|
|
21053
|
+
if (empty.length === 0) {
|
|
21054
|
+
return { name: "ws-empty", severity: "ok", detail: "no empty workstream dirs" };
|
|
21055
|
+
}
|
|
21056
|
+
return {
|
|
21057
|
+
name: "ws-empty",
|
|
21058
|
+
severity: "ok",
|
|
21059
|
+
detail: `${empty.length} empty workstream dir(s): ${empty.slice(0, 4).join(", ")}`,
|
|
21060
|
+
remediation: [
|
|
21061
|
+
"These per-workstream workspace dirs hold no checkouts:",
|
|
21062
|
+
...empty.map((name) => ` ${join12(root, name)}`),
|
|
21063
|
+
"",
|
|
21064
|
+
"`mu workspace free` removes an agent's dir and its row but not the parent,",
|
|
21065
|
+
"so this is normal residue rather than a fault. Remove if you want the",
|
|
21066
|
+
"state dir to read cleanly:",
|
|
21067
|
+
...empty.slice(0, 3).map((name) => ` rmdir ${join12(root, name)}`)
|
|
21068
|
+
]
|
|
21069
|
+
};
|
|
21070
|
+
}
|
|
21071
|
+
function findStrayDbFiles(stateDir = defaultStateDir()) {
|
|
21072
|
+
const live = /* @__PURE__ */ new Set(["mu.db", "mu.db-wal", "mu.db-shm"]);
|
|
21073
|
+
let entries;
|
|
21074
|
+
try {
|
|
21075
|
+
entries = readdirSync3(stateDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
21076
|
+
} catch {
|
|
21077
|
+
return [];
|
|
21078
|
+
}
|
|
21079
|
+
return entries.filter((name) => name.startsWith("mu.db") && !live.has(name)).map((name) => ({ name, path: join12(stateDir, name), bytes: sizeOf(join12(stateDir, name)) })).sort((a, b) => b.bytes - a.bytes);
|
|
21080
|
+
}
|
|
21081
|
+
function checkStrayDbFiles() {
|
|
21082
|
+
const stateDir = defaultStateDir();
|
|
21083
|
+
const stray = findStrayDbFiles(stateDir);
|
|
21084
|
+
if (stray.length === 0) {
|
|
21085
|
+
return { name: "db-copies", severity: "ok", detail: "no stray DB copies" };
|
|
21086
|
+
}
|
|
21087
|
+
const total = stray.reduce((n, f) => n + f.bytes, 0);
|
|
21088
|
+
return {
|
|
21089
|
+
name: "db-copies",
|
|
21090
|
+
severity: "warn",
|
|
21091
|
+
detail: `${stray.length} stray DB file(s), ${formatBytes(total)} \u2014 nothing reads these`,
|
|
21092
|
+
remediation: [
|
|
21093
|
+
`Files matching mu.db* in ${stateDir} that are not the live WAL triple:`,
|
|
21094
|
+
...stray.map((f) => ` ${formatBytes(f.bytes).padStart(6)} ${f.name}`),
|
|
21095
|
+
"",
|
|
21096
|
+
`Live DB: ${defaultDbPath()}`,
|
|
21097
|
+
"",
|
|
21098
|
+
"These are hand-made copies and pre-upgrade saves. No mu code path reads",
|
|
21099
|
+
"them and none prunes them. Real disaster recovery is the ops log, so a",
|
|
21100
|
+
"copy is only ever a convenience:",
|
|
21101
|
+
" mu rebuild <file> # DR from the ops log, not from a copy",
|
|
21102
|
+
" mu db backup <file> # a fresh copy, if that is what you wanted"
|
|
21103
|
+
]
|
|
21104
|
+
};
|
|
21105
|
+
}
|
|
21106
|
+
function checkRemovedExportsDir() {
|
|
21107
|
+
const root = join12(defaultStateDir(), "exports");
|
|
21108
|
+
const entries = dirNames(root);
|
|
21109
|
+
if (entries.length === 0) {
|
|
21110
|
+
return { name: "exports", severity: "ok", detail: "no leftover export dirs" };
|
|
21111
|
+
}
|
|
21112
|
+
return {
|
|
21113
|
+
name: "exports",
|
|
21114
|
+
severity: "warn",
|
|
21115
|
+
detail: `${entries.length} export dir(s) from the removed \`mu workstream export\``,
|
|
21116
|
+
remediation: [
|
|
21117
|
+
`${root} holds ${entries.length} directory/ies.`,
|
|
21118
|
+
"",
|
|
21119
|
+
"`mu workstream export` and its markdown bucket were removed in 1.0; this",
|
|
21120
|
+
"is output from a version that still had it. Nothing in mu reads or prunes",
|
|
21121
|
+
"it. Keep anything you still want, then:",
|
|
21122
|
+
` rm -rf ${root}`
|
|
21123
|
+
]
|
|
21124
|
+
};
|
|
21125
|
+
}
|
|
21126
|
+
function checkStaleLocks(now = Date.now()) {
|
|
21127
|
+
const root = join12(defaultStateDir(), "locks");
|
|
21128
|
+
const stale = [];
|
|
21129
|
+
for (const name of dirNames(root)) {
|
|
21130
|
+
try {
|
|
21131
|
+
const ageMs2 = now - statSync2(join12(root, name)).mtimeMs;
|
|
21132
|
+
if (ageMs2 > STALE_LOCK_REPORT_MS) stale.push({ name, ageMs: ageMs2 });
|
|
21133
|
+
} catch {
|
|
21134
|
+
}
|
|
21135
|
+
}
|
|
21136
|
+
if (stale.length === 0) {
|
|
21137
|
+
return { name: "locks", severity: "ok", detail: "no stale lock dirs" };
|
|
21138
|
+
}
|
|
21139
|
+
return {
|
|
21140
|
+
name: "locks",
|
|
21141
|
+
severity: "warn",
|
|
21142
|
+
detail: `${stale.length} lock dir(s) older than 1h`,
|
|
21143
|
+
remediation: [
|
|
21144
|
+
"These advisory-lock dirs outlived the process that took them:",
|
|
21145
|
+
...stale.map((s) => ` ${s.name} (${Math.round(s.ageMs / 6e4)}m old)`),
|
|
21146
|
+
"",
|
|
21147
|
+
"Not a deadlock: withFileLock steals a lock older than 30s. But it only",
|
|
21148
|
+
"steals under contention, so these will sit here forever. They are evidence",
|
|
21149
|
+
"of a spawn or segment flush that died mid-critical-section.",
|
|
21150
|
+
` rm -rf ${root}/<name>`
|
|
21151
|
+
]
|
|
21152
|
+
};
|
|
21153
|
+
}
|
|
21154
|
+
function walkBytes(path2) {
|
|
21155
|
+
let total = 0;
|
|
21156
|
+
let entries;
|
|
21157
|
+
try {
|
|
21158
|
+
entries = readdirSync3(path2, { withFileTypes: true });
|
|
21159
|
+
} catch {
|
|
21160
|
+
return 0;
|
|
21161
|
+
}
|
|
21162
|
+
for (const entry of entries) {
|
|
21163
|
+
const child = join12(path2, entry.name);
|
|
21164
|
+
if (entry.isDirectory()) total += walkBytes(child);
|
|
21165
|
+
else if (entry.isFile()) total += sizeOf(child);
|
|
21166
|
+
}
|
|
21167
|
+
return total;
|
|
21168
|
+
}
|
|
21169
|
+
function measureWorkspaceUsage(db) {
|
|
21170
|
+
const root = join12(defaultStateDir(), "workspaces");
|
|
21171
|
+
const registered = new Set(listWorkspaces2(db).map((w) => w.path));
|
|
21172
|
+
const usage = [];
|
|
21173
|
+
for (const wsName of dirNames(root)) {
|
|
21174
|
+
const wsRoot = join12(root, wsName);
|
|
21175
|
+
for (const agentName of dirNames(wsRoot)) {
|
|
21176
|
+
const path2 = join12(wsRoot, agentName);
|
|
21177
|
+
usage.push({
|
|
21178
|
+
workstreamName: wsName,
|
|
21179
|
+
agentName,
|
|
21180
|
+
path: path2,
|
|
21181
|
+
bytes: walkBytes(path2),
|
|
21182
|
+
orphan: !registered.has(path2)
|
|
21183
|
+
});
|
|
21184
|
+
}
|
|
21185
|
+
}
|
|
21186
|
+
return usage.sort((a, b) => b.bytes - a.bytes);
|
|
21187
|
+
}
|
|
21188
|
+
function checkDiskRecon(db) {
|
|
21189
|
+
return [
|
|
21190
|
+
checkMissingWorkspaceDirs(db),
|
|
21191
|
+
checkWorkspaceOrphanDirs(db),
|
|
21192
|
+
checkEmptyWorkstreamDirs(),
|
|
21193
|
+
checkStrayDbFiles(),
|
|
21194
|
+
checkRemovedExportsDir(),
|
|
21195
|
+
checkStaleLocks()
|
|
21196
|
+
];
|
|
21197
|
+
}
|
|
21198
|
+
|
|
21199
|
+
// src/cli/db.ts
|
|
21200
|
+
init_drift();
|
|
21201
|
+
init_output();
|
|
21202
|
+
|
|
21203
|
+
// src/cli/handle.ts
|
|
21204
|
+
init_agents();
|
|
21205
|
+
init_db();
|
|
21206
|
+
init_drift();
|
|
21207
|
+
init_link();
|
|
21208
|
+
init_logs();
|
|
21209
|
+
init_mux2();
|
|
21210
|
+
init_output();
|
|
21211
|
+
init_rebuild();
|
|
21212
|
+
init_sync();
|
|
21213
|
+
init_tasks();
|
|
21214
|
+
import { CommanderError } from "commander";
|
|
21215
|
+
|
|
21216
|
+
// src/undo.ts
|
|
21217
|
+
init_apply();
|
|
21218
|
+
init_db();
|
|
21219
|
+
init_legacy_ops();
|
|
21220
|
+
init_logs();
|
|
21221
|
+
init_op_context();
|
|
21222
|
+
init_status();
|
|
21223
|
+
var UndoGroupNotFoundError = class extends Error {
|
|
21224
|
+
constructor(groupId) {
|
|
21225
|
+
super(`no ops found for group ${JSON.stringify(groupId)}`);
|
|
21226
|
+
this.groupId = groupId;
|
|
21227
|
+
this.name = "UndoGroupNotFoundError";
|
|
21228
|
+
}
|
|
21229
|
+
groupId;
|
|
21230
|
+
errorNextSteps() {
|
|
21231
|
+
return [
|
|
21232
|
+
{ intent: "List recent undoable groups", command: "mu undo" },
|
|
21233
|
+
{ intent: "Inspect a group's ops", command: "mu log --group <id>" }
|
|
21234
|
+
];
|
|
21235
|
+
}
|
|
21236
|
+
};
|
|
21237
|
+
var NothingToUndoError = class extends Error {
|
|
21238
|
+
constructor() {
|
|
21239
|
+
super("the ops log is empty; nothing to undo");
|
|
21240
|
+
this.name = "NothingToUndoError";
|
|
21241
|
+
}
|
|
21242
|
+
errorNextSteps() {
|
|
21243
|
+
return [{ intent: "Make a change first", command: "mu task add <id> -t <title>" }];
|
|
21244
|
+
}
|
|
21245
|
+
};
|
|
21246
|
+
var UndoSupersededError = class extends Error {
|
|
21247
|
+
constructor(groupId, conflicts) {
|
|
21248
|
+
const shown = conflicts.slice(0, 5).map((c) => `${c.key}.${c.field} (changed by ${c.groupId.slice(0, 8)})`).join(", ");
|
|
21249
|
+
super(
|
|
21250
|
+
`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.`
|
|
21251
|
+
);
|
|
21252
|
+
this.groupId = groupId;
|
|
21253
|
+
this.conflicts = conflicts;
|
|
21254
|
+
this.name = "UndoSupersededError";
|
|
21255
|
+
}
|
|
21256
|
+
groupId;
|
|
21257
|
+
conflicts;
|
|
21258
|
+
errorNextSteps() {
|
|
21259
|
+
return [
|
|
21260
|
+
{
|
|
21261
|
+
intent: "Inspect what changed since",
|
|
21262
|
+
command: `mu log --group ${this.groupId.slice(0, 8)}`
|
|
21263
|
+
},
|
|
21264
|
+
{
|
|
21265
|
+
intent: "Undo anyway, discarding the newer edits",
|
|
21266
|
+
command: `mu undo ${this.groupId.slice(0, 8)} --yes --force`
|
|
21267
|
+
}
|
|
21268
|
+
];
|
|
21269
|
+
}
|
|
21270
|
+
};
|
|
21271
|
+
var ENTITY_RESTORE_ORDER = {
|
|
21272
|
+
workstream: 0,
|
|
21273
|
+
task: 1,
|
|
21274
|
+
note: 2,
|
|
21275
|
+
edge: 2
|
|
21276
|
+
};
|
|
21277
|
+
function restoreRank(entity) {
|
|
21278
|
+
return ENTITY_RESTORE_ORDER[entity] ?? 3;
|
|
21279
|
+
}
|
|
21280
|
+
function priorFieldValue(db, entity, key, hlc, field) {
|
|
21281
|
+
const row2 = db.prepare(
|
|
21282
|
+
`SELECT json_extract(payload, '$.' || @field) AS value
|
|
21283
|
+
FROM ops
|
|
21284
|
+
WHERE entity = @entity
|
|
21285
|
+
AND key = @key
|
|
21286
|
+
AND op = 'put'
|
|
21287
|
+
AND hlc < @hlc
|
|
21288
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
21289
|
+
AND json_type(payload, '$.' || @field) IS NOT NULL
|
|
21290
|
+
ORDER BY hlc DESC
|
|
21291
|
+
LIMIT 1`
|
|
21292
|
+
).get({ entity, key, hlc, field });
|
|
21293
|
+
if (row2 === void 0) return { found: false };
|
|
21294
|
+
return { found: true, value: row2.value };
|
|
21295
|
+
}
|
|
21296
|
+
function laterWriters(db, entity, key, hlc, field, excludeGroup) {
|
|
21297
|
+
return db.prepare(
|
|
21298
|
+
`SELECT DISTINCT group_id AS groupId, intent
|
|
21299
|
+
FROM ops
|
|
21300
|
+
WHERE entity = @entity
|
|
21301
|
+
AND key = @key
|
|
21302
|
+
AND op = 'put'
|
|
21303
|
+
AND hlc > @hlc
|
|
21304
|
+
AND group_id <> @excludeGroup
|
|
21305
|
+
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
|
|
21306
|
+
AND json_type(payload, '$.' || @field) IS NOT NULL`
|
|
21307
|
+
).all({ entity, key, hlc, field, excludeGroup });
|
|
21308
|
+
}
|
|
21309
|
+
function laterRowWriters(db, entity, key, hlc, excludeGroup) {
|
|
21310
|
+
return db.prepare(
|
|
21311
|
+
`SELECT DISTINCT group_id AS groupId, intent
|
|
21312
|
+
FROM ops
|
|
21313
|
+
WHERE entity = @entity
|
|
21314
|
+
AND key = @key
|
|
21315
|
+
AND op = 'put'
|
|
21316
|
+
AND hlc > @hlc
|
|
20871
21317
|
AND group_id <> @excludeGroup
|
|
20872
21318
|
AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}`
|
|
20873
21319
|
).all({ entity, key, hlc, excludeGroup });
|
|
@@ -21395,7 +21841,7 @@ function classifyError(err) {
|
|
|
21395
21841
|
}
|
|
21396
21842
|
if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError || // Substate refusals (ts_4): a missing --why, a park from the wrong
|
|
21397
21843
|
// phase, a claim on a parked task, a pair outside TASK_SUBSTATES.
|
|
21398
|
-
err instanceof SubstateReasonRequiredError || err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof InvalidSubstateError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || 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 SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
|
|
21844
|
+
err instanceof SubstateReasonRequiredError || err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof TaskInTriageError || err instanceof InvalidSubstateError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || 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 SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
|
|
21399
21845
|
// the source DB: both are name/state collisions, not usage errors.
|
|
21400
21846
|
err instanceof RebuildTargetExistsError || err instanceof RebuildTargetIsSourceError || // An abbreviated group id matching several groups: the operator must
|
|
21401
21847
|
// disambiguate. Guessing would be catastrophic for `mu undo`.
|
|
@@ -21499,315 +21945,196 @@ function handle(fn, command, opts) {
|
|
|
21499
21945
|
if (ambient) await ambientIngest(db);
|
|
21500
21946
|
await fn(db);
|
|
21501
21947
|
} catch (err) {
|
|
21502
|
-
exitCode = emitError(err);
|
|
21503
|
-
} finally {
|
|
21504
|
-
activeCommand = void 0;
|
|
21505
|
-
if (ambient && db !== void 0) {
|
|
21506
|
-
try {
|
|
21507
|
-
await ambientFlush(db);
|
|
21508
|
-
} catch {
|
|
21509
|
-
}
|
|
21510
|
-
}
|
|
21511
|
-
try {
|
|
21512
|
-
db?.close();
|
|
21513
|
-
} catch {
|
|
21514
|
-
}
|
|
21515
|
-
}
|
|
21516
|
-
if (exitCode !== void 0) process.exit(exitCode);
|
|
21517
|
-
};
|
|
21518
|
-
}
|
|
21519
|
-
function emitParseError(err, failingCommand) {
|
|
21520
|
-
activeCommand = failingCommand;
|
|
21521
|
-
try {
|
|
21522
|
-
return emitError(err);
|
|
21523
|
-
} finally {
|
|
21524
|
-
activeCommand = void 0;
|
|
21525
|
-
}
|
|
21526
|
-
}
|
|
21527
|
-
function findCommandForArgv(root, argv) {
|
|
21528
|
-
let cur = root;
|
|
21529
|
-
for (const t of argv) {
|
|
21530
|
-
if (t.startsWith("-")) break;
|
|
21531
|
-
const next = cur.commands.find(
|
|
21532
|
-
(c) => c.name() === t || (c.aliases().includes(t) ?? false)
|
|
21533
|
-
);
|
|
21534
|
-
if (!next) break;
|
|
21535
|
-
cur = next;
|
|
21536
|
-
}
|
|
21537
|
-
return cur;
|
|
21538
|
-
}
|
|
21539
|
-
|
|
21540
|
-
// src/cli/db.ts
|
|
21541
|
-
function cmdDbBackup(db, file, opts = {}) {
|
|
21542
|
-
const target = resolve7(file);
|
|
21543
|
-
if (existsSync16(target)) {
|
|
21544
|
-
throw new UsageError(`${target} already exists: mu db backup never overwrites`);
|
|
21545
|
-
}
|
|
21546
|
-
mkdirSync6(dirname13(target), { recursive: true });
|
|
21547
|
-
db.prepare("VACUUM INTO ?").run(target);
|
|
21548
|
-
const nextSteps3 = [
|
|
21549
|
-
{
|
|
21550
|
-
intent: "Inspect the copy without touching the live DB",
|
|
21551
|
-
command: `MU_DB_PATH=${target} mu state`
|
|
21552
|
-
},
|
|
21553
|
-
{ intent: "Disaster recovery is a rebuild, not a copy", command: "mu rebuild <file>" }
|
|
21554
|
-
];
|
|
21555
|
-
if (opts.json === true) {
|
|
21556
|
-
emitJson({ path: target, nextSteps: nextSteps3 });
|
|
21557
|
-
return;
|
|
21558
|
-
}
|
|
21559
|
-
console.log(`Backed up to ${pc.bold(target)}`);
|
|
21560
|
-
printNextSteps(nextSteps3);
|
|
21561
|
-
}
|
|
21562
|
-
function wireDbCommands(program) {
|
|
21563
|
-
const dbCmd = program.command("db").description("Whole-DB file commands");
|
|
21564
|
-
dbCmd.command("backup <file>").description(
|
|
21565
|
-
"VACUUM INTO copy of the whole DB \u2014 the 'one file I can scp' convenience. Never overwrites. Real disaster recovery is `mu rebuild`."
|
|
21566
|
-
).option(...JSON_OPT).action(function(file) {
|
|
21567
|
-
const opts = this.opts();
|
|
21568
|
-
return handle(async (db) => cmdDbBackup(db, file, opts), this)();
|
|
21569
|
-
});
|
|
21570
|
-
}
|
|
21571
|
-
|
|
21572
|
-
// src/cli/doctor.ts
|
|
21573
|
-
init_agents();
|
|
21574
|
-
import { dirname as dirname14 } from "path";
|
|
21575
|
-
init_db();
|
|
21576
|
-
|
|
21577
|
-
// src/disk-recon.ts
|
|
21578
|
-
init_db();
|
|
21579
|
-
init_workspace();
|
|
21580
|
-
import { readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
21581
|
-
import { join as join13 } from "path";
|
|
21582
|
-
var STALE_LOCK_REPORT_MS = 60 * 60 * 1e3;
|
|
21583
|
-
function formatBytes(bytes) {
|
|
21584
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
21585
|
-
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;
|
|
21586
|
-
if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / 1024 / 1024)}M`;
|
|
21587
|
-
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
|
|
21588
|
-
}
|
|
21589
|
-
function dirNames(path2) {
|
|
21590
|
-
try {
|
|
21591
|
-
return readdirSync4(path2, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
21592
|
-
} catch {
|
|
21593
|
-
return [];
|
|
21594
|
-
}
|
|
21595
|
-
}
|
|
21596
|
-
function exists(path2) {
|
|
21597
|
-
try {
|
|
21598
|
-
statSync4(path2);
|
|
21599
|
-
return true;
|
|
21600
|
-
} catch {
|
|
21601
|
-
return false;
|
|
21602
|
-
}
|
|
21603
|
-
}
|
|
21604
|
-
function sizeOf(path2) {
|
|
21605
|
-
try {
|
|
21606
|
-
return statSync4(path2).size;
|
|
21607
|
-
} catch {
|
|
21608
|
-
return 0;
|
|
21609
|
-
}
|
|
21610
|
-
}
|
|
21611
|
-
function findMissingWorkspaceDirs(db) {
|
|
21612
|
-
return listWorkspaces2(db).filter((row2) => !exists(row2.path)).map((row2) => ({
|
|
21613
|
-
agentName: row2.agentName,
|
|
21614
|
-
workstreamName: row2.workstreamName,
|
|
21615
|
-
path: row2.path
|
|
21616
|
-
}));
|
|
21617
|
-
}
|
|
21618
|
-
function checkMissingWorkspaceDirs(db) {
|
|
21619
|
-
const missing = findMissingWorkspaceDirs(db);
|
|
21620
|
-
if (missing.length === 0) {
|
|
21621
|
-
return { name: "ws-rows", severity: "ok", detail: "every workspace row has its dir" };
|
|
21622
|
-
}
|
|
21623
|
-
return {
|
|
21624
|
-
name: "ws-rows",
|
|
21625
|
-
severity: "warn",
|
|
21626
|
-
detail: `${missing.length} workspace row(s) point at a path that is gone`,
|
|
21627
|
-
remediation: [
|
|
21628
|
-
"These vcs_workspaces rows name a directory that no longer exists:",
|
|
21629
|
-
...missing.map((m) => ` ${m.workstreamName}/${m.agentName} ${m.path}`),
|
|
21630
|
-
"",
|
|
21631
|
-
"mu reports such a row as a healthy workspace on every read surface, and",
|
|
21632
|
-
"the next send or refresh against it fails inside the VCS backend rather",
|
|
21633
|
-
"than at the row that lied. Unregister them:",
|
|
21634
|
-
...missing.map((m) => ` mu workspace free ${m.agentName} -w ${m.workstreamName}`)
|
|
21635
|
-
]
|
|
21948
|
+
exitCode = emitError(err);
|
|
21949
|
+
} finally {
|
|
21950
|
+
activeCommand = void 0;
|
|
21951
|
+
if (ambient && db !== void 0) {
|
|
21952
|
+
try {
|
|
21953
|
+
await ambientFlush(db);
|
|
21954
|
+
} catch {
|
|
21955
|
+
}
|
|
21956
|
+
}
|
|
21957
|
+
try {
|
|
21958
|
+
db?.close();
|
|
21959
|
+
} catch {
|
|
21960
|
+
}
|
|
21961
|
+
}
|
|
21962
|
+
if (exitCode !== void 0) process.exit(exitCode);
|
|
21636
21963
|
};
|
|
21637
21964
|
}
|
|
21638
|
-
function
|
|
21639
|
-
|
|
21640
|
-
|
|
21641
|
-
return
|
|
21965
|
+
function emitParseError(err, failingCommand) {
|
|
21966
|
+
activeCommand = failingCommand;
|
|
21967
|
+
try {
|
|
21968
|
+
return emitError(err);
|
|
21969
|
+
} finally {
|
|
21970
|
+
activeCommand = void 0;
|
|
21642
21971
|
}
|
|
21643
|
-
const stranded = orphans.filter((o) => o.stranded);
|
|
21644
|
-
const strandedNote = stranded.length > 0 ? `, ${stranded.length} stranded` : "";
|
|
21645
|
-
return {
|
|
21646
|
-
name: "ws-dirs",
|
|
21647
|
-
severity: "warn",
|
|
21648
|
-
detail: `${orphans.length} workspace dir(s) with no DB row${strandedNote}`,
|
|
21649
|
-
remediation: [
|
|
21650
|
-
"These directories have no row in vcs_workspaces:",
|
|
21651
|
-
...orphans.map((o) => ` ${o.path}${o.stranded ? " (workstream row also gone)" : ""}`),
|
|
21652
|
-
"",
|
|
21653
|
-
"Each one blocks the next `--workspace` spawn for that agent name. mu will",
|
|
21654
|
-
"not remove them: they may hold uncommitted work, and a stranded dir is the",
|
|
21655
|
-
"only remaining copy of it. Inspect, then remove by hand:",
|
|
21656
|
-
" mu workspace orphans --all",
|
|
21657
|
-
...orphans.slice(0, 3).map((o) => ` rm -rf ${o.path}`)
|
|
21658
|
-
]
|
|
21659
|
-
};
|
|
21660
21972
|
}
|
|
21661
|
-
function
|
|
21662
|
-
|
|
21663
|
-
|
|
21664
|
-
|
|
21665
|
-
|
|
21973
|
+
function findCommandForArgv(root, argv) {
|
|
21974
|
+
let cur = root;
|
|
21975
|
+
for (const t of argv) {
|
|
21976
|
+
if (t.startsWith("-")) break;
|
|
21977
|
+
const next = cur.commands.find(
|
|
21978
|
+
(c) => c.name() === t || (c.aliases().includes(t) ?? false)
|
|
21979
|
+
);
|
|
21980
|
+
if (!next) break;
|
|
21981
|
+
cur = next;
|
|
21666
21982
|
}
|
|
21667
|
-
return
|
|
21668
|
-
name: "ws-empty",
|
|
21669
|
-
severity: "ok",
|
|
21670
|
-
detail: `${empty.length} empty workstream dir(s): ${empty.slice(0, 4).join(", ")}`,
|
|
21671
|
-
remediation: [
|
|
21672
|
-
"These per-workstream workspace dirs hold no checkouts:",
|
|
21673
|
-
...empty.map((name) => ` ${join13(root, name)}`),
|
|
21674
|
-
"",
|
|
21675
|
-
"`mu workspace free` removes an agent's dir and its row but not the parent,",
|
|
21676
|
-
"so this is normal residue rather than a fault. Remove if you want the",
|
|
21677
|
-
"state dir to read cleanly:",
|
|
21678
|
-
...empty.slice(0, 3).map((name) => ` rmdir ${join13(root, name)}`)
|
|
21679
|
-
]
|
|
21680
|
-
};
|
|
21983
|
+
return cur;
|
|
21681
21984
|
}
|
|
21682
|
-
|
|
21683
|
-
|
|
21684
|
-
|
|
21685
|
-
|
|
21686
|
-
|
|
21687
|
-
|
|
21688
|
-
return [];
|
|
21985
|
+
|
|
21986
|
+
// src/cli/db.ts
|
|
21987
|
+
function cmdDbBackup(db, file, opts = {}) {
|
|
21988
|
+
const target = resolve7(file);
|
|
21989
|
+
if (existsSync16(target)) {
|
|
21990
|
+
throw new UsageError(`${target} already exists: mu db backup never overwrites`);
|
|
21689
21991
|
}
|
|
21690
|
-
|
|
21691
|
-
|
|
21692
|
-
|
|
21693
|
-
|
|
21694
|
-
|
|
21695
|
-
|
|
21696
|
-
|
|
21992
|
+
mkdirSync6(dirname13(target), { recursive: true });
|
|
21993
|
+
db.prepare("VACUUM INTO ?").run(target);
|
|
21994
|
+
const nextSteps3 = [
|
|
21995
|
+
{
|
|
21996
|
+
intent: "Inspect the copy without touching the live DB",
|
|
21997
|
+
command: `MU_DB_PATH=${target} mu state`
|
|
21998
|
+
},
|
|
21999
|
+
{ intent: "Disaster recovery is a rebuild, not a copy", command: "mu rebuild <file>" }
|
|
22000
|
+
];
|
|
22001
|
+
if (opts.json === true) {
|
|
22002
|
+
emitJson({ path: target, nextSteps: nextSteps3 });
|
|
22003
|
+
return;
|
|
21697
22004
|
}
|
|
21698
|
-
|
|
21699
|
-
|
|
21700
|
-
name: "db-copies",
|
|
21701
|
-
severity: "warn",
|
|
21702
|
-
detail: `${stray.length} stray DB file(s), ${formatBytes(total)} \u2014 nothing reads these`,
|
|
21703
|
-
remediation: [
|
|
21704
|
-
`Files matching mu.db* in ${stateDir} that are not the live WAL triple:`,
|
|
21705
|
-
...stray.map((f) => ` ${formatBytes(f.bytes).padStart(6)} ${f.name}`),
|
|
21706
|
-
"",
|
|
21707
|
-
`Live DB: ${defaultDbPath()}`,
|
|
21708
|
-
"",
|
|
21709
|
-
"These are hand-made copies and pre-upgrade saves. No mu code path reads",
|
|
21710
|
-
"them and none prunes them. Real disaster recovery is the ops log, so a",
|
|
21711
|
-
"copy is only ever a convenience:",
|
|
21712
|
-
" mu rebuild <file> # DR from the ops log, not from a copy",
|
|
21713
|
-
" mu db backup <file> # a fresh copy, if that is what you wanted"
|
|
21714
|
-
]
|
|
21715
|
-
};
|
|
22005
|
+
console.log(`Backed up to ${pc.bold(target)}`);
|
|
22006
|
+
printNextSteps(nextSteps3);
|
|
21716
22007
|
}
|
|
21717
|
-
function
|
|
21718
|
-
const
|
|
21719
|
-
const
|
|
21720
|
-
|
|
21721
|
-
|
|
22008
|
+
function backupBeside(db, verb) {
|
|
22009
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
22010
|
+
const target = `${db.name}.pre-${verb}-${ts}`;
|
|
22011
|
+
db.prepare("VACUUM INTO ?").run(target);
|
|
22012
|
+
return target;
|
|
22013
|
+
}
|
|
22014
|
+
function vacuumAndCheck(db, backup) {
|
|
22015
|
+
const before = statSync5(db.name).size;
|
|
22016
|
+
db.exec("VACUUM");
|
|
22017
|
+
const after = statSync5(db.name).size;
|
|
22018
|
+
const report = checkDrift(db);
|
|
22019
|
+
if (!report.clean) {
|
|
22020
|
+
console.error(
|
|
22021
|
+
pc.red(`drift after the rewrite (${report.totalDrift}); the backup is ${backup}`)
|
|
22022
|
+
);
|
|
22023
|
+
throw new DriftDetectedError(report.totalDrift, report.records);
|
|
21722
22024
|
}
|
|
21723
|
-
return {
|
|
21724
|
-
name: "exports",
|
|
21725
|
-
severity: "warn",
|
|
21726
|
-
detail: `${entries.length} export dir(s) from the removed \`mu workstream export\``,
|
|
21727
|
-
remediation: [
|
|
21728
|
-
`${root} holds ${entries.length} directory/ies.`,
|
|
21729
|
-
"",
|
|
21730
|
-
"`mu workstream export` and its markdown bucket were removed in 1.0; this",
|
|
21731
|
-
"is output from a version that still had it. Nothing in mu reads or prunes",
|
|
21732
|
-
"it. Keep anything you still want, then:",
|
|
21733
|
-
` rm -rf ${root}`
|
|
21734
|
-
]
|
|
21735
|
-
};
|
|
22025
|
+
return { before, after };
|
|
21736
22026
|
}
|
|
21737
|
-
function
|
|
21738
|
-
const
|
|
21739
|
-
|
|
21740
|
-
|
|
21741
|
-
|
|
21742
|
-
|
|
21743
|
-
|
|
21744
|
-
} catch {
|
|
22027
|
+
function cmdDbCompact(db, opts = {}) {
|
|
22028
|
+
const plan = planCompact(db);
|
|
22029
|
+
if (opts.yes !== true || plan.tombstones === 0) {
|
|
22030
|
+
const nextSteps3 = plan.tombstones === 0 ? [] : [{ intent: "Apply it", command: "mu db compact --yes" }];
|
|
22031
|
+
if (opts.json === true) {
|
|
22032
|
+
emitJson({ dryRun: true, ...plan, nextSteps: nextSteps3 });
|
|
22033
|
+
return;
|
|
21745
22034
|
}
|
|
22035
|
+
console.log(
|
|
22036
|
+
plan.tombstones === 0 ? pc.dim("nothing to compact: no redundant note tombstones") : `${plan.tombstones} note tombstones repeat a put already in the log (${formatBytes(plan.bytes)}). Dry run; nothing changed.`
|
|
22037
|
+
);
|
|
22038
|
+
printNextSteps(nextSteps3);
|
|
22039
|
+
return;
|
|
21746
22040
|
}
|
|
21747
|
-
|
|
21748
|
-
|
|
22041
|
+
const backup = backupBeside(db, "compact");
|
|
22042
|
+
compact(db);
|
|
22043
|
+
const size = vacuumAndCheck(db, backup);
|
|
22044
|
+
if (opts.json === true) {
|
|
22045
|
+
emitJson({ dryRun: false, ...plan, backup, ...size });
|
|
22046
|
+
return;
|
|
21749
22047
|
}
|
|
21750
|
-
|
|
21751
|
-
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
|
|
21759
|
-
|
|
21760
|
-
|
|
21761
|
-
|
|
21762
|
-
|
|
21763
|
-
|
|
21764
|
-
}
|
|
21765
|
-
|
|
21766
|
-
|
|
21767
|
-
|
|
21768
|
-
|
|
21769
|
-
|
|
21770
|
-
|
|
21771
|
-
|
|
22048
|
+
console.log(
|
|
22049
|
+
`Compacted ${plan.tombstones} note tombstones: ${formatBytes(size.before)} \u2192 ${formatBytes(size.after)}. Drift check clean.`
|
|
22050
|
+
);
|
|
22051
|
+
console.log(pc.dim(`backup: ${backup}`));
|
|
22052
|
+
}
|
|
22053
|
+
function printForgetTable(plan) {
|
|
22054
|
+
const t = muTable({ head: ["workstream", "torn down", "ops", "size"].map((h) => pc.bold(h)) });
|
|
22055
|
+
for (const c of plan.candidates)
|
|
22056
|
+
t.push([c.name, c.tornDownAt.slice(0, 10), String(c.ops), formatBytes(c.bytes)]);
|
|
22057
|
+
console.log(t.toString());
|
|
22058
|
+
}
|
|
22059
|
+
function cmdDbForget(db, names, opts = {}) {
|
|
22060
|
+
const plan = planForget(db, names);
|
|
22061
|
+
if (plan.refused.length > 0) {
|
|
22062
|
+
const why = plan.refused.map((r2) => `${r2.name} (${r2.why === "live" ? "is live" : "was never torn down"})`).join(", ");
|
|
22063
|
+
throw new UsageError(`cannot forget: ${why}. Only torn-down workstreams can be forgotten.`);
|
|
22064
|
+
}
|
|
22065
|
+
const ops = plan.candidates.reduce((n, c) => n + c.ops, 0);
|
|
22066
|
+
const bytes = plan.candidates.reduce((n, c) => n + c.bytes, 0);
|
|
22067
|
+
if (opts.yes !== true) {
|
|
22068
|
+
const nextSteps3 = [
|
|
22069
|
+
{ intent: "Forget them (NOT undoable)", command: `mu db forget ${names.join(" ")} --yes` }
|
|
22070
|
+
];
|
|
22071
|
+
if (opts.json === true) {
|
|
22072
|
+
emitJson({ dryRun: true, candidates: plan.candidates, ops, bytes, nextSteps: nextSteps3 });
|
|
22073
|
+
return;
|
|
22074
|
+
}
|
|
22075
|
+
printForgetTable(plan);
|
|
22076
|
+
console.log(
|
|
22077
|
+
`${ops} ops (${formatBytes(bytes)} of payload). Forgetting deletes their history: mu undo can no longer restore these teardowns. Dry run; nothing changed.`
|
|
22078
|
+
);
|
|
22079
|
+
printNextSteps(nextSteps3);
|
|
22080
|
+
return;
|
|
21772
22081
|
}
|
|
21773
|
-
|
|
21774
|
-
|
|
21775
|
-
|
|
21776
|
-
|
|
22082
|
+
const backup = backupBeside(db, "forget");
|
|
22083
|
+
const r = forget(db, plan);
|
|
22084
|
+
const size = vacuumAndCheck(db, backup);
|
|
22085
|
+
if (opts.json === true) {
|
|
22086
|
+
emitJson({
|
|
22087
|
+
dryRun: false,
|
|
22088
|
+
forgotten: plan.candidates.map((c) => c.name),
|
|
22089
|
+
ops: r.ops,
|
|
22090
|
+
backup,
|
|
22091
|
+
...size
|
|
22092
|
+
});
|
|
22093
|
+
return;
|
|
21777
22094
|
}
|
|
21778
|
-
|
|
22095
|
+
console.log(
|
|
22096
|
+
`Forgot ${plan.candidates.length} workstream(s), ${r.ops} ops: ${formatBytes(size.before)} \u2192 ${formatBytes(size.after)}. Drift check clean.`
|
|
22097
|
+
);
|
|
22098
|
+
console.log(pc.dim(`backup (the only way back): ${backup}`));
|
|
21779
22099
|
}
|
|
21780
|
-
function
|
|
21781
|
-
const
|
|
21782
|
-
const
|
|
21783
|
-
|
|
21784
|
-
|
|
21785
|
-
|
|
21786
|
-
|
|
21787
|
-
|
|
21788
|
-
|
|
21789
|
-
|
|
21790
|
-
agentName,
|
|
21791
|
-
path: path2,
|
|
21792
|
-
bytes: walkBytes(path2),
|
|
21793
|
-
orphan: !registered.has(path2)
|
|
21794
|
-
});
|
|
21795
|
-
}
|
|
21796
|
-
}
|
|
21797
|
-
return usage.sort((a, b) => b.bytes - a.bytes);
|
|
22100
|
+
function forgetHint(db) {
|
|
22101
|
+
const all = listForgetCandidates(db);
|
|
22102
|
+
const bytes = all.reduce((n, c) => n + c.bytes, 0);
|
|
22103
|
+
if (bytes < FORGET_HINT_MIN_BYTES) return void 0;
|
|
22104
|
+
return {
|
|
22105
|
+
total: all.length,
|
|
22106
|
+
ops: all.reduce((n, c) => n + c.ops, 0),
|
|
22107
|
+
bytes,
|
|
22108
|
+
top: all.slice(0, 3).map((c) => c.name)
|
|
22109
|
+
};
|
|
21798
22110
|
}
|
|
21799
|
-
|
|
21800
|
-
|
|
21801
|
-
|
|
21802
|
-
|
|
21803
|
-
|
|
21804
|
-
|
|
21805
|
-
|
|
21806
|
-
|
|
21807
|
-
|
|
22111
|
+
var FORGET_HINT_MIN_BYTES = 5 * 1024 * 1024;
|
|
22112
|
+
function wireDbCommands(program) {
|
|
22113
|
+
const dbCmd = program.command("db").description("Whole-DB file commands");
|
|
22114
|
+
dbCmd.command("backup <file>").description(
|
|
22115
|
+
"VACUUM INTO copy of the whole DB \u2014 the 'one file I can scp' convenience. Never overwrites. Real disaster recovery is `mu rebuild`."
|
|
22116
|
+
).option(...JSON_OPT).action(function(file) {
|
|
22117
|
+
const opts = this.opts();
|
|
22118
|
+
return handle(async (db) => cmdDbBackup(db, file, opts), this)();
|
|
22119
|
+
});
|
|
22120
|
+
dbCmd.command("compact").description(
|
|
22121
|
+
"Shrink mu.db: blank note tombstones that repeat a put already in the log, then VACUUM. Dry run without --yes; --yes backs up beside the DB first and runs the drift check after. Local only; sync segments are untouched."
|
|
22122
|
+
).option("--yes", "apply (default: dry run)").option(...JSON_OPT).action(function() {
|
|
22123
|
+
const opts = this.opts();
|
|
22124
|
+
return handle(async (db) => cmdDbCompact(db, opts), this)();
|
|
22125
|
+
});
|
|
22126
|
+
dbCmd.command("forget <workstreams...>").description(
|
|
22127
|
+
"Delete every op of torn-down workstreams to shrink mu.db. NOT undoable: mu undo can no longer restore them. Dry run without --yes; --yes backs up beside the DB first, VACUUMs, and runs the drift check. List candidates: mu workstream list --torn-down."
|
|
22128
|
+
).option("--yes", "apply (default: dry run)").option(...JSON_OPT).action(function(names) {
|
|
22129
|
+
const opts = this.opts();
|
|
22130
|
+
return handle(async (db) => cmdDbForget(db, names, opts), this)();
|
|
22131
|
+
});
|
|
21808
22132
|
}
|
|
21809
22133
|
|
|
21810
22134
|
// src/cli/doctor.ts
|
|
22135
|
+
init_agents();
|
|
22136
|
+
import { dirname as dirname14 } from "path";
|
|
22137
|
+
init_db();
|
|
21811
22138
|
init_doctor_summary();
|
|
21812
22139
|
|
|
21813
22140
|
// src/dormant.ts
|
|
@@ -22136,12 +22463,37 @@ state (workstream=${ws})`));
|
|
|
22136
22463
|
throw new DriftDetectedError(cheap.totalUnexplained, []);
|
|
22137
22464
|
}
|
|
22138
22465
|
}
|
|
22466
|
+
const size = opsSizeHint(db);
|
|
22467
|
+
if (size) {
|
|
22468
|
+
console.log(` size : ${pc.yellow("hint")} ${size.line}`);
|
|
22469
|
+
for (const c of size.commands) console.log(pc.dim(` ${c}`));
|
|
22470
|
+
}
|
|
22139
22471
|
if (sawHazard) {
|
|
22140
22472
|
console.log(
|
|
22141
22473
|
pc.dim("\nSee the fleet / disk sections above: at least one finding needs attention.")
|
|
22142
22474
|
);
|
|
22143
22475
|
}
|
|
22144
22476
|
}
|
|
22477
|
+
function opsSizeHint(db) {
|
|
22478
|
+
const forgettable = forgetHint(db);
|
|
22479
|
+
const tomb = planCompact(db);
|
|
22480
|
+
const parts = [];
|
|
22481
|
+
const commands = [];
|
|
22482
|
+
if (forgettable) {
|
|
22483
|
+
parts.push(
|
|
22484
|
+
`${forgettable.total} torn-down workstreams hold ${forgettable.ops} ops (${formatBytes(forgettable.bytes)}); largest: ${forgettable.top.join(", ")}`
|
|
22485
|
+
);
|
|
22486
|
+
commands.push(
|
|
22487
|
+
"see them: mu workstream list --torn-down",
|
|
22488
|
+
`forget some: mu db forget ${forgettable.top.join(" ")}`
|
|
22489
|
+
);
|
|
22490
|
+
}
|
|
22491
|
+
if (tomb.bytes >= 1024 * 1024) {
|
|
22492
|
+
parts.push(`${tomb.tombstones} redundant note tombstones (${formatBytes(tomb.bytes)})`);
|
|
22493
|
+
commands.push("compact: mu db compact");
|
|
22494
|
+
}
|
|
22495
|
+
return parts.length === 0 ? void 0 : { line: parts.join("; "), commands };
|
|
22496
|
+
}
|
|
22145
22497
|
async function cmdDoctorJson(db, opts = {}) {
|
|
22146
22498
|
const health = await muxHealth();
|
|
22147
22499
|
const muExt = ctlExtensionDoctorCheck();
|
|
@@ -22283,6 +22635,7 @@ async function cmdDoctorJson(db, opts = {}) {
|
|
|
22283
22635
|
disk,
|
|
22284
22636
|
workspaceUsage,
|
|
22285
22637
|
drift,
|
|
22638
|
+
size: opsSizeHint(db) ?? null,
|
|
22286
22639
|
remediation: drift.ok === true ? [] : driftRemediation()
|
|
22287
22640
|
});
|
|
22288
22641
|
if (driftFailure !== null) throw driftFailure;
|
|
@@ -23020,6 +23373,7 @@ function fullJsonShape(d) {
|
|
|
23020
23373
|
orphans: d.view.orphans,
|
|
23021
23374
|
tracks: d.tracks,
|
|
23022
23375
|
ready: withRoiAll(d.ready),
|
|
23376
|
+
triage: withRoiAll(d.triage),
|
|
23023
23377
|
inProgress: withRoiAll(d.inProgress),
|
|
23024
23378
|
blocked: withRoiAll(d.blocked),
|
|
23025
23379
|
recentClosed: withRoiAll(d.recentClosed),
|
|
@@ -23092,6 +23446,7 @@ async function cmdState(db, opts) {
|
|
|
23092
23446
|
}
|
|
23093
23447
|
renderFullMode(perWs);
|
|
23094
23448
|
}
|
|
23449
|
+
var BIG_WORKSTREAM_TASKS = 300;
|
|
23095
23450
|
function renderFullMode(perWs) {
|
|
23096
23451
|
perWs.forEach((d, i) => {
|
|
23097
23452
|
if (i > 0) console.log("");
|
|
@@ -23124,6 +23479,15 @@ function renderFullCard(d) {
|
|
|
23124
23479
|
);
|
|
23125
23480
|
}
|
|
23126
23481
|
}
|
|
23482
|
+
const total = d.taskCount;
|
|
23483
|
+
const open = ready.length + inProgress.length + blocked.length + d.triage.length;
|
|
23484
|
+
if (total >= BIG_WORKSTREAM_TASKS && open * 10 < total) {
|
|
23485
|
+
console.log(
|
|
23486
|
+
pc.dim(
|
|
23487
|
+
`hint: ${total} tasks, ${open} open; start the next effort in a new <project>-<purpose> workstream`
|
|
23488
|
+
)
|
|
23489
|
+
);
|
|
23490
|
+
}
|
|
23127
23491
|
console.log("");
|
|
23128
23492
|
console.log(pc.bold(`Tracks (${tracks.length})`));
|
|
23129
23493
|
console.log(formatTracks(tracks));
|
|
@@ -23131,6 +23495,13 @@ function renderFullCard(d) {
|
|
|
23131
23495
|
console.log(pc.bold(`Ready (${ready.length})`));
|
|
23132
23496
|
console.log(ready.length === 0 ? pc.dim(" (none)") : formatTaskListTable(ready));
|
|
23133
23497
|
console.log("");
|
|
23498
|
+
if (d.triage.length > 0) {
|
|
23499
|
+
console.log(
|
|
23500
|
+
`${pc.bold(`Triage (${d.triage.length})`)} ${pc.dim(`accept: mu task accept <id> -w ${workstreamName} \xB7 decline: mu task close <id> --as rejected|duplicate --why ...`)}`
|
|
23501
|
+
);
|
|
23502
|
+
console.log(formatTaskListTable(d.triage));
|
|
23503
|
+
console.log("");
|
|
23504
|
+
}
|
|
23134
23505
|
console.log(pc.bold(`In progress (${inProgress.length})`));
|
|
23135
23506
|
console.log(inProgress.length === 0 ? pc.dim(" (none)") : formatTaskListTable(inProgress));
|
|
23136
23507
|
console.log("");
|
|
@@ -23736,6 +24107,7 @@ function wireWorkspaceCommands(program) {
|
|
|
23736
24107
|
}
|
|
23737
24108
|
|
|
23738
24109
|
// src/cli/workstream.ts
|
|
24110
|
+
import { basename as basename2 } from "path";
|
|
23739
24111
|
init_mux2();
|
|
23740
24112
|
init_output();
|
|
23741
24113
|
init_workstream();
|
|
@@ -23764,6 +24136,7 @@ async function cmdInit(db, name, opts = {}) {
|
|
|
23764
24136
|
await mux.enableMuPaneBordersForSession(sessionName).catch(() => {
|
|
23765
24137
|
});
|
|
23766
24138
|
const created = !sessionAlready || dbCreated;
|
|
24139
|
+
const nameHint = name.includes("-") ? void 0 : `name workstreams <project>-<purpose> (e.g. ${projectGuess() ?? "repo"}-${name}); one per effort`;
|
|
23767
24140
|
const nextSteps3 = [
|
|
23768
24141
|
{ intent: "Attach the session", command: mux.attachHint({ session: sessionName }) },
|
|
23769
24142
|
{
|
|
@@ -23781,6 +24154,7 @@ async function cmdInit(db, name, opts = {}) {
|
|
|
23781
24154
|
muxSessionAlreadyExisted: sessionAlready,
|
|
23782
24155
|
dbRowAlreadyExisted: !dbCreated,
|
|
23783
24156
|
muWindowRepaired,
|
|
24157
|
+
...nameHint && created ? { hint: nameHint } : {},
|
|
23784
24158
|
nextSteps: nextSteps3
|
|
23785
24159
|
});
|
|
23786
24160
|
return;
|
|
@@ -23796,8 +24170,13 @@ async function cmdInit(db, name, opts = {}) {
|
|
|
23796
24170
|
return;
|
|
23797
24171
|
}
|
|
23798
24172
|
console.log(`Created workstream ${pc.bold(name)} (mux session ${pc.bold(sessionName)})`);
|
|
24173
|
+
if (nameHint && created) console.log(pc.dim(`hint: ${nameHint}`));
|
|
23799
24174
|
printNextSteps(nextSteps3);
|
|
23800
24175
|
}
|
|
24176
|
+
function projectGuess() {
|
|
24177
|
+
const base = basename2(process.cwd()).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "");
|
|
24178
|
+
return base === "" ? void 0 : base;
|
|
24179
|
+
}
|
|
23801
24180
|
async function cmdWorkstreamList(db, opts = {}) {
|
|
23802
24181
|
if (opts.tornDown === true) {
|
|
23803
24182
|
const gone = listTornDownWorkstreams(db);
|
|
@@ -24053,8 +24432,12 @@ function withPositionalWorkstream(opts, name) {
|
|
|
24053
24432
|
return { ...opts, workstream: name };
|
|
24054
24433
|
}
|
|
24055
24434
|
function wireWorkstreamCommands(program) {
|
|
24056
|
-
const workstream = program.command("workstream").description(
|
|
24057
|
-
|
|
24435
|
+
const workstream = program.command("workstream").description(
|
|
24436
|
+
"Workstream-level commands. Name them <project>-<purpose> (hail-auth), one per effort; tear down when it ships. Reviews nobody will track belong in scratch delegates, not a workstream."
|
|
24437
|
+
);
|
|
24438
|
+
workstream.command("init <name>").description(
|
|
24439
|
+
"Create the workstream's mux session and register it in the DB. Name it <project>-<purpose>."
|
|
24440
|
+
).option(...JSON_OPT).action(function(name) {
|
|
24058
24441
|
const opts = this.opts();
|
|
24059
24442
|
return handle((db) => cmdInit(db, name, opts), this)();
|
|
24060
24443
|
});
|