@mu-crew/mu 3.2.1 → 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 +10 -5
- package/README.md +49 -2
- package/dist/cli.js +108 -78
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +244 -12
- 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 +1 -0
- package/docs/VOCABULARY.md +2 -0
- package/docs/architecture/control-socket.md +17 -3
- package/docs/architecture/mux.md +3 -4
- package/docs/guide/README.md +5 -0
- package/docs/guide/dispatch.md +11 -4
- package/docs/guide/getting-started.md +3 -0
- package/docs/guide/recovery.md +3 -1
- package/docs/guide/remote.md +1 -1
- package/docs/history/CHANGELOG-pre-3.md +1 -1
- package/docs/reference/env.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 +71 -131
- 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/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/skills/mu/{REMOTE_WORKERS.md → recipes/remote-workers.md} +0 -0
package/AGENTS.md
CHANGED
|
@@ -169,7 +169,7 @@ is one atomic call. Keep the tmux workaround out of herdr.
|
|
|
169
169
|
|
|
170
170
|
## Orchestrating on this repo
|
|
171
171
|
|
|
172
|
-
Follow
|
|
172
|
+
Follow [skills/mu/recipes/orchestrator-loop.md](skills/mu/recipes/orchestrator-loop.md). Repo specifics:
|
|
173
173
|
|
|
174
174
|
- Each workspace runs `npm install` into its own `node_modules`. Never
|
|
175
175
|
symlink main's.
|
|
@@ -188,15 +188,20 @@ Follow the loop in [skills/mu/SKILL.md](skills/mu/SKILL.md). Repo specifics:
|
|
|
188
188
|
## Skill files are context, not documentation
|
|
189
189
|
|
|
190
190
|
`skills/mu/SKILL.md` loads into every orchestrator's context, and
|
|
191
|
-
`skills/mu/
|
|
192
|
-
user's work.
|
|
193
|
-
|
|
191
|
+
the recipes in `skills/mu/recipes/` on demand. Every word competes with the
|
|
192
|
+
user's work. The recipes are written for an agent with no context;
|
|
193
|
+
[recipes/brief.md](skills/mu/recipes/brief.md) holds the rules, and
|
|
194
|
+
they apply to the skill files too.
|
|
194
195
|
|
|
195
196
|
- Leave verb lists, flags, and option tables to `--help`. The skill
|
|
196
197
|
carries only what `--help` cannot: the gotcha, the reason, the trap.
|
|
197
198
|
- Say each thing once, next to where it applies.
|
|
198
199
|
- SKILL.md holds the trigger and the rule. Detail that only some
|
|
199
|
-
branches need goes in
|
|
200
|
+
branches need goes in a recipe under `skills/mu/recipes/`, with a
|
|
201
|
+
row in SKILL.md's recipe index. Recipes reference only mu, never
|
|
202
|
+
skills outside this repo.
|
|
203
|
+
- `docs/guide/` pages stay terse for humans and link to the recipe
|
|
204
|
+
instead of restating it.
|
|
200
205
|
- Delete sentences the model obeys by default. State the target
|
|
201
206
|
behaviour.
|
|
202
207
|
|
package/README.md
CHANGED
|
@@ -89,6 +89,52 @@ pane.
|
|
|
89
89
|
For many tiny calls, a hidden subagent is lighter. mu bets that seeing
|
|
90
90
|
and steering agent work is worth a pane.
|
|
91
91
|
|
|
92
|
+
## Ultrathink, the mu way
|
|
93
|
+
|
|
94
|
+
Long agent runs fail in two ways: the agent stops at 35 of 50 items and
|
|
95
|
+
calls it done, and it grades its own work generously. The fix is to
|
|
96
|
+
take the plan out of the agent's head, fan the work out to fresh
|
|
97
|
+
agents, and have other agents try to refute each result before it
|
|
98
|
+
counts. Claude Code's ultracode does this with a hidden script. mu does
|
|
99
|
+
it in the open:
|
|
100
|
+
|
|
101
|
+
- **The plan is the DAG.** Every unit, every review, and every round
|
|
102
|
+
is a task before an agent spawns. `mu state` shows 35 of 50 done, so
|
|
103
|
+
nobody can declare victory early.
|
|
104
|
+
- **Reviews are tasks.** A `review_x` task is blocked by `x` and run by
|
|
105
|
+
a fresh agent, ideally on another model, against criteria written
|
|
106
|
+
up front. A rejection adds `fix_x` and a second review to the DAG
|
|
107
|
+
instead of rewriting history.
|
|
108
|
+
- **Every agent is a pane.** Attach to any worker, reviewer, or judge
|
|
109
|
+
mid-run and steer it.
|
|
110
|
+
- **Nothing lives only in a context window.** Verdicts, evidence, and
|
|
111
|
+
stop rules are task notes. The run survives compaction, crashes, and
|
|
112
|
+
a new orchestrator.
|
|
113
|
+
|
|
114
|
+
Ask your orchestrator to *ultrathink* a job, and it follows
|
|
115
|
+
[skills/mu/recipes/ultrathink.md](skills/mu/recipes/ultrathink.md).
|
|
116
|
+
That recipe composes smaller ones you can also use on their own:
|
|
117
|
+
|
|
118
|
+
| Recipe | Shape |
|
|
119
|
+
| ------ | ----- |
|
|
120
|
+
| [fan-out](skills/mu/recipes/fan-out.md) | one task per unit, merged as each closes |
|
|
121
|
+
| [adversarial-review](skills/mu/recipes/adversarial-review.md) | a fresh agent tries to reject each unit before it ships |
|
|
122
|
+
| [refute](skills/mu/recipes/refute.md) | finders, then one refuter per finding, then a report |
|
|
123
|
+
| [hypothesis-panel](skills/mu/recipes/hypothesis-panel.md) | independent theories from separate evidence, each attacked |
|
|
124
|
+
| [tournament](skills/mu/recipes/tournament.md) | competing attempts, judged in pairs |
|
|
125
|
+
| [loop-until-done](skills/mu/recipes/loop-until-done.md) | rounds until a stop rule written up front holds |
|
|
126
|
+
| [triage](skills/mu/recipes/triage.md) | quarantined readers classify, a trusted actor acts |
|
|
127
|
+
| [deep-research](skills/mu/recipes/deep-research.md) | searchers by angle, one checker per claim, a cited report |
|
|
128
|
+
| [review-panel](skills/mu/recipes/review-panel.md) | one reviewer per angle on a diff, refuted, then fixed or reported |
|
|
129
|
+
| [rules-audit](skills/mu/recipes/rules-audit.md) | one checker per AGENTS.md rule plus a skeptic; or mine repeated corrections into new rules |
|
|
130
|
+
|
|
131
|
+
In pi, `mu link pi` adds them as commands: `/ultrathink <job>`,
|
|
132
|
+
`/mu-research <question>`, `/mu-review [target]`, `/mu-refute <scope>`,
|
|
133
|
+
`/mu-tournament <task>`, and `/mu-rules-audit [check <target> | mine]`.
|
|
134
|
+
|
|
135
|
+
All recipes, including the orchestrator loop and remote workers, are
|
|
136
|
+
in [skills/mu/recipes/](skills/mu/recipes/).
|
|
137
|
+
|
|
92
138
|
## Documentation
|
|
93
139
|
|
|
94
140
|
- [Getting started](docs/guide/getting-started.md): the first
|
|
@@ -96,8 +142,9 @@ and steering agent work is worth a pane.
|
|
|
96
142
|
- [User guide](docs/guide/README.md): how-tos for dispatch, remote
|
|
97
143
|
workers, sync, recovery, and the dashboard.
|
|
98
144
|
- [skills/mu/SKILL.md](skills/mu/SKILL.md): what an orchestrating
|
|
99
|
-
agent reads. [
|
|
100
|
-
|
|
145
|
+
agent reads. [skills/mu/recipes/](skills/mu/recipes/) holds the
|
|
146
|
+
recipes it points to, such as
|
|
147
|
+
[remote workers](skills/mu/recipes/remote-workers.md).
|
|
101
148
|
- [ARCHITECTURE.md](docs/ARCHITECTURE.md) and
|
|
102
149
|
[docs/architecture/](docs/architecture/): the module map and deep
|
|
103
150
|
dives.
|
package/dist/cli.js
CHANGED
|
@@ -14,18 +14,20 @@ function encode(msg) {
|
|
|
14
14
|
return `${JSON.stringify(msg)}
|
|
15
15
|
`;
|
|
16
16
|
}
|
|
17
|
-
var CTL_PROTOCOL_VERSION, CTL_OPS, UNKNOWN_OP_PREFIX, LineDecoder;
|
|
17
|
+
var CTL_PROTOCOL_VERSION, CTL_COMMANDS, CTL_OPS, UNKNOWN_OP_PREFIX, LineDecoder;
|
|
18
18
|
var init_protocol = __esm({
|
|
19
19
|
"src/ctl/protocol.ts"() {
|
|
20
20
|
"use strict";
|
|
21
21
|
CTL_PROTOCOL_VERSION = 1;
|
|
22
|
+
CTL_COMMANDS = ["new", "reload", "compact"];
|
|
22
23
|
CTL_OPS = [
|
|
23
24
|
"hello",
|
|
24
25
|
"status",
|
|
25
26
|
"send",
|
|
26
27
|
"wait",
|
|
27
28
|
"abort",
|
|
28
|
-
"fresh"
|
|
29
|
+
"fresh",
|
|
30
|
+
"command"
|
|
29
31
|
];
|
|
30
32
|
UNKNOWN_OP_PREFIX = "unknown op: ";
|
|
31
33
|
LineDecoder = class {
|
|
@@ -44,7 +46,7 @@ var init_protocol = __esm({
|
|
|
44
46
|
import { createConnection } from "net";
|
|
45
47
|
function clientTimeout(req, override) {
|
|
46
48
|
if (override !== void 0) return override;
|
|
47
|
-
if (req.op === "fresh") return CTL_FRESH_TIMEOUT_MS;
|
|
49
|
+
if (req.op === "fresh" || req.op === "command") return CTL_FRESH_TIMEOUT_MS;
|
|
48
50
|
if (req.op !== "wait") return CTL_DEFAULT_TIMEOUT_MS;
|
|
49
51
|
return req.timeoutMs === void 0 ? void 0 : req.timeoutMs + CTL_WAIT_SLACK_MS;
|
|
50
52
|
}
|
|
@@ -1289,27 +1291,6 @@ async function awaitPaneQuiescence(paneId, budgetMs) {
|
|
|
1289
1291
|
await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
|
|
1290
1292
|
}
|
|
1291
1293
|
}
|
|
1292
|
-
async function awaitNewSession(paneId, baseline, budgetMs) {
|
|
1293
|
-
if (budgetMs <= 0) return true;
|
|
1294
|
-
const deadline = Date.now() + budgetMs;
|
|
1295
|
-
let previous = baseline;
|
|
1296
|
-
let stable = 0;
|
|
1297
|
-
for (; ; ) {
|
|
1298
|
-
const scrollback = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
|
|
1299
|
-
if (scrollback !== void 0 && scrollback !== baseline) {
|
|
1300
|
-
if (scrollback === previous) {
|
|
1301
|
-
stable++;
|
|
1302
|
-
if (stable >= QUIESCENCE_CONFIRMATIONS) return true;
|
|
1303
|
-
} else {
|
|
1304
|
-
stable = 0;
|
|
1305
|
-
}
|
|
1306
|
-
previous = scrollback;
|
|
1307
|
-
}
|
|
1308
|
-
const remaining = deadline - Date.now();
|
|
1309
|
-
if (remaining <= 0) return false;
|
|
1310
|
-
await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
1294
|
function pasteProbe(text2) {
|
|
1314
1295
|
const firstLine = (text2.split("\n")[0] ?? "").trim();
|
|
1315
1296
|
return firstLine.slice(0, 24);
|
|
@@ -1337,7 +1318,6 @@ async function sendToPane(paneId, text2, opts = {}) {
|
|
|
1337
1318
|
assertValidPaneId(paneId);
|
|
1338
1319
|
const readinessMs = opts.readinessMs ?? defaultSendReadinessMs();
|
|
1339
1320
|
const quiesced = await awaitPaneQuiescence(paneId, readinessMs);
|
|
1340
|
-
const newSessionBaseline = readinessMs > 0 && text2.trim() === "/new" ? await capturePane(paneId, { lines: 50 }).catch(() => void 0) : void 0;
|
|
1341
1321
|
const copyResult = await currentExecutor(["copy-mode", "-q", "-t", paneId]);
|
|
1342
1322
|
if (copyResult.exitCode !== 0 && /can't find pane|no current target/i.test(copyResult.stderr)) {
|
|
1343
1323
|
throw new TmuxError(
|
|
@@ -1360,15 +1340,6 @@ async function sendToPane(paneId, text2, opts = {}) {
|
|
|
1360
1340
|
if (delay2 > 0) await currentSleep(delay2);
|
|
1361
1341
|
await tmux(["send-keys", "-t", paneId, "Enter"]);
|
|
1362
1342
|
if (readinessMs <= 0) return;
|
|
1363
|
-
if (newSessionBaseline !== void 0 && !await awaitNewSession(paneId, newSessionBaseline, readinessMs)) {
|
|
1364
|
-
const warn3 = opts.onUndelivered ?? defaultUndeliveredWarning;
|
|
1365
|
-
warn3({
|
|
1366
|
-
paneId,
|
|
1367
|
-
reason: "transition-unconfirmed",
|
|
1368
|
-
message: `send to ${paneId} submitted /new, but the new session did not become ready within ${readinessMs}ms. Wait for the pane to settle before sending more work.`
|
|
1369
|
-
});
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
1343
|
const probe = pasteProbe(text2);
|
|
1373
1344
|
if (probe.length === 0) return;
|
|
1374
1345
|
for (let attempt = 0; attempt <= SUBMIT_RETRIES; attempt++) {
|
|
@@ -4401,7 +4372,7 @@ async function spawnAgent(db, opts) {
|
|
|
4401
4372
|
agent = created.row;
|
|
4402
4373
|
const startAgent = (await activeMux()).startAgentInPane;
|
|
4403
4374
|
if (startAgent === void 0) {
|
|
4404
|
-
await awaitSpawnLiveness(paneId, opts.name,
|
|
4375
|
+
await awaitSpawnLiveness(paneId, opts.name, command);
|
|
4405
4376
|
} else {
|
|
4406
4377
|
await startAgent({ paneId, name: opts.name, cli, command, commandSource });
|
|
4407
4378
|
}
|
|
@@ -4510,7 +4481,7 @@ function detectSpawnStartupError(scrollback) {
|
|
|
4510
4481
|
}
|
|
4511
4482
|
return void 0;
|
|
4512
4483
|
}
|
|
4513
|
-
async function awaitSpawnLiveness(paneId, agentName,
|
|
4484
|
+
async function awaitSpawnLiveness(paneId, agentName, command) {
|
|
4514
4485
|
const ms = defaultSpawnLivenessMs();
|
|
4515
4486
|
if (ms === 0) return;
|
|
4516
4487
|
await sleep(ms);
|
|
@@ -4525,30 +4496,6 @@ async function awaitSpawnLiveness(paneId, agentName, workstreamName, command) {
|
|
|
4525
4496
|
throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
|
|
4526
4497
|
}
|
|
4527
4498
|
}
|
|
4528
|
-
await awaitSpawnReadiness(paneId, agentName, workstreamName, command);
|
|
4529
|
-
}
|
|
4530
|
-
function defaultSpawnReadinessMs() {
|
|
4531
|
-
const raw = process.env.MU_SPAWN_READINESS_MS;
|
|
4532
|
-
if (raw === void 0) return 1e4;
|
|
4533
|
-
const parsed = Number.parseInt(raw, 10);
|
|
4534
|
-
if (Number.isNaN(parsed) || parsed < 0) return 1e4;
|
|
4535
|
-
return parsed;
|
|
4536
|
-
}
|
|
4537
|
-
async function awaitSpawnReadiness(paneId, agentName, workstreamName, command) {
|
|
4538
|
-
const budgetMs = defaultSpawnReadinessMs();
|
|
4539
|
-
if (budgetMs === 0 || !murmurAvailable()) return;
|
|
4540
|
-
const mux = await activeMux();
|
|
4541
|
-
const deadline = Date.now() + budgetMs;
|
|
4542
|
-
const agent = { name: agentName, workstreamName, paneId };
|
|
4543
|
-
while (Date.now() < deadline) {
|
|
4544
|
-
if (!await mux.paneExists(paneId)) {
|
|
4545
|
-
const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
|
|
4546
|
-
throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
|
|
4547
|
-
}
|
|
4548
|
-
const reading = (await readAgentStates([agent])).get(agentKey(agent));
|
|
4549
|
-
if (reading?.source === "murmur") return;
|
|
4550
|
-
await sleep(Math.min(READINESS_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
|
|
4551
|
-
}
|
|
4552
4499
|
}
|
|
4553
4500
|
async function createOrReusePane(opts) {
|
|
4554
4501
|
const mux = await activeMux();
|
|
@@ -4594,11 +4541,10 @@ async function firstPaneInWindow(mux, session, windowId) {
|
|
|
4594
4541
|
}
|
|
4595
4542
|
return pane.paneId;
|
|
4596
4543
|
}
|
|
4597
|
-
var AGENT_NAME_CONVENTION_RE, execFileP, activeCommandResolver, CTL_POLL_INTERVAL_MS, STARTUP_ERROR_PATTERNS, STARTUP_ERROR_TAIL_LINES
|
|
4544
|
+
var AGENT_NAME_CONVENTION_RE, execFileP, activeCommandResolver, CTL_POLL_INTERVAL_MS, STARTUP_ERROR_PATTERNS, STARTUP_ERROR_TAIL_LINES;
|
|
4598
4545
|
var init_spawn = __esm({
|
|
4599
4546
|
"src/agents/spawn.ts"() {
|
|
4600
4547
|
"use strict";
|
|
4601
|
-
init_agent_state();
|
|
4602
4548
|
init_agents();
|
|
4603
4549
|
init_client();
|
|
4604
4550
|
init_path();
|
|
@@ -4644,12 +4590,11 @@ var init_spawn = __esm({
|
|
|
4644
4590
|
/^(?:.*: )?(?:command not found|No such file or directory)(?::\s*\S+)?$/i
|
|
4645
4591
|
];
|
|
4646
4592
|
STARTUP_ERROR_TAIL_LINES = 30;
|
|
4647
|
-
READINESS_POLL_INTERVAL_MS = 250;
|
|
4648
4593
|
}
|
|
4649
4594
|
});
|
|
4650
4595
|
|
|
4651
4596
|
// src/agents/errors.ts
|
|
4652
|
-
var AgentSpawnCliNotFoundError, AgentExistsError, AgentNotFoundError, AgentNotInWorkstreamError, AgentDiedOnSpawnError, AgentSpawnStartupError, WorkspacePreservedError, AgentCtlUnreachableError, AgentAbortNeedsCtlError, AgentAbortTimeoutError, AgentFreshNeedsCtlError, AgentExtensionOutdatedError, AgentBusyError;
|
|
4597
|
+
var AgentSpawnCliNotFoundError, AgentExistsError, AgentNotFoundError, AgentNotInWorkstreamError, AgentDiedOnSpawnError, AgentSpawnStartupError, WorkspacePreservedError, AgentCtlUnreachableError, AgentAbortNeedsCtlError, AgentAbortTimeoutError, AgentFreshNeedsCtlError, AgentExtensionOutdatedError, AgentBusyError, AgentSlashCommandUnsupportedError;
|
|
4653
4598
|
var init_errors = __esm({
|
|
4654
4599
|
"src/agents/errors.ts"() {
|
|
4655
4600
|
"use strict";
|
|
@@ -5029,23 +4974,47 @@ ${scrollback.trim()}
|
|
|
5029
4974
|
}
|
|
5030
4975
|
};
|
|
5031
4976
|
AgentBusyError = class extends Error {
|
|
5032
|
-
constructor(agentName, workstream) {
|
|
5033
|
-
super(`agent ${agentName} is busy: --fresh would abandon its running turn`);
|
|
4977
|
+
constructor(agentName, workstream, command) {
|
|
4978
|
+
super(`agent ${agentName} is busy: ${command ?? "--fresh"} would abandon its running turn`);
|
|
5034
4979
|
this.agentName = agentName;
|
|
5035
4980
|
this.workstream = workstream;
|
|
4981
|
+
this.command = command;
|
|
5036
4982
|
}
|
|
5037
4983
|
agentName;
|
|
5038
4984
|
workstream;
|
|
4985
|
+
command;
|
|
5039
4986
|
name = "AgentBusyError";
|
|
5040
4987
|
errorNextSteps() {
|
|
4988
|
+
const retry = this.command === void 0 ? `mu agent send ${this.agentName} --fresh --force '...' -w ${this.workstream}` : `mu agent send ${this.agentName} '${this.command}' --force -w ${this.workstream}`;
|
|
5041
4989
|
return [
|
|
5042
4990
|
{
|
|
5043
4991
|
intent: "Stop the turn first, then retry",
|
|
5044
4992
|
command: `mu agent abort ${this.agentName} -w ${this.workstream}`
|
|
5045
4993
|
},
|
|
4994
|
+
{ intent: "Or abandon the turn in one step", command: retry }
|
|
4995
|
+
];
|
|
4996
|
+
}
|
|
4997
|
+
};
|
|
4998
|
+
AgentSlashCommandUnsupportedError = class extends Error {
|
|
4999
|
+
constructor(agentName, workstream, text2, supported) {
|
|
5000
|
+
super(
|
|
5001
|
+
`agent ${agentName} runs pi: over the control socket mu runs only ${supported.join(", ")}; ${JSON.stringify(text2.split("\n")[0])} is not one of them. Pass --via mux to type it into the pane`
|
|
5002
|
+
);
|
|
5003
|
+
this.agentName = agentName;
|
|
5004
|
+
this.workstream = workstream;
|
|
5005
|
+
this.text = text2;
|
|
5006
|
+
this.supported = supported;
|
|
5007
|
+
}
|
|
5008
|
+
agentName;
|
|
5009
|
+
workstream;
|
|
5010
|
+
text;
|
|
5011
|
+
supported;
|
|
5012
|
+
name = "AgentSlashCommandUnsupportedError";
|
|
5013
|
+
errorNextSteps() {
|
|
5014
|
+
return [
|
|
5046
5015
|
{
|
|
5047
|
-
intent: "
|
|
5048
|
-
command: `mu agent send ${this.agentName}
|
|
5016
|
+
intent: "Type it into the pane on purpose (tmux paste; unconfirmed)",
|
|
5017
|
+
command: `mu agent send ${this.agentName} '${this.text.split("\n")[0]}' --via mux -w ${this.workstream}`
|
|
5049
5018
|
}
|
|
5050
5019
|
];
|
|
5051
5020
|
}
|
|
@@ -5062,10 +5031,21 @@ function agentCtlSocket(db, agent) {
|
|
|
5062
5031
|
function expectsCtl(agent, sock = ctlSocketPath(agent.workstreamName, agent.name)) {
|
|
5063
5032
|
return speaksMuCtl(agent.cli, resolveCliCommand(agent.cli)) || existsSync7(sock);
|
|
5064
5033
|
}
|
|
5065
|
-
function chooseTransport(agent,
|
|
5066
|
-
if (text2.startsWith("/")) return "mux";
|
|
5034
|
+
function chooseTransport(agent, sock) {
|
|
5067
5035
|
return expectsCtl(agent, sock) ? "ctl" : "mux";
|
|
5068
5036
|
}
|
|
5037
|
+
function parseSessionCommand(text2) {
|
|
5038
|
+
const t = text2.trim();
|
|
5039
|
+
if (t === "/new") return { name: "new" };
|
|
5040
|
+
if (t === "/reload") return { name: "reload" };
|
|
5041
|
+
const m = /^\/compact(?:\s+([\s\S]*))?$/.exec(t);
|
|
5042
|
+
if (!m) return void 0;
|
|
5043
|
+
const instructions = m[1]?.trim();
|
|
5044
|
+
return instructions ? { name: "compact", instructions } : { name: "compact" };
|
|
5045
|
+
}
|
|
5046
|
+
function isSlashCommand(text2) {
|
|
5047
|
+
return /^\/[A-Za-z][\w:-]*(?:\s|$)/.test(text2.trimStart());
|
|
5048
|
+
}
|
|
5069
5049
|
async function extensionOutdated(agent, sock, e) {
|
|
5070
5050
|
let extVersion;
|
|
5071
5051
|
try {
|
|
@@ -5094,11 +5074,23 @@ async function sendViaTransport(agent, text2, opts) {
|
|
|
5094
5074
|
if (opts.fresh && (opts.via === "mux" || !expectsCtl(agent, sock))) {
|
|
5095
5075
|
throw new AgentFreshNeedsCtlError(agent.name, agent.workstreamName, agent.cli);
|
|
5096
5076
|
}
|
|
5097
|
-
const transport = opts.fresh ? "ctl" : opts.via ?? chooseTransport(agent,
|
|
5077
|
+
const transport = opts.fresh ? "ctl" : opts.via ?? chooseTransport(agent, sock);
|
|
5098
5078
|
if (transport === "mux") {
|
|
5099
5079
|
await (await activeMux()).sendToPane(agent.paneId, text2, opts);
|
|
5100
5080
|
return { transport };
|
|
5101
5081
|
}
|
|
5082
|
+
if (!opts.fresh) {
|
|
5083
|
+
const cmd = parseSessionCommand(text2);
|
|
5084
|
+
if (cmd) return runSessionCommand(agent, sock, cmd, opts.force === true);
|
|
5085
|
+
if (isSlashCommand(text2)) {
|
|
5086
|
+
throw new AgentSlashCommandUnsupportedError(
|
|
5087
|
+
agent.name,
|
|
5088
|
+
agent.workstreamName,
|
|
5089
|
+
text2,
|
|
5090
|
+
CTL_SLASH_COMMANDS
|
|
5091
|
+
);
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5102
5094
|
const reply = await ctlRequestFor(
|
|
5103
5095
|
agent,
|
|
5104
5096
|
sock,
|
|
@@ -5112,14 +5104,36 @@ async function sendViaTransport(agent, text2, opts) {
|
|
|
5112
5104
|
}
|
|
5113
5105
|
return reply.state === void 0 ? { transport } : { transport, state: reply.state };
|
|
5114
5106
|
}
|
|
5107
|
+
async function runSessionCommand(agent, sock, cmd, force) {
|
|
5108
|
+
const reply = await ctlRequestFor(agent, sock, {
|
|
5109
|
+
op: "command",
|
|
5110
|
+
name: cmd.name,
|
|
5111
|
+
...cmd.instructions !== void 0 ? { instructions: cmd.instructions } : {},
|
|
5112
|
+
...force ? { force: true } : {}
|
|
5113
|
+
});
|
|
5114
|
+
if (!reply.ok) {
|
|
5115
|
+
if (reply.error === "busy") {
|
|
5116
|
+
throw new AgentBusyError(agent.name, agent.workstreamName, `/${cmd.name}`);
|
|
5117
|
+
}
|
|
5118
|
+
throw new Error(`pi refused /${cmd.name}: ${reply.error}`);
|
|
5119
|
+
}
|
|
5120
|
+
return {
|
|
5121
|
+
transport: "ctl",
|
|
5122
|
+
command: cmd.name,
|
|
5123
|
+
...reply.state !== void 0 ? { state: reply.state } : {}
|
|
5124
|
+
};
|
|
5125
|
+
}
|
|
5126
|
+
var CTL_SLASH_COMMANDS;
|
|
5115
5127
|
var init_transport = __esm({
|
|
5116
5128
|
"src/agents/transport.ts"() {
|
|
5117
5129
|
"use strict";
|
|
5118
5130
|
init_client();
|
|
5119
5131
|
init_path();
|
|
5132
|
+
init_protocol();
|
|
5120
5133
|
init_mux2();
|
|
5121
5134
|
init_errors();
|
|
5122
5135
|
init_spawn();
|
|
5136
|
+
CTL_SLASH_COMMANDS = CTL_COMMANDS.map((n) => `/${n}`);
|
|
5123
5137
|
}
|
|
5124
5138
|
});
|
|
5125
5139
|
|
|
@@ -9597,6 +9611,15 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
|
|
|
9597
9611
|
detail: version === null ? "agent state from murmur" : `agent state from murmur ${version}`
|
|
9598
9612
|
};
|
|
9599
9613
|
}
|
|
9614
|
+
function extFeatures(env) {
|
|
9615
|
+
const features = [
|
|
9616
|
+
["mu_delegate", "MU_DELEGATE"],
|
|
9617
|
+
["nudge", "MU_NUDGE"]
|
|
9618
|
+
];
|
|
9619
|
+
const on = ["ctl", ...features.filter(([, v]) => env[v] !== "0").map(([f]) => f)];
|
|
9620
|
+
const off = features.filter(([, v]) => env[v] === "0").map(([f, v]) => `${f} disabled by ${v}=0`);
|
|
9621
|
+
return `(${on.join(", ")}${off.length ? `; ${off.join("; ")}` : ""})`;
|
|
9622
|
+
}
|
|
9600
9623
|
function ctlExtensionDoctorCheck(opts) {
|
|
9601
9624
|
const { extension } = inspectLinks(opts);
|
|
9602
9625
|
switch (extension.state) {
|
|
@@ -9604,7 +9627,7 @@ function ctlExtensionDoctorCheck(opts) {
|
|
|
9604
9627
|
return {
|
|
9605
9628
|
name: "mu ext",
|
|
9606
9629
|
status: "ok",
|
|
9607
|
-
detail: process.env
|
|
9630
|
+
detail: extFeatures(process.env)
|
|
9608
9631
|
};
|
|
9609
9632
|
case "stale-copy":
|
|
9610
9633
|
return { name: "mu ext", status: "warn", detail: `stale copy: ${RELINK}` };
|
|
@@ -20117,7 +20140,9 @@ function parseVia(raw) {
|
|
|
20117
20140
|
}
|
|
20118
20141
|
async function cmdSend(db, rawName, text2, opts = {}) {
|
|
20119
20142
|
const via = parseVia(opts.via);
|
|
20120
|
-
if (opts.force && !opts.fresh
|
|
20143
|
+
if (opts.force && !opts.fresh && parseSessionCommand(text2) === void 0) {
|
|
20144
|
+
throw new UsageError("--force only applies with --fresh or a /new, /reload, /compact");
|
|
20145
|
+
}
|
|
20121
20146
|
if (opts.fresh && opts.steer) throw new UsageError("--fresh and --steer are mutually exclusive");
|
|
20122
20147
|
const { name } = await resolveEntityRef(db, rawName, opts, "agent");
|
|
20123
20148
|
assertAgentInWorkstream(db, name, opts.workstream);
|
|
@@ -20127,7 +20152,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
|
|
|
20127
20152
|
});
|
|
20128
20153
|
let undelivered;
|
|
20129
20154
|
const agentRow = getAgent(db, name, ws);
|
|
20130
|
-
const plainCtl = agentRow !== void 0 && !opts.fresh && !opts.steer && (via ?? chooseTransport(agentRow,
|
|
20155
|
+
const plainCtl = agentRow !== void 0 && !opts.fresh && !opts.steer && parseSessionCommand(text2) === void 0 && (via ?? chooseTransport(agentRow, agentCtlSocket(db, agentRow))) === "ctl";
|
|
20131
20156
|
const before = plainCtl && agentRow !== void 0 ? await ctlProbe(agentCtlSocket(db, agentRow)) : void 0;
|
|
20132
20157
|
const sent = await sendToAgent(db, name, text2, {
|
|
20133
20158
|
workstream: ws,
|
|
@@ -20161,6 +20186,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
|
|
|
20161
20186
|
sentBytes: text2.length,
|
|
20162
20187
|
transport: sent.transport,
|
|
20163
20188
|
fresh: opts.fresh === true,
|
|
20189
|
+
...sent.command !== void 0 ? { command: sent.command } : {},
|
|
20164
20190
|
...sent.state !== void 0 ? { state: sent.state } : {},
|
|
20165
20191
|
delivered: undelivered === void 0,
|
|
20166
20192
|
...undelivered !== void 0 ? { undelivered: { reason: undelivered.reason, message: undelivered.message } } : {},
|
|
@@ -20172,7 +20198,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
|
|
|
20172
20198
|
if (undelivered !== void 0) {
|
|
20173
20199
|
console.error(pc.yellow(`warning: ${undelivered.message}`));
|
|
20174
20200
|
} else {
|
|
20175
|
-
const how = sent.transport === "mux" ? "via mux paste" : opts.fresh ? "via ctl, fresh session" : `via ctl, pi ${sent.state ?? "?"}`;
|
|
20201
|
+
const how = sent.transport === "mux" ? "via mux paste" : opts.fresh ? "via ctl, fresh session" : sent.command !== void 0 ? `via ctl, pi ran /${sent.command}` : `via ctl, pi ${sent.state ?? "?"}`;
|
|
20176
20202
|
console.log(pc.dim(`sent ${text2.length} bytes to ${name} (${how})`));
|
|
20177
20203
|
}
|
|
20178
20204
|
printNextSteps(nextSteps3);
|
|
@@ -20613,14 +20639,17 @@ function wireAgentCommands(program) {
|
|
|
20613
20639
|
return handle((db) => cmdSpawn(db, name, opts), this)();
|
|
20614
20640
|
});
|
|
20615
20641
|
agent.command("send <name> <text>").description(
|
|
20616
|
-
"Send text to an agent. pi agents: through the control socket (fails loud if it does not answer; no paste fallback)
|
|
20642
|
+
"Send text to an agent. pi agents: through the control socket (fails loud if it does not answer; no paste fallback); '/new', '/reload' and '/compact [instructions]' run inside pi over the socket (refused while busy unless --force), any other slash command is refused (--via mux types it into the pane). Non-pi CLIs: pasted into the pane (atomic on herdr; bracketed-paste on tmux). --fresh (pi only): start a new session and send the text into it as one operation; returns once the prompt's run has started, so an immediate next send cannot be lost. Use it instead of sending '/new' then the prompt"
|
|
20617
20643
|
).option(
|
|
20618
20644
|
"--strict-staleness",
|
|
20619
20645
|
"refuse send when the target agent's workspace is stale (default: warn and proceed)"
|
|
20620
20646
|
).option("--steer", "if pi is busy, interrupt the current run (default: queue as a follow-up)").option("--via <transport>", "force the transport: ctl or mux (mux = paste into the pane)").option(
|
|
20621
20647
|
"--fresh",
|
|
20622
20648
|
"pi only: new session + this prompt, one operation (refused with exit 4 while pi is busy)"
|
|
20623
|
-
).option(
|
|
20649
|
+
).option(
|
|
20650
|
+
"--force",
|
|
20651
|
+
"with --fresh or a pi /new, /reload, /compact: abandon a running turn instead of refusing"
|
|
20652
|
+
).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name, text2) {
|
|
20624
20653
|
const opts = this.opts();
|
|
20625
20654
|
return handle((db) => cmdSend(db, name, text2, opts), this)();
|
|
20626
20655
|
});
|
|
@@ -21348,7 +21377,8 @@ var NameAmbiguousError = class extends Error {
|
|
|
21348
21377
|
function classifyError(err) {
|
|
21349
21378
|
if (err instanceof UsageError || // abort on a non-pi agent: the operator picked the wrong verb (kick).
|
|
21350
21379
|
err instanceof AgentAbortNeedsCtlError || // send --fresh on an agent with no control socket: wrong recipe.
|
|
21351
|
-
err instanceof AgentFreshNeedsCtlError ||
|
|
21380
|
+
err instanceof AgentFreshNeedsCtlError || // an unsupported slash command to a pi agent: --via mux is the recipe.
|
|
21381
|
+
err instanceof AgentSlashCommandUnsupportedError || err instanceof WorkstreamNameInvalidError || err instanceof WorkstreamNameReservedError || // The operator asked the ACTIVE backend for something it cannot do:
|
|
21352
21382
|
// a `--cli` the mux does not recognise as an agent kind, or a
|
|
21353
21383
|
// `--command` / `MU_<CLI>_COMMAND` override it cannot honour. The
|
|
21354
21384
|
// substrate is healthy and answered precisely, so this is the usage
|