@mu-crew/mu 3.2.0 → 3.3.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 +208 -599
- package/README.md +125 -329
- package/dist/cli.js +108 -78
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +421 -56
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +36 -10
- package/dist/index.js +89 -67
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +176 -1072
- package/docs/ROADMAP.md +88 -169
- package/docs/VISION.md +205 -415
- package/docs/VOCABULARY.md +204 -466
- package/docs/architecture/control-socket.md +133 -0
- package/docs/architecture/dag.md +113 -0
- package/docs/architecture/mux.md +162 -0
- package/docs/architecture/ops-log.md +140 -0
- package/docs/architecture/sdk.md +185 -0
- package/docs/architecture/sync.md +129 -0
- package/docs/architecture/tui.md +146 -0
- package/docs/guide/README.md +30 -0
- package/docs/guide/backends.md +61 -0
- package/docs/guide/cleanup.md +74 -0
- package/docs/guide/delegate.md +87 -0
- package/docs/guide/dispatch.md +144 -0
- package/docs/guide/getting-started.md +180 -0
- package/docs/guide/recovery.md +135 -0
- package/docs/guide/remote.md +74 -0
- package/docs/guide/sql.md +76 -0
- package/docs/guide/stop-a-worker.md +64 -0
- package/docs/guide/sync.md +78 -0
- package/docs/guide/tui.md +98 -0
- package/docs/guide/upgrade.md +76 -0
- package/docs/history/CHANGELOG-pre-3.md +4866 -0
- package/docs/reference/env.md +87 -0
- package/docs/reference/naming.md +82 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
- package/docs/specs/2026-09-29-task-substates.md +1 -1
- package/docs/specs/2026-10-03-keep-driving-nudge.md +142 -0
- package/package.json +2 -1
- package/prompts/mu-refute.md +8 -0
- package/prompts/mu-research.md +8 -0
- package/prompts/mu-review.md +8 -0
- package/prompts/mu-rules-audit.md +8 -0
- package/prompts/mu-tournament.md +9 -0
- package/prompts/ultrathink.md +9 -0
- package/skills/mu/SKILL.md +126 -255
- package/skills/mu/recipes/adversarial-review.md +83 -0
- package/skills/mu/recipes/brief.md +53 -0
- package/skills/mu/recipes/codemode-driver.md +54 -0
- package/skills/mu/recipes/deep-research.md +52 -0
- package/skills/mu/recipes/fan-out.md +36 -0
- package/skills/mu/recipes/hypothesis-panel.md +40 -0
- package/skills/mu/recipes/long-run.md +20 -0
- package/skills/mu/recipes/loop-until-done.md +38 -0
- package/skills/mu/recipes/orchestrator-loop.md +87 -0
- package/skills/mu/recipes/plan.md +54 -0
- package/skills/mu/recipes/recovery.md +26 -0
- package/skills/mu/recipes/refute.md +49 -0
- package/skills/mu/recipes/remote-workers.md +292 -0
- package/skills/mu/recipes/review-panel.md +57 -0
- package/skills/mu/recipes/rules-audit.md +70 -0
- package/skills/mu/recipes/tournament.md +37 -0
- package/skills/mu/recipes/triage.md +38 -0
- package/skills/mu/recipes/ultrathink.md +73 -0
- package/skills/mu/recipes/watcher.md +16 -0
- package/skills/mu/recipes/waves.md +19 -0
- package/skills/mu/recipes/worker.md +24 -0
- package/docs/HANDOVER.md +0 -486
- package/docs/USAGE_GUIDE.md +0 -2738
- package/skills/mu/REMOTE_WORKERS.md +0 -617
package/dist/extension/mu-pi.js
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
// extension/mu-pi.ts
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
|
-
existsSync as
|
|
4
|
+
existsSync as existsSync3,
|
|
5
5
|
linkSync,
|
|
6
6
|
mkdirSync,
|
|
7
|
-
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync as readFileSync2,
|
|
8
9
|
realpathSync,
|
|
9
10
|
rmSync,
|
|
10
|
-
statSync
|
|
11
|
+
statSync as statSync2
|
|
11
12
|
} from "fs";
|
|
12
13
|
import { connect, createServer } from "net";
|
|
13
|
-
import { dirname, join as
|
|
14
|
+
import { dirname, join as join3 } from "path";
|
|
15
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14
16
|
|
|
15
17
|
// src/ctl/protocol.ts
|
|
16
18
|
var CTL_PROTOCOL_VERSION = 1;
|
|
19
|
+
var CTL_COMMANDS = ["new", "reload", "compact"];
|
|
17
20
|
var CTL_OPS = [
|
|
18
21
|
"hello",
|
|
19
22
|
"status",
|
|
20
23
|
"send",
|
|
21
24
|
"wait",
|
|
22
25
|
"abort",
|
|
23
|
-
"fresh"
|
|
26
|
+
"fresh",
|
|
27
|
+
"command"
|
|
24
28
|
];
|
|
25
29
|
var UNKNOWN_OP_PREFIX = "unknown op: ";
|
|
26
30
|
function encode(msg) {
|
|
@@ -39,13 +43,17 @@ var LineDecoder = class {
|
|
|
39
43
|
|
|
40
44
|
// extension/delegate.ts
|
|
41
45
|
import { execFile } from "child_process";
|
|
42
|
-
import { existsSync } from "fs";
|
|
46
|
+
import { existsSync, statSync } from "fs";
|
|
43
47
|
import { delimiter, join } from "path";
|
|
44
48
|
import { fileURLToPath } from "url";
|
|
45
49
|
var DELEGATE_TOOL = "mu_delegate";
|
|
46
50
|
var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
|
|
47
51
|
var DELEGATE_MESSAGE_TYPE = "mu-delegate";
|
|
48
52
|
var DELEGATE_WORKSTREAM = "scratch";
|
|
53
|
+
var DELEGATE_STATUS_KEY = "mu-delegate";
|
|
54
|
+
function delegateStatus(n) {
|
|
55
|
+
return n > 0 ? `${n} delegate${n === 1 ? "" : "s"} running` : void 0;
|
|
56
|
+
}
|
|
49
57
|
var DELEGATE_TIMEOUT_S = 3600;
|
|
50
58
|
var W = DELEGATE_WORKSTREAM;
|
|
51
59
|
function delegateEnabled(env = process.env) {
|
|
@@ -54,6 +62,13 @@ function delegateEnabled(env = process.env) {
|
|
|
54
62
|
if (env.MU_MUX || env.HERDR_ENV === "1" || env.TMUX || env.TMUX_PANE) return true;
|
|
55
63
|
return onPath("tmux", env) || onPath("herdr", env);
|
|
56
64
|
}
|
|
65
|
+
function isDir(p) {
|
|
66
|
+
try {
|
|
67
|
+
return statSync(p).isDirectory();
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
57
72
|
function onPath(bin, env) {
|
|
58
73
|
return (env.PATH ?? "").split(delimiter).some((d) => d !== "" && existsSync(join(d, bin)));
|
|
59
74
|
}
|
|
@@ -77,41 +92,71 @@ function defaultRunner() {
|
|
|
77
92
|
);
|
|
78
93
|
});
|
|
79
94
|
}
|
|
80
|
-
function
|
|
95
|
+
function formatElapsed(ms) {
|
|
96
|
+
const t = Math.max(0, Math.round(ms / 1e3));
|
|
97
|
+
if (t < 60) return `${t}s`;
|
|
98
|
+
const m = Math.floor(t / 60);
|
|
99
|
+
if (m < 60) return `${m}m ${String(t % 60).padStart(2, "0")}s`;
|
|
100
|
+
return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
|
|
101
|
+
}
|
|
102
|
+
function delegateMessage(name, agent, pane, x = {}) {
|
|
103
|
+
const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
|
|
81
104
|
const who = `Delegate ${W}/${name}`;
|
|
105
|
+
const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
|
|
82
106
|
const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
|
|
83
107
|
const look = [
|
|
84
|
-
attach ? `Attach: ${attach}` : void 0,
|
|
108
|
+
x.attach ? `Attach: ${x.attach}` : void 0,
|
|
85
109
|
`Read: mu agent read ${name} -n 50 -w ${W}`
|
|
86
|
-
].filter((
|
|
87
|
-
const tailBlock = tail?.trim() ? `
|
|
110
|
+
].filter((v) => v !== void 0);
|
|
111
|
+
const tailBlock = x.tail?.trim() ? `
|
|
88
112
|
|
|
89
113
|
Pane tail:
|
|
90
|
-
${tail.trimEnd()}` : "";
|
|
114
|
+
${x.tail.trimEnd()}` : "";
|
|
115
|
+
const ws = x.workspace ? ` Workspace: ${x.workspace}` : "";
|
|
91
116
|
switch (agent?.outcome) {
|
|
92
117
|
case "done":
|
|
93
|
-
return `${who} finished. ${fate}
|
|
118
|
+
return `${who} finished${took}. ${fate}${ws}
|
|
94
119
|
|
|
95
120
|
${agent.lastText ?? ""}`;
|
|
96
121
|
case "empty":
|
|
97
|
-
return `${who} finished without a text answer. ${fate}${tailBlock}`;
|
|
122
|
+
return `${who} finished${took} without a text answer. ${fate}${ws}${tailBlock}`;
|
|
98
123
|
case "died":
|
|
99
|
-
return `${who} died before answering. ${fate}${tailBlock}`;
|
|
124
|
+
return `${who} died${took} before answering. ${fate}${ws}${tailBlock}`;
|
|
100
125
|
case "timeout":
|
|
101
126
|
case "pending":
|
|
102
127
|
return [
|
|
103
|
-
`${who} is still running after ${
|
|
128
|
+
`${who} is still running after ${timeoutS}s; stopped waiting, so its answer will not arrive here. ${fate}${ws}`,
|
|
104
129
|
...look,
|
|
105
130
|
`Wait again: mu agent wait ${name} -w ${W} --json`
|
|
106
131
|
].join("\n");
|
|
107
132
|
default:
|
|
108
|
-
return `${who}: mu agent wait gave no result. ${fate}${tailBlock}`;
|
|
133
|
+
return `${who}: mu agent wait gave no result. ${fate}${ws}${tailBlock}`;
|
|
109
134
|
}
|
|
110
135
|
}
|
|
136
|
+
function labelStem(label) {
|
|
137
|
+
if (typeof label !== "string") return void 0;
|
|
138
|
+
const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
|
|
139
|
+
return stem || void 0;
|
|
140
|
+
}
|
|
111
141
|
function registerDelegate(pi, run = defaultRunner()) {
|
|
112
142
|
if (!delegateEnabled()) return;
|
|
113
143
|
const inflight = /* @__PURE__ */ new Map();
|
|
114
144
|
const reserved = /* @__PURE__ */ new Set();
|
|
145
|
+
let ui;
|
|
146
|
+
const showStatus = () => {
|
|
147
|
+
try {
|
|
148
|
+
ui?.setStatus?.(DELEGATE_STATUS_KEY, delegateStatus(inflight.size));
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const track = (ctx) => {
|
|
153
|
+
if (ctx?.hasUI !== false && ctx?.ui) ui = ctx.ui;
|
|
154
|
+
};
|
|
155
|
+
const forget = (name) => {
|
|
156
|
+
inflight.delete(name);
|
|
157
|
+
reserved.delete(name);
|
|
158
|
+
showStatus();
|
|
159
|
+
};
|
|
115
160
|
const mu = run;
|
|
116
161
|
const json = (r) => {
|
|
117
162
|
try {
|
|
@@ -122,28 +167,39 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
122
167
|
}
|
|
123
168
|
};
|
|
124
169
|
const failure = (what, r) => new Error(`${what} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
|
|
125
|
-
async function pickName() {
|
|
170
|
+
async function pickName(stem) {
|
|
126
171
|
const r = await mu(["agent", "list", "-w", W, "--json"]);
|
|
127
172
|
const agents = json(r)?.agents;
|
|
128
173
|
const taken = new Set(
|
|
129
174
|
Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
|
|
130
175
|
);
|
|
131
176
|
for (let n = 1; ; n++) {
|
|
132
|
-
const name = `delegate-${n}`;
|
|
177
|
+
const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
|
|
133
178
|
if (!taken.has(name) && !reserved.has(name)) {
|
|
134
179
|
reserved.add(name);
|
|
135
180
|
return name;
|
|
136
181
|
}
|
|
137
182
|
}
|
|
138
183
|
}
|
|
139
|
-
async function spawn(params) {
|
|
184
|
+
async function spawn(params, cwd, signal) {
|
|
185
|
+
const stem = labelStem(params.label);
|
|
140
186
|
for (let attempt = 0; ; attempt++) {
|
|
141
|
-
|
|
187
|
+
signal?.throwIfAborted();
|
|
188
|
+
const name = await pickName(stem);
|
|
142
189
|
const args = ["agent", "spawn", name, "-w", W, "--json"];
|
|
143
190
|
if (params.workspace === true) args.push("--workspace");
|
|
191
|
+
else if (cwd) args.push("--cwd", cwd);
|
|
144
192
|
if (typeof params.cli === "string" && params.cli) args.push("--cli", params.cli);
|
|
145
193
|
const r = await mu(args);
|
|
146
|
-
if (
|
|
194
|
+
if (signal?.aborted && r.code === 0) {
|
|
195
|
+
reserved.delete(name);
|
|
196
|
+
await mu(["agent", "close", name, "-w", W]);
|
|
197
|
+
signal.throwIfAborted();
|
|
198
|
+
}
|
|
199
|
+
if (r.code !== 0 && /already exists/.test(r.stderr) && attempt < 3) {
|
|
200
|
+
reserved.delete(name);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
147
203
|
if (r.code !== 0) {
|
|
148
204
|
reserved.delete(name);
|
|
149
205
|
throw failure("mu agent spawn", r);
|
|
@@ -157,10 +213,15 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
157
213
|
}
|
|
158
214
|
const steps = Array.isArray(out.nextSteps) ? out.nextSteps : [];
|
|
159
215
|
const attach = steps.map((s) => s).find((s) => s.intent === "Attach the pane")?.command;
|
|
160
|
-
|
|
216
|
+
const wsPath = out.workspace?.path;
|
|
217
|
+
return {
|
|
218
|
+
name,
|
|
219
|
+
...typeof attach === "string" ? { attach } : {},
|
|
220
|
+
...typeof wsPath === "string" ? { workspace: wsPath } : {}
|
|
221
|
+
};
|
|
161
222
|
}
|
|
162
223
|
}
|
|
163
|
-
async function deliver(name, wait, keep,
|
|
224
|
+
async function deliver(name, wait, keep, extras) {
|
|
164
225
|
const agent = json(wait)?.agents?.[0];
|
|
165
226
|
const outcome = agent?.outcome;
|
|
166
227
|
const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
|
|
@@ -174,9 +235,17 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
174
235
|
await pi.sendMessage(
|
|
175
236
|
{
|
|
176
237
|
customType: DELEGATE_MESSAGE_TYPE,
|
|
177
|
-
content: delegateMessage(name, agent, pane,
|
|
238
|
+
content: delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} }),
|
|
178
239
|
display: true,
|
|
179
|
-
details: {
|
|
240
|
+
details: {
|
|
241
|
+
name,
|
|
242
|
+
workstream: W,
|
|
243
|
+
outcome: outcome ?? null,
|
|
244
|
+
closed: pane.closed,
|
|
245
|
+
answer: outcome === "done" ? agent?.lastText ?? null : null,
|
|
246
|
+
elapsedMs: extras.elapsedMs ?? null,
|
|
247
|
+
workspace: extras.workspace ?? null
|
|
248
|
+
}
|
|
180
249
|
},
|
|
181
250
|
{ deliverAs: "followUp", triggerTurn: true }
|
|
182
251
|
);
|
|
@@ -184,23 +253,36 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
184
253
|
pi.registerTool({
|
|
185
254
|
name: DELEGATE_TOOL,
|
|
186
255
|
label: "mu delegate",
|
|
187
|
-
description: `
|
|
188
|
-
promptSnippet: "
|
|
256
|
+
description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation; call it several times to fan out in parallel. The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later as a follow-up message: carry on with other work, or end your turn if your next step needs the answer (the follow-up resumes you). The pane closes after a clean finish; keep: true leaves it open.`,
|
|
257
|
+
promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
|
|
189
258
|
parameters: {
|
|
190
259
|
type: "object",
|
|
191
260
|
properties: {
|
|
192
261
|
task: { type: "string", description: "The complete task, with all context it needs" },
|
|
262
|
+
label: {
|
|
263
|
+
type: "string",
|
|
264
|
+
description: "Short label naming the delegate, e.g. 'review' \u2192 delegate-review. Use one per subtask when fanning out."
|
|
265
|
+
},
|
|
193
266
|
brief: {
|
|
194
267
|
type: "string",
|
|
195
|
-
description: "Optional
|
|
268
|
+
description: "Optional standing role or rules, sent before the task, e.g. 'You are a reviewer; read only.' The rules are instructions, not enforced. The job itself goes in task."
|
|
269
|
+
},
|
|
270
|
+
cwd: {
|
|
271
|
+
type: "string",
|
|
272
|
+
description: "Working directory for the delegate (default: yours; ignored with workspace)"
|
|
273
|
+
},
|
|
274
|
+
timeout: {
|
|
275
|
+
type: "number",
|
|
276
|
+
exclusiveMinimum: 0,
|
|
277
|
+
description: `Seconds to wait for the answer (default ${DELEGATE_TIMEOUT_S}). On timeout you get a "still running" follow-up instead of the answer, and the answer will not arrive later; the subagent keeps running in its pane.`
|
|
196
278
|
},
|
|
197
279
|
workspace: {
|
|
198
280
|
type: "boolean",
|
|
199
|
-
description: "
|
|
281
|
+
description: "For tasks that edit files: its own VCS checkout (jj workspace / git worktree / sl share) branched from the current commit. The result and follow-up name the path; changes stay there until you merge them. This isolates repository edits only: the subagent runs as you, with your files, environment and network."
|
|
200
282
|
},
|
|
201
283
|
cli: {
|
|
202
284
|
type: "string",
|
|
203
|
-
description: "Agent CLI key
|
|
285
|
+
description: "Agent CLI key (default pi). Name a configured key such as 'pi_fast' to run a cheaper model."
|
|
204
286
|
},
|
|
205
287
|
keep: {
|
|
206
288
|
type: "boolean",
|
|
@@ -210,16 +292,30 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
210
292
|
required: ["task"],
|
|
211
293
|
additionalProperties: false
|
|
212
294
|
},
|
|
213
|
-
async execute(_id, params) {
|
|
295
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
296
|
+
track(ctx);
|
|
214
297
|
const task = typeof params.task === "string" ? params.task.trim() : "";
|
|
215
298
|
if (!task) throw new Error("mu_delegate needs a non-empty task");
|
|
216
299
|
const brief = typeof params.brief === "string" ? params.brief.trim() : "";
|
|
217
300
|
const keep = params.keep === true;
|
|
218
|
-
const
|
|
219
|
-
|
|
301
|
+
const t = params.timeout;
|
|
302
|
+
if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
|
|
303
|
+
throw new Error(
|
|
304
|
+
`mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
|
|
305
|
+
);
|
|
306
|
+
const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
|
|
307
|
+
let cwd;
|
|
308
|
+
if (params.workspace !== true) {
|
|
309
|
+
cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
|
|
310
|
+
if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
|
|
311
|
+
}
|
|
312
|
+
const { name, attach, workspace } = await spawn(params, cwd, signal);
|
|
313
|
+
const entry = { abort: new AbortController(), cancelling: false };
|
|
220
314
|
inflight.set(name, entry);
|
|
315
|
+
showStatus();
|
|
316
|
+
const started = Date.now();
|
|
221
317
|
const waiting = mu(
|
|
222
|
-
["agent", "wait", name, "-w", W, "--json", "--timeout", String(
|
|
318
|
+
["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
|
|
223
319
|
entry.abort.signal
|
|
224
320
|
);
|
|
225
321
|
const sent = await mu(["agent", "send", name, brief ? `${brief}
|
|
@@ -227,23 +323,55 @@ function registerDelegate(pi, run = defaultRunner()) {
|
|
|
227
323
|
${task}` : task, "-w", W]);
|
|
228
324
|
if (sent.code !== 0) {
|
|
229
325
|
entry.abort.abort();
|
|
230
|
-
|
|
231
|
-
|
|
326
|
+
forget(name);
|
|
327
|
+
const c = await mu(["agent", "close", name, "-w", W]);
|
|
328
|
+
const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
|
|
329
|
+
throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
|
|
232
330
|
}
|
|
331
|
+
const settle = async (r) => {
|
|
332
|
+
forget(name);
|
|
333
|
+
try {
|
|
334
|
+
await deliver(name, r, keep, {
|
|
335
|
+
timeoutS,
|
|
336
|
+
elapsedMs: Date.now() - started,
|
|
337
|
+
...attach ? { attach } : {},
|
|
338
|
+
...workspace ? { workspace } : {}
|
|
339
|
+
});
|
|
340
|
+
} catch (e) {
|
|
341
|
+
await Promise.resolve(
|
|
342
|
+
pi.sendMessage(
|
|
343
|
+
{
|
|
344
|
+
customType: DELEGATE_MESSAGE_TYPE,
|
|
345
|
+
content: `Delegate ${W}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${W}`,
|
|
346
|
+
display: true,
|
|
347
|
+
details: { name, workstream: W, outcome: null, closed: false }
|
|
348
|
+
},
|
|
349
|
+
{ deliverAs: "followUp", triggerTurn: true }
|
|
350
|
+
)
|
|
351
|
+
).catch(() => {
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
};
|
|
233
355
|
void waiting.then(async (r) => {
|
|
234
|
-
if (inflight.get(name) !== entry
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await deliver(name, r, keep, attach);
|
|
356
|
+
if (inflight.get(name) !== entry) return;
|
|
357
|
+
if (entry.cancelling) entry.parked = () => settle(r);
|
|
358
|
+
else await settle(r);
|
|
238
359
|
});
|
|
239
360
|
const lines = [
|
|
240
361
|
`Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
|
|
362
|
+
workspace ? `Workspace: ${workspace}` : void 0,
|
|
241
363
|
attach ? `Attach: ${attach}` : void 0,
|
|
242
364
|
`Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
|
|
243
365
|
].filter((x) => x !== void 0);
|
|
244
366
|
return {
|
|
245
367
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
246
|
-
details: {
|
|
368
|
+
details: {
|
|
369
|
+
name,
|
|
370
|
+
workstream: W,
|
|
371
|
+
attach: attach ?? null,
|
|
372
|
+
workspace: workspace ?? null,
|
|
373
|
+
keep
|
|
374
|
+
}
|
|
247
375
|
};
|
|
248
376
|
}
|
|
249
377
|
});
|
|
@@ -260,19 +388,26 @@ ${task}` : task, "-w", W]);
|
|
|
260
388
|
required: ["name"],
|
|
261
389
|
additionalProperties: false
|
|
262
390
|
},
|
|
263
|
-
async execute(_id, params) {
|
|
391
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
392
|
+
track(ctx);
|
|
264
393
|
const name = typeof params.name === "string" ? params.name : "";
|
|
265
394
|
if (!name) throw new Error("mu_delegate_cancel needs a name");
|
|
266
395
|
const entry = inflight.get(name);
|
|
267
|
-
if (entry) entry.
|
|
396
|
+
if (entry) entry.cancelling = true;
|
|
268
397
|
const ab = await mu(["agent", "abort", name, "-w", W]);
|
|
269
398
|
if (ab.code !== 0) {
|
|
270
|
-
if (entry)
|
|
399
|
+
if (entry) {
|
|
400
|
+
entry.cancelling = false;
|
|
401
|
+
const parked = entry.parked;
|
|
402
|
+
entry.parked = void 0;
|
|
403
|
+
if (parked) void parked();
|
|
404
|
+
}
|
|
271
405
|
throw failure(`mu agent abort ${W}/${name}`, ab);
|
|
272
406
|
}
|
|
273
|
-
entry
|
|
274
|
-
|
|
275
|
-
|
|
407
|
+
if (entry) {
|
|
408
|
+
entry.abort.abort();
|
|
409
|
+
forget(name);
|
|
410
|
+
}
|
|
276
411
|
let text = `Aborted ${W}/${name}.`;
|
|
277
412
|
if (params.keep === true) text += " Pane kept.";
|
|
278
413
|
else {
|
|
@@ -287,6 +422,8 @@ ${task}` : task, "-w", W]);
|
|
|
287
422
|
const names = [...inflight.keys()];
|
|
288
423
|
for (const e of inflight.values()) e.abort.abort();
|
|
289
424
|
inflight.clear();
|
|
425
|
+
showStatus();
|
|
426
|
+
ui = void 0;
|
|
290
427
|
const msg = `${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`;
|
|
291
428
|
if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
|
|
292
429
|
else process.stderr.write(`mu: ${msg}
|
|
@@ -294,10 +431,143 @@ ${task}` : task, "-w", W]);
|
|
|
294
431
|
});
|
|
295
432
|
}
|
|
296
433
|
|
|
434
|
+
// extension/nudge.ts
|
|
435
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
436
|
+
import { join as join2 } from "path";
|
|
437
|
+
var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
|
|
438
|
+
var NUDGE_LOG_KIND = "nudge";
|
|
439
|
+
var MARK_START = "<!-- mu:keep-driving -->";
|
|
440
|
+
var MARK_END = "<!-- /mu:keep-driving -->";
|
|
441
|
+
var WAIT_HINT_MAX = 8;
|
|
442
|
+
function nudgeEnabled(env = process.env) {
|
|
443
|
+
return env.MU_NUDGE !== "0";
|
|
444
|
+
}
|
|
445
|
+
function keepDrivingRule(skillMd) {
|
|
446
|
+
const i = skillMd.indexOf(MARK_START);
|
|
447
|
+
const j = skillMd.indexOf(MARK_END);
|
|
448
|
+
if (i < 0 || j < i) return void 0;
|
|
449
|
+
const text = skillMd.slice(i + MARK_START.length, j).trim();
|
|
450
|
+
return text === "" ? void 0 : text;
|
|
451
|
+
}
|
|
452
|
+
function readSkill() {
|
|
453
|
+
for (const up of ["..", "../.."]) {
|
|
454
|
+
const p = join2(import.meta.dirname ?? "", up, "skills", "mu", "SKILL.md");
|
|
455
|
+
if (existsSync2(p)) return readFileSync(p, "utf8");
|
|
456
|
+
}
|
|
457
|
+
return void 0;
|
|
458
|
+
}
|
|
459
|
+
function dispatchedWorkstreams(command) {
|
|
460
|
+
const found = /* @__PURE__ */ new Set();
|
|
461
|
+
for (const seg of command.split(/&&|\|\||;|\n|\|/)) {
|
|
462
|
+
const words = seg.trim().split(/\s+/);
|
|
463
|
+
const at = words.findIndex((w) => w === "mu" || w.endsWith("/mu"));
|
|
464
|
+
if (at < 0) continue;
|
|
465
|
+
const [noun, verb] = [words[at + 1], words[at + 2]];
|
|
466
|
+
const isSendSpawn = noun === "agent" && (verb === "send" || verb === "spawn");
|
|
467
|
+
const isClaimFor = noun === "task" && verb === "claim" && words.includes("--for");
|
|
468
|
+
if (!isSendSpawn && !isClaimFor) continue;
|
|
469
|
+
let ws = "";
|
|
470
|
+
for (let k = at + 3; k < words.length; k++) {
|
|
471
|
+
const w = words[k] ?? "";
|
|
472
|
+
if (w === "-w" || w === "--workstream") ws = unquote(words[k + 1] ?? "");
|
|
473
|
+
else if (w.startsWith("--workstream=")) ws = unquote(w.slice("--workstream=".length));
|
|
474
|
+
}
|
|
475
|
+
if (ws === "" && isClaimFor) {
|
|
476
|
+
const ref = words[at + 3] ?? "";
|
|
477
|
+
if (ref.includes("/")) ws = unquote(ref.split("/")[0] ?? "");
|
|
478
|
+
}
|
|
479
|
+
if (ws !== "scratch") found.add(ws);
|
|
480
|
+
}
|
|
481
|
+
return found.size > 0 ? [...found] : void 0;
|
|
482
|
+
}
|
|
483
|
+
function unquote(s) {
|
|
484
|
+
return s.replace(/^['"]|['"]$/g, "");
|
|
485
|
+
}
|
|
486
|
+
async function inProgress(run, workstreams) {
|
|
487
|
+
const named = workstreams.filter((w) => w !== "");
|
|
488
|
+
const args = ["state", "--json", "--events", "0"];
|
|
489
|
+
for (const w of named) args.push("-w", w);
|
|
490
|
+
const r = await run(args);
|
|
491
|
+
if (r.code !== 0) return void 0;
|
|
492
|
+
let json;
|
|
493
|
+
try {
|
|
494
|
+
json = JSON.parse(r.stdout);
|
|
495
|
+
} catch {
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
const cards = json.workstreams ?? [json];
|
|
499
|
+
const refs = [];
|
|
500
|
+
for (const ws of cards) {
|
|
501
|
+
for (const t of ws.inProgress ?? []) {
|
|
502
|
+
if (t.name) refs.push(`${ws.workstreamName ?? ""}/${t.name}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return refs;
|
|
506
|
+
}
|
|
507
|
+
function nudgeText(rule, refs) {
|
|
508
|
+
const shown = refs.slice(0, WAIT_HINT_MAX).join(" ");
|
|
509
|
+
const more = refs.length > WAIT_HINT_MAX ? ` (+${refs.length - WAIT_HINT_MAX} more)` : "";
|
|
510
|
+
return [
|
|
511
|
+
`[mu] ${rule}`,
|
|
512
|
+
"",
|
|
513
|
+
`${refs.length} task(s) IN_PROGRESS. Next: mu task wait ${shown} --first --on-stall exit --json${more}`
|
|
514
|
+
].join("\n");
|
|
515
|
+
}
|
|
516
|
+
function outcomeOf(event) {
|
|
517
|
+
return typeof event === "object" && event !== null ? event.outcome : void 0;
|
|
518
|
+
}
|
|
519
|
+
function bashCommand(event) {
|
|
520
|
+
if (typeof event !== "object" || event === null) return void 0;
|
|
521
|
+
const e = event;
|
|
522
|
+
return e.toolName === "bash" && typeof e.input?.command === "string" ? e.input.command : void 0;
|
|
523
|
+
}
|
|
524
|
+
function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
|
|
525
|
+
if (!nudgeEnabled()) return;
|
|
526
|
+
const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
|
|
527
|
+
if (rule === void 0) return;
|
|
528
|
+
const armed = /* @__PURE__ */ new Set();
|
|
529
|
+
let fired = false;
|
|
530
|
+
pi.on("input", () => {
|
|
531
|
+
armed.clear();
|
|
532
|
+
fired = false;
|
|
533
|
+
});
|
|
534
|
+
pi.on("tool_call", (event) => {
|
|
535
|
+
const cmd = bashCommand(event);
|
|
536
|
+
if (cmd === void 0) return;
|
|
537
|
+
for (const ws of dispatchedWorkstreams(cmd) ?? []) armed.add(ws);
|
|
538
|
+
});
|
|
539
|
+
pi.on("agent_before_settle", async (event) => {
|
|
540
|
+
if (fired || armed.size === 0) return;
|
|
541
|
+
if (outcomeOf(event) !== "completed") return;
|
|
542
|
+
fired = true;
|
|
543
|
+
const refs = await inProgress(run, [...armed]);
|
|
544
|
+
if (refs === void 0 || refs.length === 0) return;
|
|
545
|
+
const text = nudgeText(rule, refs);
|
|
546
|
+
const logWs = [...armed].find((w) => w !== "");
|
|
547
|
+
await run([
|
|
548
|
+
"log",
|
|
549
|
+
...logWs ? ["-w", logWs] : [],
|
|
550
|
+
"--kind",
|
|
551
|
+
NUDGE_LOG_KIND,
|
|
552
|
+
`keep-driving: ${refs.length} in progress`
|
|
553
|
+
]);
|
|
554
|
+
return {
|
|
555
|
+
entries: [
|
|
556
|
+
{ type: "custom_message", customType: NUDGE_MESSAGE_TYPE, content: text, display: true }
|
|
557
|
+
],
|
|
558
|
+
continue: true
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
297
563
|
// extension/mu-pi.ts
|
|
298
|
-
var EXT_VERSION = true ? "3.
|
|
564
|
+
var EXT_VERSION = true ? "3.3.0" : void 0;
|
|
299
565
|
var FRESH_COMMAND = "mu-fresh";
|
|
300
566
|
var FRESH_TIMEOUT_MS = 3e4;
|
|
567
|
+
function commandName(name) {
|
|
568
|
+
return `mu-${name}`;
|
|
569
|
+
}
|
|
570
|
+
var COMMAND_TIMEOUT_MS = 3e4;
|
|
301
571
|
var LAST_TEXT_MAX_BYTES = 64 * 1024;
|
|
302
572
|
var LAST_TEXT_TRUNCATED = "[truncated, see pane]";
|
|
303
573
|
var LIVE_PROBE_MS = 500;
|
|
@@ -311,9 +581,9 @@ function detectPiVersion() {
|
|
|
311
581
|
if (!entry) return void 0;
|
|
312
582
|
let dir = dirname(realpathSync(entry));
|
|
313
583
|
for (let i = 0; i < 6; i++) {
|
|
314
|
-
const pkgPath =
|
|
315
|
-
if (
|
|
316
|
-
const pkg = JSON.parse(
|
|
584
|
+
const pkgPath = join3(dir, "package.json");
|
|
585
|
+
if (existsSync3(pkgPath)) {
|
|
586
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
317
587
|
if (typeof pkg === "object" && pkg !== null) {
|
|
318
588
|
const { name, version } = pkg;
|
|
319
589
|
if (typeof name === "string" && /pi/.test(name) && typeof version === "string") {
|
|
@@ -370,7 +640,7 @@ ${LAST_TEXT_TRUNCATED}`;
|
|
|
370
640
|
}
|
|
371
641
|
function inodeOf(path) {
|
|
372
642
|
try {
|
|
373
|
-
return
|
|
643
|
+
return statSync2(path).ino;
|
|
374
644
|
} catch {
|
|
375
645
|
return void 0;
|
|
376
646
|
}
|
|
@@ -396,8 +666,31 @@ function reasonOf(event) {
|
|
|
396
666
|
const r = typeof event === "object" && event !== null ? event.reason : void 0;
|
|
397
667
|
return typeof r === "string" ? r : void 0;
|
|
398
668
|
}
|
|
669
|
+
function promptsDir(from = import.meta.url) {
|
|
670
|
+
let here;
|
|
671
|
+
try {
|
|
672
|
+
here = dirname(fileURLToPath2(from));
|
|
673
|
+
} catch {
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
for (const up of ["..", "../.."]) {
|
|
677
|
+
const d = join3(here, up, "prompts");
|
|
678
|
+
if (existsSync3(join3(d, "ultrathink.md"))) return d;
|
|
679
|
+
}
|
|
680
|
+
return void 0;
|
|
681
|
+
}
|
|
682
|
+
function registerPrompts(pi) {
|
|
683
|
+
pi.on("resources_discover", () => {
|
|
684
|
+
const d = promptsDir();
|
|
685
|
+
if (!d) return {};
|
|
686
|
+
const paths = readdirSync(d).filter((f) => f.endsWith(".md")).sort().map((f) => join3(d, f));
|
|
687
|
+
return { promptPaths: paths };
|
|
688
|
+
});
|
|
689
|
+
}
|
|
399
690
|
function muPi(pi) {
|
|
400
691
|
registerDelegate(pi);
|
|
692
|
+
registerPrompts(pi);
|
|
693
|
+
registerNudge(pi);
|
|
401
694
|
serveCtl(pi);
|
|
402
695
|
}
|
|
403
696
|
function serveCtl(pi) {
|
|
@@ -432,6 +725,12 @@ function serveCtl(pi) {
|
|
|
432
725
|
g.fresh = void 0;
|
|
433
726
|
f.settle(r);
|
|
434
727
|
};
|
|
728
|
+
const settleCommand = (r) => {
|
|
729
|
+
const c = g.command;
|
|
730
|
+
if (!c) return;
|
|
731
|
+
g.command = void 0;
|
|
732
|
+
c.settle(r);
|
|
733
|
+
};
|
|
435
734
|
pi.on("agent_start", (_e, c) => {
|
|
436
735
|
g.ctx = c;
|
|
437
736
|
g.state = "busy";
|
|
@@ -470,10 +769,69 @@ function serveCtl(pi) {
|
|
|
470
769
|
settleFresh({ v: V, ok: true, ...status() });
|
|
471
770
|
}
|
|
472
771
|
});
|
|
772
|
+
pi.on("session_before_compact", () => {
|
|
773
|
+
if (g.command?.name === "compact") settleCommand({ v: V, ok: true, ...status() });
|
|
774
|
+
});
|
|
775
|
+
async function runCommand(name, ctx) {
|
|
776
|
+
const c = g.command;
|
|
777
|
+
if (c?.name !== name) return;
|
|
778
|
+
const ok = () => settleCommand({ v: V, ok: true, ...status() });
|
|
779
|
+
const failWith = (e) => settleCommand(fail(e instanceof Error ? e.message : String(e)));
|
|
780
|
+
try {
|
|
781
|
+
if (name === "new") {
|
|
782
|
+
const r = await ctx.newSession();
|
|
783
|
+
if (r.cancelled) settleCommand(fail("new session was cancelled"));
|
|
784
|
+
else ok();
|
|
785
|
+
} else if (name === "reload") {
|
|
786
|
+
await ctx.reload();
|
|
787
|
+
if (c.reloaded) ok();
|
|
788
|
+
else settleCommand(fail("pi did not reload (busy or compacting)"));
|
|
789
|
+
} else {
|
|
790
|
+
ctx.compact({
|
|
791
|
+
...c.instructions !== void 0 ? { customInstructions: c.instructions } : {},
|
|
792
|
+
onComplete: ok,
|
|
793
|
+
onError: failWith
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
failWith(e);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const name of CTL_COMMANDS) {
|
|
801
|
+
pi.registerCommand(commandName(name), {
|
|
802
|
+
description: `mu internal: /${name} requested over the control socket (mu agent send '/${name}')`,
|
|
803
|
+
handler: (_args, ctx) => runCommand(name, ctx)
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function command(req) {
|
|
807
|
+
const name = CTL_COMMANDS.find((n) => n === req.name);
|
|
808
|
+
if (name === void 0) {
|
|
809
|
+
return fail(`command must be one of ${CTL_COMMANDS.join(", ")}`);
|
|
810
|
+
}
|
|
811
|
+
if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
|
|
812
|
+
const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
|
|
813
|
+
if (busy && req.force !== true) return fail("busy");
|
|
814
|
+
const instructions = str(req, "instructions");
|
|
815
|
+
return new Promise((resolve) => {
|
|
816
|
+
const timer = setTimeout(() => settleCommand(fail(`/${name} timed out`)), COMMAND_TIMEOUT_MS);
|
|
817
|
+
g.command = {
|
|
818
|
+
name,
|
|
819
|
+
...instructions !== void 0 ? { instructions } : {},
|
|
820
|
+
reloaded: false,
|
|
821
|
+
settle: (r) => {
|
|
822
|
+
clearTimeout(timer);
|
|
823
|
+
resolve(r);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
Promise.resolve(
|
|
827
|
+
g.pi.sendUserMessage(`/${commandName(name)}`, { expandPromptTemplates: true })
|
|
828
|
+
).catch((e) => settleCommand(fail(e instanceof Error ? e.message : String(e))));
|
|
829
|
+
});
|
|
830
|
+
}
|
|
473
831
|
function fresh(req) {
|
|
474
832
|
const text = str(req, "text");
|
|
475
833
|
if (text === void 0) return fail("fresh needs a string text");
|
|
476
|
-
if (g.fresh) return fail("a fresh send is already in progress");
|
|
834
|
+
if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
|
|
477
835
|
const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
|
|
478
836
|
if (busy && req.force !== true) return fail("busy");
|
|
479
837
|
return new Promise((resolve) => {
|
|
@@ -515,6 +873,8 @@ function serveCtl(pi) {
|
|
|
515
873
|
}
|
|
516
874
|
case "fresh":
|
|
517
875
|
return fresh(req);
|
|
876
|
+
case "command":
|
|
877
|
+
return command(req);
|
|
518
878
|
case "wait":
|
|
519
879
|
return wait(num(req, "afterRuns"), num(req, "timeoutMs"), conn);
|
|
520
880
|
case "abort":
|
|
@@ -587,7 +947,7 @@ function serveCtl(pi) {
|
|
|
587
947
|
return;
|
|
588
948
|
}
|
|
589
949
|
rmSync(sockPath, { force: true });
|
|
590
|
-
const tmp =
|
|
950
|
+
const tmp = join3(dirname(sockPath), `.${process.pid.toString(36)}${(++bindSeq).toString(36)}`);
|
|
591
951
|
rmSync(tmp, { force: true });
|
|
592
952
|
const s = createServer(onConnection);
|
|
593
953
|
await new Promise((resolve, reject) => {
|
|
@@ -612,8 +972,9 @@ function serveCtl(pi) {
|
|
|
612
972
|
g.server = s;
|
|
613
973
|
g.ino = inodeOf(sockPath);
|
|
614
974
|
}
|
|
615
|
-
pi.on("session_start", async (
|
|
975
|
+
pi.on("session_start", async (e, c) => {
|
|
616
976
|
g.ctx = c;
|
|
977
|
+
if (reasonOf(e) === "reload" && g.command?.name === "reload") g.command.reloaded = true;
|
|
617
978
|
const s = g.server;
|
|
618
979
|
if (s) {
|
|
619
980
|
if (g.ino !== void 0 && inodeOf(sockPath) === g.ino) return;
|
|
@@ -636,18 +997,22 @@ function serveCtl(pi) {
|
|
|
636
997
|
if (!s) return;
|
|
637
998
|
g.waiters.clear();
|
|
638
999
|
settleFresh(fail("pi is shutting down"));
|
|
1000
|
+
settleCommand(fail("pi is shutting down"));
|
|
639
1001
|
for (const c of g.conns) c.destroy();
|
|
640
1002
|
await new Promise((resolve) => s.close(() => resolve()));
|
|
641
1003
|
if (ino !== void 0 && inodeOf(sockPath) === ino) rmSync(sockPath, { force: true });
|
|
642
1004
|
});
|
|
643
1005
|
}
|
|
644
1006
|
export {
|
|
1007
|
+
COMMAND_TIMEOUT_MS,
|
|
645
1008
|
FRESH_COMMAND,
|
|
646
1009
|
FRESH_TIMEOUT_MS,
|
|
647
1010
|
LAST_TEXT_MAX_BYTES,
|
|
648
1011
|
LAST_TEXT_TRUNCATED,
|
|
649
1012
|
LIVE_PROBE_MS,
|
|
1013
|
+
commandName,
|
|
650
1014
|
muPi as default,
|
|
651
|
-
lastAssistantText
|
|
1015
|
+
lastAssistantText,
|
|
1016
|
+
promptsDir
|
|
652
1017
|
};
|
|
653
1018
|
//# sourceMappingURL=mu-pi.js.map
|