@brutalsystems/muster 0.10.0 → 0.11.1
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 +38 -13
- package/dist/config.js +44 -1
- package/dist/guard.js +68 -14
- package/dist/help.js +8 -2
- package/dist/hosts/index.js +2 -2
- package/dist/hosts/tmux.js +28 -2
- package/dist/identity/processes.js +10 -0
- package/dist/muster.js +9 -1
- package/dist/run.js +513 -311
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ piped output and `TERM=dumb` are always uncolored. JSON never includes colors.
|
|
|
120
120
|
|
|
121
121
|
`run` accepts an optional `--` followed by runtime arguments. The normal
|
|
122
122
|
allowlist is `--model` / `-m`, plus Claude's `--effort` and OpenCode's `--agent`
|
|
123
|
-
and `--variant`. For Claude and Codex a model named there is
|
|
123
|
+
and `--variant`. For Claude and Codex a model named there is _consumed_ rather
|
|
124
124
|
than forwarded — it reaches the child through the same path as `--model`, so it
|
|
125
125
|
appears once — and naming a model both ways is refused rather than resolved by
|
|
126
126
|
precedence. Unknown options, bundled short options, config injection and
|
|
@@ -212,7 +212,7 @@ and exit-status lifecycle as other tasks.
|
|
|
212
212
|
|
|
213
213
|
## Repeating a launch request
|
|
214
214
|
|
|
215
|
-
`--request-key KEY` (MCP: `requestKey`) names the launch
|
|
215
|
+
`--request-key KEY` (MCP: `requestKey`) names the launch _request_, not the
|
|
216
216
|
session it produces. Sending the same key again returns the launch it already
|
|
217
217
|
produced instead of starting a second agent:
|
|
218
218
|
|
|
@@ -233,12 +233,12 @@ computed before validation would disagree with itself.
|
|
|
233
233
|
|
|
234
234
|
What a repeat returns depends on what the first attempt did:
|
|
235
235
|
|
|
236
|
-
| First attempt
|
|
237
|
-
|
|
|
238
|
-
| Reachable session or running task | That session or task, unchanged
|
|
239
|
-
| Still launching
|
|
240
|
-
| Failed, exited or stopped
|
|
241
|
-
| Outcome unknown
|
|
236
|
+
| First attempt | A repeat gets |
|
|
237
|
+
| --------------------------------- | ------------------------------------------ |
|
|
238
|
+
| Reachable session or running task | That session or task, unchanged |
|
|
239
|
+
| Still launching | Refused — ask for its status instead |
|
|
240
|
+
| Failed, exited or stopped | Refused, naming the recorded outcome |
|
|
241
|
+
| Outcome unknown | Refused, and told to inspect first (below) |
|
|
242
242
|
|
|
243
243
|
A repeat never consumes launch capacity, including when the concurrency cap is
|
|
244
244
|
full of the very launch being asked about. Omitting the key leaves behaviour
|
|
@@ -329,7 +329,7 @@ Muster records the group the launched root leads and takes the union. A stop
|
|
|
329
329
|
reaches reparented workers, and a session whose root has died but whose group is
|
|
330
330
|
still busy is reported as running rather than ended.
|
|
331
331
|
|
|
332
|
-
Only a group the root
|
|
332
|
+
Only a group the root _leads_ is used. A process that merely belongs to someone
|
|
333
333
|
else's group shares it with whatever else is there — a shell, a tmux pane, the
|
|
334
334
|
terminal — and treating that as membership would reach far outside the session.
|
|
335
335
|
Both terminal hosts start their root in a new session, so in practice it does
|
|
@@ -339,10 +339,21 @@ nothing on macOS prevents that.
|
|
|
339
339
|
## Configuration and permissions
|
|
340
340
|
|
|
341
341
|
Muster reads `~/.muster/config.toml` once at startup and **never writes it**.
|
|
342
|
+
|
|
343
|
+
A key this build does not know is reported and ignored, so a config written for
|
|
344
|
+
a newer Muster does not stop an older one from running — a strict top level is
|
|
345
|
+
what made every 0.10.0 command fail with `unrecognized_keys` the moment
|
|
346
|
+
`terminal` was added in 0.11.0. Values are still validated, and the nested
|
|
347
|
+
policy tables stay strict: a dropped `sandbox` value would launch under the
|
|
348
|
+
default rather than the pair that was written, and an unknown key inside a
|
|
349
|
+
requester profile is a policy statement that would not apply.
|
|
350
|
+
|
|
342
351
|
Missing configuration uses these defaults:
|
|
343
352
|
|
|
344
353
|
```toml
|
|
345
354
|
host = "auto"
|
|
355
|
+
tmux_status = "off"
|
|
356
|
+
terminal = "terminal"
|
|
346
357
|
launch_timeout_sec = 30
|
|
347
358
|
max_concurrent = 4
|
|
348
359
|
permissions = "deny"
|
|
@@ -472,10 +483,24 @@ without adding a retention policy nobody asked for.
|
|
|
472
483
|
|
|
473
484
|
### Containment levels
|
|
474
485
|
|
|
486
|
+
Two axes, answering different questions:
|
|
487
|
+
|
|
488
|
+
- **`--permissions` decides whether the agent stops to ask a human.** It
|
|
489
|
+
configures the runtime's own approval prompting — a setting the agent's
|
|
490
|
+
process honours.
|
|
491
|
+
- **`--sandbox` decides what the agent may touch, and the kernel enforces it.**
|
|
492
|
+
On macOS that is seatbelt. Nothing the agent does talks its way past it.
|
|
493
|
+
|
|
494
|
+
The difference is not academic: a permissions refusal arrives as a prompt or a
|
|
495
|
+
tool error the agent can read and react to, while a sandbox refusal arrives as
|
|
496
|
+
`EPERM` from the kernel, with no negotiation and often no explanation the agent
|
|
497
|
+
can interpret. `enforcement` below records which of the two a launch actually
|
|
498
|
+
got.
|
|
499
|
+
|
|
475
500
|
`--level` names a legal `--permissions` / `--sandbox` pair. It is a spelling,
|
|
476
501
|
not a second policy path, and naming both a level and either flag is an error.
|
|
477
502
|
|
|
478
|
-
| Level | Equivalent to
|
|
503
|
+
| Level | Equivalent to |
|
|
479
504
|
| ------ | ------------------------------------------------------------------------------------- |
|
|
480
505
|
| `read` | `--permissions deny --sandbox read-only` |
|
|
481
506
|
| `work` | `--permissions auto --sandbox workspace-write` |
|
|
@@ -487,7 +512,7 @@ through the flags.
|
|
|
487
512
|
Every launch also records how strongly its containment was imposed:
|
|
488
513
|
|
|
489
514
|
| `enforcement` | Meaning |
|
|
490
|
-
| ------------- |
|
|
515
|
+
| ------------- | --------------------------------------------------------------------------- |
|
|
491
516
|
| `kernel` | Codex or Claude Code with an OS sandbox configured and active. |
|
|
492
517
|
| `tool-policy` | OpenCode — rules the agent is asked to respect, with no kernel enforcement. |
|
|
493
518
|
| `none` | `full-access` — the sandbox is deliberately disabled. |
|
|
@@ -739,7 +764,7 @@ configuration.
|
|
|
739
764
|
|
|
740
765
|
Five things worth stating plainly, because each is a sharp edge:
|
|
741
766
|
|
|
742
|
-
1. **`muster identities` reports
|
|
767
|
+
1. **`muster identities` reports _configured_, never _valid_.** A keychain
|
|
743
768
|
item, an auth file, a stored token or a set variable proves a credential
|
|
744
769
|
exists — not that it still works. Tokens expire and are revoked; there is
|
|
745
770
|
no way to check that without exercising the credential, and `identities`
|
|
@@ -777,7 +802,7 @@ Five things worth stating plainly, because each is a sharp edge:
|
|
|
777
802
|
about bounding.
|
|
778
803
|
|
|
779
804
|
A Claude launch under an identity also marks the launch's working directory
|
|
780
|
-
trusted
|
|
805
|
+
trusted _inside that launch's own copy_ of the configuration, so a brand-new
|
|
781
806
|
directory starts as a registered session without anyone accepting a trust
|
|
782
807
|
dialog. This is distinct from the ordinary Claude prerequisite described
|
|
783
808
|
above: it only ever writes to the per-launch copy, never to the operator's own
|
package/dist/config.js
CHANGED
|
@@ -64,6 +64,20 @@ const sessionSectionSchema = z
|
|
|
64
64
|
export const configSchema = z
|
|
65
65
|
.object({
|
|
66
66
|
host: z.enum(["auto", "tmux", "pty", "macos-terminal"]).default("auto"),
|
|
67
|
+
/**
|
|
68
|
+
* Off by default. Muster's tmux runs on its own server with `-f /dev/null`,
|
|
69
|
+
* so this bar is never the developer's configured one, and a Muster session
|
|
70
|
+
* is one window running one agent.
|
|
71
|
+
*/
|
|
72
|
+
tmux_status: z.enum(["on", "off"]).default("off"),
|
|
73
|
+
/**
|
|
74
|
+
* Which terminal `--open` uses when the launch does not name one. Was
|
|
75
|
+
* hardcoded to Terminal.app, so a developer on Ghostty or iTerm2 had to pass
|
|
76
|
+
* `--terminal` on every launch with no way to state it once.
|
|
77
|
+
*/
|
|
78
|
+
terminal: z
|
|
79
|
+
.enum(["auto", "terminal", "iterm2", "ghostty"])
|
|
80
|
+
.default("terminal"),
|
|
67
81
|
launch_timeout_sec: z.number().finite().positive().max(600).default(30),
|
|
68
82
|
max_concurrent: z.number().int().positive().max(1000).default(4),
|
|
69
83
|
allowed_roots: z.array(z.string().min(1)).default([]),
|
|
@@ -91,7 +105,32 @@ export const configSchema = z
|
|
|
91
105
|
.default({}),
|
|
92
106
|
requesters: z.array(requesterEnrolmentSchema).default([]),
|
|
93
107
|
})
|
|
94
|
-
.
|
|
108
|
+
// Deliberately NOT strict, unlike the nested schemas above. A config written
|
|
109
|
+
// for a newer Muster must not brick an older one: when `terminal` arrived in
|
|
110
|
+
// 0.11.0 a strict top level made EVERY command on 0.10.0 fail with
|
|
111
|
+
// `unrecognized_keys` — not a warning, a dead CLI, `list` and `stop`
|
|
112
|
+
// included. Unknown keys here are reported by `unknownConfigKeys` and
|
|
113
|
+
// dropped.
|
|
114
|
+
//
|
|
115
|
+
// Values are still validated, which is the half that matters for safety: a
|
|
116
|
+
// silently ignored `sandbox = "workspce-write"` would launch under the
|
|
117
|
+
// default rather than the pair the operator wrote. The nested policy tables
|
|
118
|
+
// stay strict for the same reason — an unknown key inside a requester
|
|
119
|
+
// profile is a policy statement that would not apply.
|
|
120
|
+
.strip();
|
|
121
|
+
const knownConfigKeys = new Set(Object.keys(configSchema.shape));
|
|
122
|
+
/**
|
|
123
|
+
* Top-level keys this build does not know, sorted. Reported rather than
|
|
124
|
+
* refused, so a newer config degrades to "this Muster ignores that" instead of
|
|
125
|
+
* refusing to run at all.
|
|
126
|
+
*/
|
|
127
|
+
export function unknownConfigKeys(raw) {
|
|
128
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
129
|
+
return [];
|
|
130
|
+
return Object.keys(raw)
|
|
131
|
+
.filter((k) => !knownConfigKeys.has(k))
|
|
132
|
+
.sort();
|
|
133
|
+
}
|
|
95
134
|
export const musterHome = () => join(homedir(), ".muster");
|
|
96
135
|
export async function loadConfig(home = musterHome()) {
|
|
97
136
|
let value = {};
|
|
@@ -102,6 +141,10 @@ export async function loadConfig(home = musterHome()) {
|
|
|
102
141
|
if (e.code !== "ENOENT")
|
|
103
142
|
throw e;
|
|
104
143
|
}
|
|
144
|
+
const ignored = unknownConfigKeys(value);
|
|
145
|
+
if (ignored.length)
|
|
146
|
+
process.stderr.write(`[muster] ignoring unknown config key${ignored.length > 1 ? "s" : ""}: ` +
|
|
147
|
+
`${ignored.join(", ")} — written for a different version of muster?\n`);
|
|
105
148
|
const result = configSchema.parse(value);
|
|
106
149
|
const seen = new Set();
|
|
107
150
|
for (const e of result.requesters) {
|
package/dist/guard.js
CHANGED
|
@@ -2,8 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { realpath } from "node:fs/promises";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
5
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { claudeConfigDir } from "./identity/claude.js";
|
|
7
7
|
import { mcpName, claudeMcpConfig } from "./mcp.js";
|
|
8
8
|
import { pluginName } from "./plugins.js";
|
|
9
9
|
import { RequesterConfigError } from "./requester-policy.js";
|
|
@@ -318,18 +318,6 @@ export function runtimeArgs(req) {
|
|
|
318
318
|
}
|
|
319
319
|
return result;
|
|
320
320
|
}
|
|
321
|
-
/**
|
|
322
|
-
* The config dir the launched agent will actually use. `launchEnv`'s blocklist
|
|
323
|
-
* is /^(CLAUDECODE|CLAUDE_CODE_|...)/, which does not match CLAUDE_CONFIG_DIR,
|
|
324
|
-
* so the child inherits whatever the caller set and this resolves to the same
|
|
325
|
-
* dir the child will open.
|
|
326
|
-
*/
|
|
327
|
-
export function claudeConfigDir(env) {
|
|
328
|
-
const configured = env.CLAUDE_CONFIG_DIR;
|
|
329
|
-
if (configured && configured.trim().length > 0)
|
|
330
|
-
return resolve(configured);
|
|
331
|
-
return join(env.HOME ?? homedir(), ".claude");
|
|
332
|
-
}
|
|
333
321
|
/**
|
|
334
322
|
* The keys of a profile's own settings.json that name its skill set rather than
|
|
335
323
|
* its policy. `--setting-sources ""` is what keeps `permissions`, `sandbox` and
|
|
@@ -358,6 +346,61 @@ function profilePluginSettings(configDir) {
|
|
|
358
346
|
out[key] = source[key];
|
|
359
347
|
return out;
|
|
360
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Whether the caller typed `--terminal` themselves, read from the raw argv
|
|
351
|
+
* BEFORE `normalizeOpenFlag` runs. Every `--open <app>` becomes `--terminal
|
|
352
|
+
* <app>`, so after normalisation the two are indistinguishable and a deprecation
|
|
353
|
+
* notice would fire on the spelling Muster is steering callers towards.
|
|
354
|
+
*/
|
|
355
|
+
export function usesTerminalFlag(argv) {
|
|
356
|
+
const end = argv.indexOf("--");
|
|
357
|
+
return (end === -1 ? argv : argv.slice(0, end)).some((a) => a === "--terminal" || a.startsWith("--terminal="));
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Lifts `--open <app>` and `--open=<app>` onto `--terminal <app>` before
|
|
361
|
+
* `parseArgs` sees them, so `--open` can carry an optional value at all: in
|
|
362
|
+
* strict mode a boolean option refuses `=value` and a string option refuses a
|
|
363
|
+
* bare flag, and there is no third kind. Everything after `--` belongs to the
|
|
364
|
+
* runtime and is copied through untouched.
|
|
365
|
+
*
|
|
366
|
+
* An unrecognised app is lifted too rather than left behind, so the schema names
|
|
367
|
+
* it in the error instead of it becoming a silent runtime argument.
|
|
368
|
+
*/
|
|
369
|
+
export function normalizeOpenFlag(argv) {
|
|
370
|
+
const out = [];
|
|
371
|
+
for (let i = 0; i < argv.length; i++) {
|
|
372
|
+
const arg = argv[i];
|
|
373
|
+
if (arg === "--") {
|
|
374
|
+
out.push(...argv.slice(i));
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
if (arg.startsWith("--open=")) {
|
|
378
|
+
out.push("--open", "--terminal", arg.slice("--open=".length));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (arg === "--open") {
|
|
382
|
+
const next = argv[i + 1];
|
|
383
|
+
if (next !== undefined && next !== "--" && !next.startsWith("-")) {
|
|
384
|
+
out.push("--open", "--terminal", next);
|
|
385
|
+
i++;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
out.push(arg);
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Which terminal `--open` uses: the launch's own choice, else config's, and
|
|
395
|
+
* Terminal.app only when neither names one — it is the one terminal every macOS
|
|
396
|
+
* is guaranteed to have. `auto` is a request to decide rather than a literal
|
|
397
|
+
* app, so it defers to config exactly as an absent value does.
|
|
398
|
+
*/
|
|
399
|
+
export function resolveTerminal(req, config) {
|
|
400
|
+
if (req.terminal && req.terminal !== "auto")
|
|
401
|
+
return req.terminal;
|
|
402
|
+
return config.terminal === "auto" ? "terminal" : config.terminal;
|
|
403
|
+
}
|
|
361
404
|
export function launchArgs(req, config, mcp = [], bridgePath, env = process.env) {
|
|
362
405
|
// Before `runtimeArgs`, so this function owns every model error on its own
|
|
363
406
|
// rather than by its caller's discipline: `Muster.launch` happens to resolve
|
|
@@ -399,6 +442,12 @@ export function launchArgs(req, config, mcp = [], bridgePath, env = process.env)
|
|
|
399
442
|
: policy.permissions === "bypass"
|
|
400
443
|
? "bypassPermissions"
|
|
401
444
|
: "dontAsk";
|
|
445
|
+
// The dir the launched agent will actually open: `launchEnv`'s blocklist is
|
|
446
|
+
// /^(CLAUDECODE|CLAUDE_CODE_|...)/, which does not match CLAUDE_CONFIG_DIR, so
|
|
447
|
+
// the child inherits whatever the caller set. Shared with the trust read and
|
|
448
|
+
// the sessions dir rather than restated — a second copy here drifted from that
|
|
449
|
+
// one already (it resolve()d, and read a blank value as unset), which is how a
|
|
450
|
+
// write root ends up naming a directory nobody is launching in.
|
|
402
451
|
const configDir = claudeConfigDir(env);
|
|
403
452
|
const settings = {
|
|
404
453
|
...profilePluginSettings(configDir),
|
|
@@ -425,8 +474,13 @@ export function launchArgs(req, config, mcp = [], bridgePath, env = process.env)
|
|
|
425
474
|
// failed with EPERM while the in-process file tools succeeded (#80).
|
|
426
475
|
// Granted only under workspace-write: read-only must stay read-only,
|
|
427
476
|
// and full-access has no sandbox to grant into.
|
|
477
|
+
// Absolute only here: the sandbox is an OS boundary evaluated against a
|
|
478
|
+
// path, and a relative CLAUDE_CONFIG_DIR would grant whatever the
|
|
479
|
+
// launching process's cwd happened to be. The shared resolver stays
|
|
480
|
+
// as-written so the trust read and the sessions dir keep their
|
|
481
|
+
// behaviour.
|
|
428
482
|
...(policy.sandbox === "workspace-write"
|
|
429
|
-
? { allowWrite: [configDir] }
|
|
483
|
+
? { allowWrite: [resolve(configDir)] }
|
|
430
484
|
: {}),
|
|
431
485
|
},
|
|
432
486
|
},
|
package/dist/help.js
CHANGED
|
@@ -43,8 +43,14 @@ export const COMMANDS = [
|
|
|
43
43
|
["--kind KIND", "session (default) or task"],
|
|
44
44
|
["--host ID", "tmux, pty, or auto (default: tmux, then pty)"],
|
|
45
45
|
["--identity NAME", "run as a configured identity"],
|
|
46
|
-
[
|
|
47
|
-
|
|
46
|
+
[
|
|
47
|
+
"--open [APP]",
|
|
48
|
+
"display the tmux session in a terminal (macOS); APP is terminal, iterm2 or ghostty, defaulting to the configured terminal",
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"--terminal APP",
|
|
52
|
+
"deprecated, removed in v2: pass the app to --open instead",
|
|
53
|
+
],
|
|
48
54
|
[
|
|
49
55
|
"--level LEVEL",
|
|
50
56
|
"read, work, or open: a legal permissions and sandbox pair",
|
package/dist/hosts/index.js
CHANGED
|
@@ -9,9 +9,9 @@ import { MacosTerminalHost } from "./macos-terminal.js";
|
|
|
9
9
|
* muster` and stay there, since a tmux launch is meant to survive its parent
|
|
10
10
|
* exiting (#57).
|
|
11
11
|
*/
|
|
12
|
-
export function hosts(env = process.env) {
|
|
12
|
+
export function hosts(env = process.env, tmuxStatus = "off") {
|
|
13
13
|
return [
|
|
14
|
-
new TmuxHost(env.MUSTER_TMUX_SERVER || undefined),
|
|
14
|
+
new TmuxHost(env.MUSTER_TMUX_SERVER || undefined, tmuxStatus),
|
|
15
15
|
new PtyHost(),
|
|
16
16
|
new MacosTerminalHost(),
|
|
17
17
|
];
|
package/dist/hosts/tmux.js
CHANGED
|
@@ -72,9 +72,19 @@ export function tmuxCommand(argv) {
|
|
|
72
72
|
}
|
|
73
73
|
export class TmuxHost {
|
|
74
74
|
server;
|
|
75
|
+
status;
|
|
75
76
|
id = "tmux";
|
|
76
|
-
|
|
77
|
+
/**
|
|
78
|
+
* `status` is set explicitly on every session rather than left to tmux's
|
|
79
|
+
* default, so the bar's state is what Muster asked for whatever version of
|
|
80
|
+
* tmux is installed. Off by default: Muster's server is its own (`-L`, with
|
|
81
|
+
* `-f /dev/null`), so the bar here is never the developer's configured one,
|
|
82
|
+
* and a session is one window running one agent — the bar reports only what
|
|
83
|
+
* the caller already knows. `tmux_status = "on"` in config.toml restores it.
|
|
84
|
+
*/
|
|
85
|
+
constructor(server = "muster", status = "off") {
|
|
77
86
|
this.server = server;
|
|
87
|
+
this.status = status;
|
|
78
88
|
if (!/^[a-zA-Z0-9_-]+$/.test(server))
|
|
79
89
|
throw new Error("Invalid tmux server");
|
|
80
90
|
}
|
|
@@ -155,7 +165,23 @@ export class TmuxHost {
|
|
|
155
165
|
try {
|
|
156
166
|
// Window selection is session-wide: sharing a session makes every
|
|
157
167
|
// attached terminal follow whichever agent was opened most recently.
|
|
158
|
-
const
|
|
168
|
+
const session = `muster-${randomUUID()}`;
|
|
169
|
+
// One tmux invocation, not two: a second round trip would leave the bar
|
|
170
|
+
// visible for as long as it took, and `;` keeps set-option in the same
|
|
171
|
+
// command list. Targeted by session name rather than relying on "current",
|
|
172
|
+
// which is not meaningful for a detached session.
|
|
173
|
+
const output = await this.call([
|
|
174
|
+
"new-session",
|
|
175
|
+
"-s",
|
|
176
|
+
session,
|
|
177
|
+
...common,
|
|
178
|
+
";",
|
|
179
|
+
"set-option",
|
|
180
|
+
"-t",
|
|
181
|
+
session,
|
|
182
|
+
"status",
|
|
183
|
+
this.status,
|
|
184
|
+
], opts.deadline);
|
|
159
185
|
return parseWindowRow(output);
|
|
160
186
|
}
|
|
161
187
|
catch (e) {
|
|
@@ -126,6 +126,16 @@ function signal(ref, name) {
|
|
|
126
126
|
* Reparented descendants are reachable only through the process group, so pass
|
|
127
127
|
* one when the caller has recorded it; the parent chain alone cannot see them.
|
|
128
128
|
*/
|
|
129
|
+
/**
|
|
130
|
+
* Signals a process tree dead: SIGTERM deepest-first, a grace period, a second
|
|
131
|
+
* pass for anything forked during it, then SIGKILL.
|
|
132
|
+
*
|
|
133
|
+
* Resolves once the signals are SENT, not once the processes are gone. SIGKILL
|
|
134
|
+
* is asynchronous, so a pid may still be in the process table when this returns
|
|
135
|
+
* — callers that need the stronger guarantee have to poll. Nothing does today:
|
|
136
|
+
* `stop` marks the entry stopped, and `refreshed` re-probes only entries still
|
|
137
|
+
* marked running, so the window cannot flip a stopped session back to alive.
|
|
138
|
+
*/
|
|
129
139
|
export async function stopTree(ref, group) {
|
|
130
140
|
if (!(await isSame(ref)))
|
|
131
141
|
return;
|
package/dist/muster.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loadConfig, musterHome } from "./config.js";
|
|
|
13
13
|
import { describeIdentities, interactiveSetup, setupIdentity, } from "./identity-cli.js";
|
|
14
14
|
import { IdentityError } from "./identity-store.js";
|
|
15
15
|
import { MCP_TOOLS } from "./mcp-tools.js";
|
|
16
|
+
import { normalizeOpenFlag, usesTerminalFlag } from "./guard.js";
|
|
16
17
|
import { commandHelp, topLevelHelp } from "./help.js";
|
|
17
18
|
import { promptForMissing, terminalReader } from "./prompt.js";
|
|
18
19
|
const listSchema = z
|
|
@@ -147,8 +148,13 @@ async function main() {
|
|
|
147
148
|
}
|
|
148
149
|
try {
|
|
149
150
|
const command = argv.shift();
|
|
151
|
+
// Read before normalisation, which turns `--open <app>` into `--terminal
|
|
152
|
+
// <app>` and would otherwise make the notice fire on the replacement.
|
|
153
|
+
const typedTerminalFlag = usesTerminalFlag(argv);
|
|
150
154
|
const { values: parsed, positionals } = parseArgs({
|
|
151
|
-
|
|
155
|
+
// `--open` carries an optional terminal name, which parseArgs has no kind
|
|
156
|
+
// for; the value is lifted onto `--terminal` first. See normalizeOpenFlag.
|
|
157
|
+
args: normalizeOpenFlag(argv),
|
|
152
158
|
allowPositionals: true,
|
|
153
159
|
strict: true,
|
|
154
160
|
options: {
|
|
@@ -221,6 +227,8 @@ async function main() {
|
|
|
221
227
|
? formatHuman(value, humanColorEnabled(process.stdout.isTTY, process.env))
|
|
222
228
|
: JSON.stringify(value) + "\n");
|
|
223
229
|
if (command === "run") {
|
|
230
|
+
if (typedTerminalFlag)
|
|
231
|
+
process.stderr.write("[muster] --terminal is deprecated and will be removed in v2; pass the app to --open instead, or set terminal in config.toml\n");
|
|
224
232
|
const runtime = positionals[0];
|
|
225
233
|
const args = positionals.slice(1);
|
|
226
234
|
const record = await muster.run({
|