@mu-crew/muzzle 0.1.2 → 0.2.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/bin/muzzle CHANGED
@@ -34,8 +34,12 @@ try {
34
34
  if (typeof process.execve === "function") {
35
35
  try {
36
36
  process.execve(binary, [binary, ...process.argv.slice(2)], process.env);
37
- } catch {
38
- // fall through to spawn, which reports the error
37
+ } catch (error) {
38
+ // Say so: the spawn below brings back the group-kill leak (D16), and
39
+ // silently is how it went unnoticed. Spawn then reports any real error.
40
+ console.error(
41
+ `muzzle: execve failed (${error.code ?? error.message}), falling back to spawn: a group kill may leak the job`,
42
+ );
39
43
  }
40
44
  }
41
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mu-crew/muzzle",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Per-host admission for heavy commands, with a run log that tunes itself",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,8 +42,8 @@
42
42
  }
43
43
  },
44
44
  "optionalDependencies": {
45
- "@mu-crew/muzzle-linux-x64": "0.1.2",
46
- "@mu-crew/muzzle-linux-arm64": "0.1.2",
47
- "@mu-crew/muzzle-darwin-arm64": "0.1.2"
45
+ "@mu-crew/muzzle-linux-x64": "0.2.0",
46
+ "@mu-crew/muzzle-linux-arm64": "0.2.0",
47
+ "@mu-crew/muzzle-darwin-arm64": "0.2.0"
48
48
  }
49
49
  }
@@ -60,6 +60,14 @@ muzzle run --rule gate --run-timeout 900 -- contrib/fast-gate
60
60
  `MUZZLE_SLOTS` from the claim and overwrites any it inherits. Drop
61
61
  hand-set caps from the command; an assignment inside the command string
62
62
  still wins.
63
+ - **Wrap a script as a whole.** muzzle can't see inside a script, so a
64
+ script you write that runs builds or tests (`./check.sh`, `python3
65
+ /tmp/x.py`, a heredoc) matches no rule and runs unclaimed. Wrap the
66
+ whole script under the rule of the heaviest thing it runs:
67
+ `muzzle run --rule cargo-test -- ./check.sh`, or
68
+ `muzzle run --rule cargo-test -- bash -c 'cargo build && cargo test'`.
69
+ Don't move a gate into a script to skip the queue: the machine still
70
+ pays for it, and every other agent's gate slows down.
63
71
  - A background launch (`(gate > log 2>&1 &)`) keeps its slots: `muzzle run`
64
72
  returns at once and a keeper holds the claim until the job exits. Polling
65
73
  the log is fine. A poll that repeats the gate's command line would claim a
@@ -98,6 +106,30 @@ muzzle run --rule gate --run-timeout 900 -- contrib/fast-gate
98
106
  running in the container. Its slots stay held until it exits, so find
99
107
  and kill it there.
100
108
 
109
+ ## The config
110
+
111
+ `~/.config/muzzle/config.toml` holds global `[[rule]]` entries. A
112
+ `[path."<dir>"]` table changes rules for the repos under a directory, and
113
+ `include = ["machine.toml"]` merges a per-machine file. Example:
114
+
115
+ ```toml
116
+ include = ["machine.toml"]
117
+
118
+ [[rule]]
119
+ name = "cargo-test"
120
+ match = '\bcargo (test|nextest|clippy|build)\b'
121
+ slots = 3
122
+
123
+ [path."~/hacking/crew"]
124
+ [[path."~/hacking/crew".rule]]
125
+ name = "cargo-test"
126
+ slots = 2 # reweight only; match is inherited
127
+ ```
128
+
129
+ There is no discovery mode. To set muzzle up on a machine, write the rules
130
+ from the heavy commands you know, check them with `muzzle match`, and tune
131
+ from the run log: [recipes/tune.md](recipes/tune.md).
132
+
101
133
  ## Recipes
102
134
 
103
135
  - Changing a rule's weight or pattern, reading `muzzle report`:
@@ -8,6 +8,19 @@ under, so `report --compare` can split by them later.
8
8
  Run on the host whose config you are changing. `runs.jsonl` is local machine
9
9
  state: read it here, and never publish it.
10
10
 
11
+ ## 0. A new machine: write the config, then trial it
12
+
13
+ There is no discovery mode. Start from what you know:
14
+
15
+ 1. List the heavy commands agents run here: gates, `cargo test`, `npm test`,
16
+ e2e suites. museum helps (step 2).
17
+ 2. Write a rule for each, in the shared file, with a weight from the
18
+ command's usual job cap (`CARGO_BUILD_JOBS=4` means 4). Check each
19
+ pattern with `muzzle match -- 'contrib/gate --full'`.
20
+ 3. Put this machine's budget and weights in an untracked `machine.toml` next
21
+ to it (layout in step 3), so one shared file serves every machine.
22
+ 4. Run real work for a day or two, then start the loop at step 1.
23
+
11
24
  ## 1. Read the report, machine figures first
12
25
 
13
26
  ```sh
@@ -22,6 +35,7 @@ Read the machine section before the rows:
22
35
  | CPU efficiency (low) | claims are bigger than the jobs use: weights too big |
23
36
  | utilisation near 100% while the machine's CPU stays low | the budget is saturated but the cores aren't: the reserve is too big |
24
37
  | queue non-empty | how often anyone waited at all. Near 0% means the weights cost nothing yet |
38
+ | unclaimed load (cores) | load that no claim accounts for: `load1 − held` at admission. High means heavy work runs outside muzzle. Find the unmatched heavy commands (step 2) and add rules, or raise `reserve` to leave room for them. It is rough: the load average lags, and on Linux it counts I/O wait |
25
39
 
26
40
  Then the rows. `suggest` names its evidence and carries a tag:
27
41
 
@@ -38,6 +52,24 @@ Then the rows. `suggest` names its evidence and carries a tag:
38
52
  Narrow to one rule with `--rule NAME`, or to a period with `--since D`
39
53
  and `--until D` (`now`, `7d`, `12h`, `30m`, `YYYY-MM-DD`, or RFC 3339).
40
54
 
55
+ ### Per repo
56
+
57
+ One rule can behave very differently per repo: a `cargo test` that takes
58
+ 1 s in one repo can take 30 s in another. Split the rows by repo:
59
+
60
+ ```sh
61
+ muzzle report --by repo --since 7d --rule cargo-test
62
+ ```
63
+
64
+ Each row is one (rule, repo); `(none)` is runs outside any repo. The
65
+ `now` column is the slots the **current** config gives that repo, and
66
+ where they come from: `now 3 (from [path."~/hacking/crew"] in
67
+ machine.toml)`. When the runs used a different weight, it adds `; runs used
68
+ 4`. `not in the current config here` means the rule is off or absent for
69
+ that repo now, so there is no suggestion. When suggestions for one rule
70
+ differ a lot between repos, give the outliers a path table (step 3).
71
+ `--by repo` can't be combined with `--compare`.
72
+
41
73
  **Done when** you have written down, for the rule you will change, its
42
74
  current slots, run p50/p90, cores p50/p90 and the suggestion.
43
75
 
@@ -54,11 +86,53 @@ Check a candidate against the live config with
54
86
  `muzzle match -- 'contrib/gate --full'`. It prints the rule, or `no rule
55
87
  matches`.
56
88
 
89
+ Heavy work inside a script (`./run.sh`, `python3 /tmp/x.py`) matches no
90
+ rule, because muzzle only sees the script's name. A high unclaimed load
91
+ with no unmatched commands in museum usually means this. The fix is
92
+ wrapping such scripts with `muzzle run --rule cargo-test -- ./run.sh`
93
+ (SKILL.md), not a rule for every script name.
94
+
57
95
  ## 3. Edit the config
58
96
 
59
97
  `~/.config/muzzle/config.toml`. Change one rule per round, so the next
60
98
  report shows what that change did.
61
99
 
100
+ Where a change goes:
101
+
102
+ ```toml
103
+ # ~/.config/muzzle/config.toml: shared, tracked in dotfiles
104
+ include = ["machine.toml"]
105
+
106
+ [[rule]]
107
+ name = "cargo-test"
108
+ match = '\bcargo (test|nextest|clippy|build)\b'
109
+ slots = 3
110
+
111
+ [path."~/hacking/swayward-wm"] # every repo under here, every worktree
112
+ [[path."~/hacking/swayward-wm".rule]]
113
+ name = "cargo-test"
114
+ slots = 6 # reweight only; match inherited
115
+ ```
116
+
117
+ ```toml
118
+ # ~/.config/muzzle/machine.toml: this machine only, untracked
119
+ reserve = 2
120
+ [[rule]]
121
+ name = "cargo-test"
122
+ slots = 4 # this machine's global weight
123
+ ```
124
+
125
+ - **A weight for every repo on this machine**: the global rule in
126
+ `machine.toml`.
127
+ - **A weight for some repos**: a `[path."<dir>"]` table with just `name`
128
+ and `slots`. The key is a directory; it covers every repo under it and
129
+ all their worktrees. A nearer table wins over a further one.
130
+ - **A rule that should not fire in one repo**: `enabled = false` in its
131
+ table.
132
+ - After the edit, `muzzle match --json --cwd ~/hacking/swayward-wm/swayward
133
+ -- 'cargo test'` shows the slots there and `slots_from`, the file and
134
+ table they came from.
135
+
62
136
  - **Weights**: set `slots` to the suggestion. For an `experiment`, that
63
137
  is the `try N` value, and step 5 decides whether it stays.
64
138
  - **New rules**: give each a `name`. The name is the label in reports and
@@ -83,7 +157,9 @@ Check the edit loads, and note the new hash:
83
157
  muzzle doctor
84
158
  ```
85
159
 
86
- A bad config exits 2 and names the key.
160
+ A bad config exits 2 and names the key and the file. `doctor` also lists
161
+ each include as `found` or `not found`: a typo in an include path is
162
+ skipped silently otherwise.
87
163
 
88
164
  **Hand-wrapped calls carry their own weight.** A `--slots` on
89
165
  `muzzle run` overrides the config, so editing the config changes nothing for briefs,
@@ -117,6 +193,10 @@ After a few days, or at least 5 clean runs at the new weight:
117
193
  muzzle report --compare 2026-11-01..2026-11-03 2026-11-04..now --rule cargo-test
118
194
  ```
119
195
 
196
+ For a path-table change, compare per repo instead: run
197
+ `muzzle report --by repo --since 2026-11-04 --rule cargo-test` and check
198
+ the repo's row against the one you wrote down in step 1.
199
+
120
200
  The output shows both periods, split by config hash when it changed inside
121
201
  one, then an `A -> C` table of deltas. An experiment reads `confirmed` when
122
202
  run p50 fell, `rejected` when it didn't.