mu-agent 0.2.4 → 0.2.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.6] - 2026-10-24
6
+
7
+ **0.2.6 shows the note that Laya was replaced when a person sends a message, keeps mude 1.0 local only when it's installed again, and stops `mu doctor` failing on a machine without Chrome.**
8
+
9
+ ### Fixed
10
+
11
+ - The `laya_removed` notice waits for a person. In 0.2.5 the desktop backend's health check of mu (a session that never gets a prompt) was the first session with a UI: it took Laya out of mu.json and got the notice about 3 s after the app started, so nobody saw it. mu.json still loses Laya at the first session start with a terminal or the desktop app, but the notice is now kept in `<agentDir>/mu/notices.json` (`layaRemoved: { judge, from }`) and said at the first message a person sends, in that session or a later one: `judge.notice`, then the line, and the entry is removed. pi's `input` event is that message; mu's own commands (`/goal`, `/permissions`, `/mu …`), which pi runs before it, and messages an extension sends don't count. A session that gets no message leaves the notice where it is, and `mu --print` and json mode still change nothing. `MU_JUDGE=laya` leaves the same notice at the start of each session that has it. The free Jev notice now writes its day beside other entries in notices.json instead of replacing the file. A mu.json that 0.2.5 already updated has no notice left to show.
12
+ - `mu judge mude install` without `--local-only` or `--with-fallback` keeps the current setting when mude 1.0 is already the judge (as `enable` does), instead of putting Jev back behind it.
13
+ - `mu judge model add`: a 401 without `HF_TOKEN` says the model doesn't exist or is private or gated, and to check the name, since Hugging Face answers both the same way.
14
+ - `mu doctor` and `/doctor` mark a missing Chrome, Chromium, Edge or Brave as optional (`--`) instead of something to fix, so `mu doctor` exits 0 when everything else is set up. `mu --help` lines fit in 100 columns.
15
+
16
+ ## [0.2.5] - 2026-10-24
17
+
18
+ **0.2.5 removes Laya from mu's built-in judges. A configuration that still names it switches to mude 1.0 or Jev on its own.**
19
+
20
+ ### Changed
21
+
22
+ - A configuration that still names Laya keeps working. `laya` in `tiers` or a route, and a judge of type `local`, are read without it: mude 1.0 takes its place when it is installed on this Mac and its key (`MU_JUDGE_MUDE_KEY`) is set, otherwise the judges left do, or Jev when none is left. A route left with no judge goes back to `tiers`. The first session with a terminal or the desktop app (not `mu --print` or json mode) writes that to mu.json once. It changes only those values, keeps a copy in `~/.mu/backups`, and says so once: `judge.notice` with the code `laya_removed` and the `judge`, then the line itself. A mu.json that isn't plain JSON is left alone. `MU_JUDGE=laya` is skipped in every session that has it. For such a configuration the launcher starts mude's server in the background, as it does for `mude`.
23
+ - `/mu judge laya` and `/mu route <point> laya` say that Laya is no longer built in. An `http` judge needs a `baseUrl`; its client is `HttpJudgeProvider` (it was `LocalJudgeProvider`).
24
+
25
+ ### Removed
26
+
27
+ - Laya, the third-party local judge, is no longer built in: the `laya` judge, the `local` judge type, its sidecar (`kyrn/bin/kyrn-judge-local`, `kyrn/local-judge/server.py` and its lock, no longer in the package) and `mu judge setup | start | stop | status | run`. Those commands now say so and point to `mu judge mude install` and `mu judge model add <link>`; `mu judge` alone prints the judge commands. The launcher no longer starts a sidecar, and `mu doctor` drops its rows.
28
+ - `continuation.localPromised`, which only Laya used, and the one-question-at-a-time path of a `local` judge: `tool.admission` and `tool.injection` always ask in batches, and the questions about a message start together.
29
+
5
30
  ## [0.2.4] - 2026-10-24
6
31
 
7
32
  **0.2.4 brings Gemini 4 Argon through Google Antigravity and fixes third-party OpenAI-compatible providers that turned away the `developer` role.**
package/README.md CHANGED
@@ -27,13 +27,13 @@ mu is built on [pi](https://github.com/earendil-works/pi). It hands the small qu
27
27
 
28
28
  ## Choose a judge
29
29
 
30
- Out of the box, the judge is Jev 1.13, TypeSafe's hosted judge model, free on OpenCode Zen for a limited time and with no key needed. What it judges goes to OpenCode, which doesn't train on it, and mu reminds you of that once a day. To send nothing anywhere, run `mu setup --judge off` (or choose "Not now" when `mu setup` asks): no decision point asks a judge, and mu's features keep working on their rules. Or use the local judge, Laya.
30
+ Out of the box, the judge is Jev 1.13, TypeSafe's hosted judge model, free on OpenCode Zen for a limited time and with no key needed. What it judges goes to OpenCode, which doesn't train on it, and mu reminds you of that once a day. To send nothing anywhere, run `mu setup --judge off` (or choose "Not now" when `mu setup` asks): no decision point asks a judge, and mu's features fall back to their own rules. Or, on a Mac with Apple silicon, use mude 1.0 with `--local-only`.
31
31
 
32
32
  - **Jev** (hosted): set `TYPESAFE_API_KEY` (TypeSafe), `MU_JUDGE_OPENROUTER_API_KEY` (Jev on OpenRouter), `AI_GATEWAY_API_KEY` (Vercel AI Gateway), `OPENCODE_API_KEY` (OpenCode Zen) or `CLOUDFLARE_API_KEY` with `CLOUDFLARE_ACCOUNT_ID` (Cloudflare Workers AI) in your environment, or add it to `~/.mu/.env` as `KEY=value`. `mu setup` asks for a key and writes it there. mu reads that file as data, never prints it and starts using the key automatically.
33
33
  - **Another classifier** from pi's catalog: `/mu judge classifier:<provider>/<model>`, for example Cloudflare's Clef (`clef`, `clef-flash`) or a System One model on OpenRouter, with that provider's key.
34
34
  - **CLM** (self-hosted; its encoder needs a GPU): start `clm-serve` from [Contrastive-LM/CLM](https://github.com/Contrastive-LM/CLM), then run `/mu judge clm`. For a server on another machine, add `"judges": { "clm": { "type": "clm", "baseUrl": "http://<host>:8700" } }` to `~/.mu/agent/mu.json`. If the server was started with `CLM_API_KEY`, set the same value in `MU_JUDGE_CLM_API_KEY`. `mu doctor` checks the server.
35
35
  - **mude 1.0** (local, a Mac with Apple silicon, macOS 14+), mu's own decision model, optimized for coding: `mu judge mude install` downloads it (1.8 GB), sets it up in `~/.mu/local-judge/mude`, starts its server on 127.0.0.1 and puts it first. What it can't answer goes to the judges you had before, which are hosted; `mu judge mude enable --local-only` keeps everything on your Mac. Its server uses about 4 GB of memory until `mu judge mude stop`.
36
- - **Laya** (local, macOS on Apple silicon): `mu judge setup` installs it into `~/.mu/local-judge`. It downloads about 930 MB, and only when you run that command.
36
+ - **Other decision models from Hugging Face** (local, a Mac with Apple silicon): `mu judge model add <link>` adds one that is packaged like mude 1.0 and makes it the judge. Laya is no longer included: a `mu.json` that still names it gets mude 1.0 in its place when mude is set up, otherwise the remaining judges or Jev, and mu says so once.
37
37
  - **Any model you already use:** `/mu judge llm:<provider>/<model>`.
38
38
 
39
39
  34 of the 40 decision points act on their verdicts from the start (`active`). To only record what the judge would do, run `/mu mode default shadow`, or put `"modes": { "default": "shadow" }` in `~/.mu/agent/mu.json`.
@@ -47,7 +47,7 @@ mu -p "prompt" run one prompt, print the answer and exit
47
47
  mu -c | -r continue the last session | pick a session to resume
48
48
  mu setup connect a model and choose a judge (see mu setup --help for scripting and services)
49
49
  mu judge mude <cmd> mude 1.0 on this Mac: install | enable | disable | start | stop | status | uninstall
50
- mu judge <cmd> manage the local judge, Laya (macOS on Apple silicon): setup | start | stop | status | run
50
+ mu judge model <cmd> other decision models from Hugging Face: add <link> | list | use | remove
51
51
  mu ledger [n] [--json] print the judge's verdicts from the last n sessions
52
52
  mu import <file>... import Claude Code, Codex and ZCode conversations; --list shows them (mu import --help)
53
53
  mu doctor check the installation, the judges and the connections
@@ -73,12 +73,13 @@ var DEFAULT_CONFIG = {
73
73
  recordState: false
74
74
  };
75
75
  var MODES = ["off", "shadow", "active"];
76
+ var LAYA = "laya";
77
+ var RETIRED_TYPE = "local";
76
78
  var JUDGE_TYPES = [
77
79
  "jev",
78
80
  "typesafe",
79
81
  "clm",
80
82
  "gateway",
81
- "local",
82
83
  "http",
83
84
  "llm",
84
85
  "classifier",
@@ -118,14 +119,48 @@ function readJudges(value) {
118
119
  }
119
120
  return judges;
120
121
  }
121
- function parseConfig(value) {
122
+ function retiredJudges(judges) {
123
+ const names = /* @__PURE__ */ new Set();
124
+ const entries = isRecord(judges) ? judges : {};
125
+ const own = entries[LAYA];
126
+ if (!(isRecord(own) && typeof own.type === "string" && JUDGE_TYPES.includes(own.type))) names.add(LAYA);
127
+ for (const [name, entry] of Object.entries(entries)) {
128
+ if (isRecord(entry) && entry.type === RETIRED_TYPE) names.add(name);
129
+ }
130
+ return names;
131
+ }
132
+ function withoutRetired(tiers, retired, replacement) {
133
+ if (!tiers.some((tier) => retired.has(tier.trim()))) return void 0;
134
+ const mapped = tiers.flatMap((tier) => retired.has(tier.trim()) ? replacement ? [replacement] : [] : [tier]);
135
+ return mapped.filter((tier, index) => tier !== replacement || mapped.indexOf(tier) === index);
136
+ }
137
+ function namesRetired(value) {
138
+ if (!isRecord(value)) return false;
139
+ const retired = retiredJudges(value.judges);
140
+ const lists = [
141
+ Array.isArray(value.tiers) ? value.tiers : [],
142
+ ...isRecord(value.routes) ? Object.values(value.routes) : []
143
+ ];
144
+ return lists.some((tiers) => readTiers(tiers).some((tier) => retired.has(tier)));
145
+ }
146
+ function parseConfig(value, options = {}) {
122
147
  if (!isRecord(value)) return DEFAULT_CONFIG;
123
- const tiers = Array.isArray(value.tiers) ? value.tiers.filter((tier) => typeof tier === "string") : [];
148
+ const named = Array.isArray(value.tiers) ? value.tiers.filter((tier) => typeof tier === "string") : [];
149
+ const retired = retiredJudges(value.judges);
150
+ const replacement = namesRetired(value) ? options.replaceLaya?.() : void 0;
151
+ const tiers = withoutRetired(named, retired, replacement) ?? named;
152
+ const routes = readRoutes(value.routes);
153
+ for (const [specId, routed] of Object.entries(routes)) {
154
+ const kept = withoutRetired(routed, retired, replacement);
155
+ if (kept === void 0) continue;
156
+ if (kept.length > 0) routes[specId] = kept;
157
+ else delete routes[specId];
158
+ }
124
159
  return {
125
160
  tiers: tiers.length > 0 ? tiers : DEFAULT_CONFIG.tiers,
126
161
  judges: readJudges(value.judges),
127
162
  modes: { ...DEFAULT_CONFIG.modes, ...readModes(value.modes) },
128
- routes: readRoutes(value.routes),
163
+ routes,
129
164
  features: isRecord(value.features) ? value.features : {},
130
165
  mcp: isRecord(value.mcp) ? value.mcp : void 0,
131
166
  writer: typeof value.writer === "string" ? value.writer : void 0,
@@ -136,9 +171,18 @@ function loadConfig(source = {}) {
136
171
  const env = source.env ?? process.env;
137
172
  let config = DEFAULT_CONFIG;
138
173
  let problem;
174
+ let inFile = false;
175
+ let replacement;
139
176
  for (const file of source.dir ? [CONFIG_FILE, LEGACY_CONFIG_FILE] : []) {
140
177
  try {
141
- config = parseConfig(JSON.parse(readFileSync(join2(source.dir, file), "utf8")));
178
+ const value = JSON.parse(readFileSync(join2(source.dir, file), "utf8"));
179
+ inFile = namesRetired(value);
180
+ config = parseConfig(value, {
181
+ replaceLaya: () => {
182
+ replacement = source.replaceLaya?.();
183
+ return replacement;
184
+ }
185
+ });
142
186
  break;
143
187
  } catch (error) {
144
188
  if (error.code === "ENOENT") continue;
@@ -146,17 +190,21 @@ function loadConfig(source = {}) {
146
190
  break;
147
191
  }
148
192
  }
193
+ let inEnv = false;
149
194
  const judge = muEnv("JUDGE", env)?.trim();
150
195
  if (judge === "off") return { config, disabled: true, problem };
151
196
  if (judge) {
152
- const tiers = judge.split(",").map((tier) => tier.trim() === "gateway" ? "jev" : tier.trim());
153
- config = { ...config, tiers: tiers.filter(Boolean) };
197
+ const tiers = judge.split(",").map((tier) => tier.trim() === "gateway" ? "jev" : tier.trim()).filter(Boolean);
198
+ const kept = config.judges[LAYA] ? tiers : tiers.filter((tier) => tier !== LAYA);
199
+ inEnv = kept.length < tiers.length;
200
+ if (kept.length > 0) config = { ...config, tiers: kept };
154
201
  }
155
202
  const mode = muEnv("JUDGE_MODE", env);
156
203
  if (mode && MODES.includes(mode)) config = { ...config, modes: { ...config.modes, default: mode } };
157
204
  const writer = muEnv("WRITER", env);
158
205
  if (writer) config = { ...config, writer };
159
- return { config, disabled: false, problem };
206
+ const laya = inFile || inEnv ? { file: inFile, env: inEnv, replacement } : void 0;
207
+ return { config, disabled: false, problem, ...laya ? { laya } : {} };
160
208
  }
161
209
  function featureOptions(config, name, defaults) {
162
210
  const value = config.features[name];
@@ -38,18 +38,6 @@ var BUILT_IN_JUDGES = {
38
38
  "clef-flash": { type: "classifier", model: "cloudflare-workers-ai/@cf/cloudflare/clef-flash" },
39
39
  // CLM-8B on this machine, where `clm-serve` listens by default. Its profile is not measured on mu's questions yet.
40
40
  clm: { type: "clm" },
41
- // Measured in kyrn/docs/03-local-judge.md: the base checkpoint classifies one text well, says "yes" to
42
- // nearly every relational question, and cannot use a rubric or judge the request itself. Measured on
43
- // Terminal-Bench 2.1 (Oct 2026): on tool output it is wrong or late. All 15 passages it withheld as injections were
44
- // clean (directory listings, href lists, headers, JSON), its one admission drop was a makefile, and most of its
45
- // calls on both failed or came too late. So screening tool output (`screen`) goes to the next tier, or to the
46
- // rules. It also read a finished "Done. Created /app/meeting_scheduled.ics …" as a promise of more work, twice in one
47
- // run (constraints-scheduling): 4 extra requests and the model searching / for test files. So whether a closing
48
- // message promised more (`intent`, turn.continue) goes to the next tier too; `continuation.localPromised` asks it.
49
- laya: {
50
- type: "local",
51
- profile: { capabilities: { relate: false, rate: false, meta: false, screen: false, intent: false } }
52
- },
53
41
  // mude, the decision model trained for mu's judgment layer, on this Mac (`mu judge mude install`, Apple silicon):
54
42
  // the System One server that comes in its package, on 127.0.0.1:8790, with the key the install wrote to the .env.
55
43
  // Trusted on every capability: on mu's own dev scenarios (an internal benchmark, not public yet) it did as well as Jev
@@ -66,6 +54,31 @@ var BUILT_IN_JUDGES = {
66
54
  },
67
55
  mock: { type: "mock" }
68
56
  };
57
+ var LAYA = "laya";
58
+ var RETIRED_TYPE = "local";
59
+ var JUDGE_TYPES = [
60
+ "jev",
61
+ "typesafe",
62
+ "clm",
63
+ "gateway",
64
+ "http",
65
+ "llm",
66
+ "classifier",
67
+ "mock"
68
+ ];
69
+ function isRecord(value) {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+ function retiredJudges(judges) {
73
+ const names = /* @__PURE__ */ new Set();
74
+ const entries = isRecord(judges) ? judges : {};
75
+ const own = entries[LAYA];
76
+ if (!(isRecord(own) && typeof own.type === "string" && JUDGE_TYPES.includes(own.type))) names.add(LAYA);
77
+ for (const [name, entry] of Object.entries(entries)) {
78
+ if (isRecord(entry) && entry.type === RETIRED_TYPE) names.add(name);
79
+ }
80
+ return names;
81
+ }
69
82
 
70
83
  // packages/kyrn-judge/src/setup/env-file.ts
71
84
  var DEFINITION = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
@@ -255,7 +268,7 @@ function writeJsonFile(path, value) {
255
268
  writeFileAtomic(path, `${JSON.stringify(value, null, 2)}
256
269
  `);
257
270
  }
258
- function isRecord(value) {
271
+ function isRecord2(value) {
259
272
  return typeof value === "object" && value !== null && !Array.isArray(value);
260
273
  }
261
274
  function judgeConfigPath(agentDir, exists = existsSync) {
@@ -272,10 +285,10 @@ function withTiers(config, tiers) {
272
285
  return { ...config, tiers: [...tiers] };
273
286
  }
274
287
  function judgeOff(config) {
275
- return isRecord(config?.modes) && config.modes.default === "off";
288
+ return isRecord2(config?.modes) && config.modes.default === "off";
276
289
  }
277
290
  function withJudge(config, on) {
278
- const { default: _default, ...modes } = isRecord(config.modes) ? config.modes : {};
291
+ const { default: _default, ...modes } = isRecord2(config.modes) ? config.modes : {};
279
292
  if (!on) return { ...config, modes: { default: "off", ...modes } };
280
293
  if (!judgeOff(config)) return config;
281
294
  const { modes: _modes, ...rest } = config;
@@ -596,8 +609,10 @@ function enableLocal(judge, paths, files, options) {
596
609
  const name = judge.name;
597
610
  const { path, config } = readConfig(files);
598
611
  const state = readState(paths);
599
- const current = tiersOf(config).filter((tier) => tier !== name);
600
- const before = current.length > 0 ? current : [...state?.previousTiers ?? ["jev"]];
612
+ const retired = retiredJudges(config.judges);
613
+ const current = tiersOf(config).filter((tier) => tier !== name && !retired.has(tier));
614
+ const kept = (state?.previousTiers ?? []).filter((tier) => !retired.has(tier));
615
+ const before = current.length > 0 ? current : kept.length > 0 ? kept : ["jev"];
601
616
  const tiers = options.localOnly ? [name] : [name, ...before.filter((tier) => tier !== name)];
602
617
  const judgeWasOff = state?.judgeWasOff ?? judgeOff(config);
603
618
  let next = withJudge(withTiers(withoutOwnRoutes(config, state), tiers), true);
@@ -640,8 +655,10 @@ function disableMude(paths, files, options = {}) {
640
655
  function disableLocal(judge, paths, files, options = {}) {
641
656
  const { path, config } = readConfig(files);
642
657
  const state = readState(paths);
643
- const rest = tiersOf(config).filter((tier) => tier !== judge.name);
644
- const tiers = rest.length > 0 ? rest : [...state?.previousTiers ?? ["jev"]];
658
+ const retired = retiredJudges(config.judges);
659
+ const rest = tiersOf(config).filter((tier) => tier !== judge.name && !retired.has(tier));
660
+ const previous = (state?.previousTiers ?? []).filter((tier) => !retired.has(tier));
661
+ const tiers = rest.length > 0 ? rest : previous.length > 0 ? previous : ["jev"];
645
662
  let next = withTiers(withoutOwnRoutes(config, state), tiers);
646
663
  if (state?.judgeWasOff) next = withJudge(next, false);
647
664
  if (options.removeEntry && state?.wroteEntry) {
@@ -1054,7 +1071,7 @@ async function resolveModel(ref, options = {}) {
1054
1071
  }
1055
1072
  if (response.status === 401 || response.status === 403) {
1056
1073
  throw new ModelError(
1057
- `Hugging Face won't show ${ref.repo} (${response.status}): it's private or gated. Accept its terms on huggingface.co, set HF_TOKEN, then add it again`,
1074
+ response.status === 401 && !options.token ? `Couldn't open ${ref.repo} on Hugging Face (401): it doesn't exist, or it's private or gated. Check the name; for a gated model, accept its terms on huggingface.co, set HF_TOKEN, then add it again` : `${ref.repo} is private or gated on Hugging Face (${response.status}). Accept its terms on huggingface.co, set HF_TOKEN, then add it again`,
1058
1075
  "access"
1059
1076
  );
1060
1077
  }
@@ -1131,7 +1148,7 @@ function writeRecord(muHome, record) {
1131
1148
  writeFileSync5(join5(root, "model.json"), `${JSON.stringify(record, null, 1)}
1132
1149
  `);
1133
1150
  }
1134
- function isRecord2(value) {
1151
+ function isRecord3(value) {
1135
1152
  if (typeof value !== "object" || value === null) return false;
1136
1153
  const record = value;
1137
1154
  return typeof record.id === "string" && typeof record.repo === "string" && typeof record.revision === "string" && typeof record.bytes === "number" && Array.isArray(record.files);
@@ -1147,7 +1164,7 @@ function listRecords(muHome) {
1147
1164
  for (const name of names.sort()) {
1148
1165
  try {
1149
1166
  const value = JSON.parse(readFileSync5(join5(modelsDir(muHome), name, "model.json"), "utf8"));
1150
- if (isRecord2(value) && value.id === name) records.push(value);
1167
+ if (isRecord3(value) && value.id === name) records.push(value);
1151
1168
  } catch {
1152
1169
  }
1153
1170
  }
@@ -1460,10 +1477,11 @@ function installLocalOnly(ctx, args) {
1460
1477
  if (args.includes("--with-fallback")) return false;
1461
1478
  try {
1462
1479
  const value = JSON.parse(readFileSync7(ctx.paths.install, "utf8"));
1463
- return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
1480
+ if (typeof value === "object" && value !== null && "localOnly" in value) return value.localOnly === true;
1464
1481
  } catch {
1465
- return false;
1466
1482
  }
1483
+ const current = judgeSetting(ctx.files, pkgOf(ctx).judge.name);
1484
+ return current.enabled && current.localOnly;
1467
1485
  }
1468
1486
  function startedAt(pid) {
1469
1487
  try {
@@ -1944,7 +1962,7 @@ ${USAGE2}`, "usage");
1944
1962
  else out = humanOut(write, tty, each2.pkg);
1945
1963
  out.step(
1946
1964
  "model",
1947
- `${record2.repo} @ ${record2.revision.slice(0, 7)} (${gb2(record2.bytes)} GB) is the decision model ${record2.id}. It runs its own server code (${SERVER_FILE}) on this Mac: add only models you trust.`
1965
+ `Adding ${record2.repo} @ ${record2.revision.slice(0, 7)} (${gb2(record2.bytes)} GB) as ${record2.id}. It runs its own server code (${SERVER_FILE}) on this Mac: add only models you trust.`
1948
1966
  );
1949
1967
  await install(each2, out, flags, mudePaths(home));
1950
1968
  out.status(await status(each2));
@@ -100,18 +100,6 @@ var BUILT_IN_JUDGES = {
100
100
  "clef-flash": { type: "classifier", model: "cloudflare-workers-ai/@cf/cloudflare/clef-flash" },
101
101
  // CLM-8B on this machine, where `clm-serve` listens by default. Its profile is not measured on mu's questions yet.
102
102
  clm: { type: "clm" },
103
- // Measured in kyrn/docs/03-local-judge.md: the base checkpoint classifies one text well, says "yes" to
104
- // nearly every relational question, and cannot use a rubric or judge the request itself. Measured on
105
- // Terminal-Bench 2.1 (Oct 2026): on tool output it is wrong or late. All 15 passages it withheld as injections were
106
- // clean (directory listings, href lists, headers, JSON), its one admission drop was a makefile, and most of its
107
- // calls on both failed or came too late. So screening tool output (`screen`) goes to the next tier, or to the
108
- // rules. It also read a finished "Done. Created /app/meeting_scheduled.ics …" as a promise of more work, twice in one
109
- // run (constraints-scheduling): 4 extra requests and the model searching / for test files. So whether a closing
110
- // message promised more (`intent`, turn.continue) goes to the next tier too; `continuation.localPromised` asks it.
111
- laya: {
112
- type: "local",
113
- profile: { capabilities: { relate: false, rate: false, meta: false, screen: false, intent: false } }
114
- },
115
103
  // mude, the decision model trained for mu's judgment layer, on this Mac (`mu judge mude install`, Apple silicon):
116
104
  // the System One server that comes in its package, on 127.0.0.1:8790, with the key the install wrote to the .env.
117
105
  // Trusted on every capability: on mu's own dev scenarios (an internal benchmark, not public yet) it did as well as Jev
@@ -128,6 +116,31 @@ var BUILT_IN_JUDGES = {
128
116
  },
129
117
  mock: { type: "mock" }
130
118
  };
119
+ var LAYA = "laya";
120
+ var RETIRED_TYPE = "local";
121
+ var JUDGE_TYPES = [
122
+ "jev",
123
+ "typesafe",
124
+ "clm",
125
+ "gateway",
126
+ "http",
127
+ "llm",
128
+ "classifier",
129
+ "mock"
130
+ ];
131
+ function isRecord(value) {
132
+ return typeof value === "object" && value !== null && !Array.isArray(value);
133
+ }
134
+ function retiredJudges(judges) {
135
+ const names = /* @__PURE__ */ new Set();
136
+ const entries = isRecord(judges) ? judges : {};
137
+ const own = entries[LAYA];
138
+ if (!(isRecord(own) && typeof own.type === "string" && JUDGE_TYPES.includes(own.type))) names.add(LAYA);
139
+ for (const [name, entry] of Object.entries(entries)) {
140
+ if (isRecord(entry) && entry.type === RETIRED_TYPE) names.add(name);
141
+ }
142
+ return names;
143
+ }
131
144
 
132
145
  // packages/kyrn-judge/src/setup/env-file.ts
133
146
  var DEFINITION = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
@@ -317,21 +330,21 @@ function writeJsonFile(path, value) {
317
330
  writeFileAtomic(path, `${JSON.stringify(value, null, 2)}
318
331
  `);
319
332
  }
320
- function isRecord(value) {
333
+ function isRecord2(value) {
321
334
  return typeof value === "object" && value !== null && !Array.isArray(value);
322
335
  }
323
336
  function existingProvider(models, id) {
324
337
  const providers = models.providers;
325
- return isRecord(providers) && isRecord(providers[id]) ? providers[id] : void 0;
338
+ return isRecord2(providers) && isRecord2(providers[id]) ? providers[id] : void 0;
326
339
  }
327
340
  function mergeModels(existing, added) {
328
341
  const kept = Array.isArray(existing) ? [...existing] : [];
329
- const have = new Set(kept.flatMap((model) => isRecord(model) && typeof model.id === "string" ? [model.id] : []));
342
+ const have = new Set(kept.flatMap((model) => isRecord2(model) && typeof model.id === "string" ? [model.id] : []));
330
343
  return [...kept, ...added.filter((model) => !have.has(model.id)).map((model) => ({ ...model }))];
331
344
  }
332
345
  function withProvider(models, id, entry) {
333
- const providers = isRecord(models.providers) ? models.providers : {};
334
- const current = isRecord(providers[id]) ? providers[id] : {};
346
+ const providers = isRecord2(models.providers) ? models.providers : {};
347
+ const current = isRecord2(providers[id]) ? providers[id] : {};
335
348
  const next = {
336
349
  ...current,
337
350
  name: typeof current.name === "string" ? current.name : entry.name,
@@ -345,8 +358,8 @@ function withProvider(models, id, entry) {
345
358
  return { ...models, providers: { ...providers, [id]: next } };
346
359
  }
347
360
  function withModel(models, providerId, modelId) {
348
- const providers = isRecord(models.providers) ? models.providers : {};
349
- const current = isRecord(providers[providerId]) ? providers[providerId] : {};
361
+ const providers = isRecord2(models.providers) ? models.providers : {};
362
+ const current = isRecord2(providers[providerId]) ? providers[providerId] : {};
350
363
  return {
351
364
  ...models,
352
365
  providers: { ...providers, [providerId]: { ...current, models: mergeModels(current.models, [{ id: modelId }]) } }
@@ -366,10 +379,10 @@ function withTiers(config, tiers) {
366
379
  return { ...config, tiers: [...tiers] };
367
380
  }
368
381
  function judgeOff(config) {
369
- return isRecord(config?.modes) && config.modes.default === "off";
382
+ return isRecord2(config?.modes) && config.modes.default === "off";
370
383
  }
371
384
  function withJudge(config, on) {
372
- const { default: _default, ...modes } = isRecord(config.modes) ? config.modes : {};
385
+ const { default: _default, ...modes } = isRecord2(config.modes) ? config.modes : {};
373
386
  if (!on) return { ...config, modes: { default: "off", ...modes } };
374
387
  if (!judgeOff(config)) return config;
375
388
  const { modes: _modes, ...rest } = config;
@@ -690,8 +703,10 @@ function enableLocal(judge, paths, files, options) {
690
703
  const name = judge.name;
691
704
  const { path, config } = readConfig(files);
692
705
  const state = readState(paths);
693
- const current = tiersOf(config).filter((tier) => tier !== name);
694
- const before = current.length > 0 ? current : [...state?.previousTiers ?? ["jev"]];
706
+ const retired = retiredJudges(config.judges);
707
+ const current = tiersOf(config).filter((tier) => tier !== name && !retired.has(tier));
708
+ const kept = (state?.previousTiers ?? []).filter((tier) => !retired.has(tier));
709
+ const before = current.length > 0 ? current : kept.length > 0 ? kept : ["jev"];
695
710
  const tiers = options.localOnly ? [name] : [name, ...before.filter((tier) => tier !== name)];
696
711
  const judgeWasOff = state?.judgeWasOff ?? judgeOff(config);
697
712
  let next = withJudge(withTiers(withoutOwnRoutes(config, state), tiers), true);
@@ -734,8 +749,10 @@ function disableMude(paths, files, options = {}) {
734
749
  function disableLocal(judge, paths, files, options = {}) {
735
750
  const { path, config } = readConfig(files);
736
751
  const state = readState(paths);
737
- const rest = tiersOf(config).filter((tier) => tier !== judge.name);
738
- const tiers = rest.length > 0 ? rest : [...state?.previousTiers ?? ["jev"]];
752
+ const retired = retiredJudges(config.judges);
753
+ const rest = tiersOf(config).filter((tier) => tier !== judge.name && !retired.has(tier));
754
+ const previous = (state?.previousTiers ?? []).filter((tier) => !retired.has(tier));
755
+ const tiers = rest.length > 0 ? rest : previous.length > 0 ? previous : ["jev"];
739
756
  let next = withTiers(withoutOwnRoutes(config, state), tiers);
740
757
  if (state?.judgeWasOff) next = withJudge(next, false);
741
758
  if (options.removeEntry && state?.wroteEntry) {
@@ -1098,7 +1115,7 @@ function modelPaths(muHome2, record) {
1098
1115
  installLock: join6(root, "install.lock")
1099
1116
  };
1100
1117
  }
1101
- function isRecord2(value) {
1118
+ function isRecord3(value) {
1102
1119
  if (typeof value !== "object" || value === null) return false;
1103
1120
  const record = value;
1104
1121
  return typeof record.id === "string" && typeof record.repo === "string" && typeof record.revision === "string" && typeof record.bytes === "number" && Array.isArray(record.files);
@@ -1114,7 +1131,7 @@ function listRecords(muHome2) {
1114
1131
  for (const name of names.sort()) {
1115
1132
  try {
1116
1133
  const value = JSON.parse(readFileSync5(join6(modelsDir(muHome2), name, "model.json"), "utf8"));
1117
- if (isRecord2(value) && value.id === name) records.push(value);
1134
+ if (isRecord3(value) && value.id === name) records.push(value);
1118
1135
  } catch {
1119
1136
  }
1120
1137
  }
@@ -1422,10 +1439,11 @@ function installLocalOnly(ctx, args) {
1422
1439
  if (args.includes("--with-fallback")) return false;
1423
1440
  try {
1424
1441
  const value = JSON.parse(readFileSync7(ctx.paths.install, "utf8"));
1425
- return typeof value === "object" && value !== null && "localOnly" in value && value.localOnly === true;
1442
+ if (typeof value === "object" && value !== null && "localOnly" in value) return value.localOnly === true;
1426
1443
  } catch {
1427
- return false;
1428
1444
  }
1445
+ const current = judgeSetting(ctx.files, pkgOf(ctx).judge.name);
1446
+ return current.enabled && current.localOnly;
1429
1447
  }
1430
1448
  function startedAt(pid) {
1431
1449
  try {
@@ -270,7 +270,7 @@
270
270
  },
271
271
  "summary": {
272
272
  "zh": "任务进行中你又发来一句话:判断是在下一步就带进工作,还是等这一轮做完再处理。判定最多等 1.5 秒;没有判定时,消息按你发送的方式送达。",
273
- "en": "For a message you send while the agent is working: bring it into the work at the next step, or hold it until the run is done. mu waits up to 1.5 seconds for the verdict; without one, the message goes as you sent it.",
273
+ "en": "For a message you send while the agent is working: deliver it at the next step, or hold it until the run is done. mu waits up to 1.5 seconds for the verdict; without one, the message goes as you sent it.",
274
274
  "zh-TW": "任務進行中你又傳來一則訊息:判斷是在下一步就帶進工作,還是等這一輪做完再處理。判定最多等 1.5 秒;沒有判定時,訊息照你傳送的方式送達。",
275
275
  "ja-JP": "エージェントの作業中に送ったメッセージを、次のステップで作業に取り込むか、実行が終わるまで待たせるかを判定します。判定は最大 1.5 秒待ち、判定がなければメッセージは送ったとおりに届きます。",
276
276
  "ko-KR": "에이전트가 작업하는 중에 보낸 메시지를 다음 단계에서 작업에 반영할지, 실행이 끝날 때까지 기다리게 할지 판정합니다. 판정은 최대 1.5초 기다리며, 판정이 없으면 메시지는 보낸 그대로 전달됩니다.",
@@ -1181,8 +1181,8 @@
1181
1181
  "fa-IR": "نیمه‌کاره ماند"
1182
1182
  },
1183
1183
  "summary": {
1184
- "zh": "一轮结束在「接下来我去跑测试」却什么也没做,或者你已经让它做了它还在问「要我改吗」:让它接着做。难以撤销、或要出这台电脑的一步(推送、发布、删除、付款)不催。每条消息最多催两次;无人值守的运行里「说了没做」最多催一次,它回应上一次催促时没调工具就不再催。本地判定器(Laya)不回答「说了没做」。",
1185
- "en": "When a run ends on \"next I'll run the tests\" without doing it, or asks permission for work you already asked for, sends the agent back to it. Never nudges a step that is hard to undo or reaches beyond this machine (push, publish, delete, pay). At most twice per message; in an unattended run, at most once for work it said it would do, and not after it answered a nudge without a tool call. The local judge (Laya) doesn't answer whether work was left undone."
1184
+ "zh": "一轮结束在「接下来我去跑测试」却什么也没做,或者你已经让它做了它还在问「要我改吗」:让它接着做。难以撤销、或要出这台电脑的一步(推送、发布、删除、付款)不催。每条消息最多催两次;无人值守的运行里「说了没做」最多催一次,它回应上一次催促时没调工具就不再催。",
1185
+ "en": "When a run ends on \"next I'll run the tests\" without doing it, or asks permission for work you already asked for, sends the agent back to it. Never nudges a step that is hard to undo or reaches beyond this machine (push, publish, delete, pay). At most twice per message; in an unattended run, at most once for work it said it would do, and not after it answered a nudge without a tool call."
1186
1186
  }
1187
1187
  },
1188
1188
  {
@@ -1381,7 +1381,7 @@
1381
1381
  },
1382
1382
  "summary": {
1383
1383
  "zh": "上下文预算、后台命令结束等事件出现时,判断该现在告诉模型、稍后再说,还是不必说。默认是影子模式:判定只记录,事件按各自的规则投递,后台命令结束和上下文预算都是立刻告知。",
1384
- "en": "For events such as the context budget or a background job's end: tell the model now, later or not at all. In shadow by default: the verdict is only logged, and each event is delivered by its own rule: right away for a job's end and the context budget.",
1384
+ "en": "For events such as the context budget or a background job's end: tell the model now, later or not at all. In shadow by default: the verdict is only logged, and each event is delivered by its own rule (right away for a job's end and the context budget).",
1385
1385
  "zh-TW": "上下文預算、背景工作結束這類事件出現時,判斷該現在告訴模型、晚點再說,還是不必說。預設為影子模式:判定只記錄下來,事件各按自己的規則送達;背景工作結束和上下文預算都是立刻告知。",
1386
1386
  "ja-JP": "コンテキスト予算やバックグラウンドジョブの終了などのイベントを、モデルにすぐ伝えるか、後で伝えるか、伝えないかを判定します。既定ではシャドーモードです。判定は記録するだけで、各イベントはそれぞれのルールで届けます。ジョブの終了とコンテキスト予算の場合は、すぐに届けます。",
1387
1387
  "ko-KR": "컨텍스트 예산이나 백그라운드 작업 종료 같은 이벤트를 모델에게 지금 알릴지, 나중에 알릴지, 알리지 않을지 정합니다. 기본값은 섀도 모드입니다. 판정은 기록만 하고 각 이벤트는 자체 규칙에 따라 전달되는데, 작업 종료와 컨텍스트 예산은 바로 알립니다.",
@@ -3700,19 +3700,19 @@
3700
3700
  "fa-IR": "تعداد تکه در هر درخواست"
3701
3701
  },
3702
3702
  "help": {
3703
- "zh": "托管判定器把多块放进一个请求,状态只计费一次;本地判定器始终一次一块。",
3704
- "en": "A hosted judge takes many chunks in one request, with the shared context billed once; the local judge takes one at a time.",
3705
- "zh-TW": "託管判定器把多塊放進一個請求,狀態只計費一次;本機判定器一律一次一塊。",
3706
- "ja-JP": "ホスト型の判定器は多くのチャンクを 1 回のリクエストで受け取り、状態の課金は 1 回だけです。ローカルの判定器は 1 つずつ処理します。",
3707
- "ko-KR": "호스팅 판정기는 여러 청크를 한 번의 요청으로 받으며 상태는 한 번만 과금됩니다. 로컬 판정기는 한 번에 하나씩 처리합니다.",
3708
- "de-DE": "Ein gehosteter Judge nimmt viele Abschnitte in einer Anfrage, der Zustand wird nur einmal berechnet; der lokale Judge nimmt einen nach dem anderen.",
3709
- "fr-FR": "Un juge hébergé reçoit de nombreux fragments en une seule requête, l’état n’étant facturé qu’une fois ; le juge local les traite un par un.",
3710
- "es-ES": "Un juez alojado recibe muchos fragmentos en una sola solicitud y el estado se factura una vez; el juez local los procesa de uno en uno.",
3711
- "pt-BR": "Um juiz hospedado recebe muitos blocos numa só solicitação, com o estado cobrado uma vez; o juiz local recebe um de cada vez.",
3712
- "ru-RU": "Облачный судья принимает много частей одним запросом, и состояние оплачивается один раз; локальный судья берёт по одной.",
3713
- "uk-UA": "Хмарний суддя приймає багато частин одним запитом, і стан оплачується один раз; локальний суддя бере по одній.",
3714
- "tr-TR": "Barındırılan bir hakem birçok parçayı tek istekte alır ve durum bir kez ücretlendirilir; yerel hakem her seferinde bir parça alır.",
3715
- "fa-IR": "داور میزبانی‌شده چندین تکه را در یک درخواست می‌گیرد و هزینهٔ وضعیت فقط یک بار حساب می‌شود؛ داور محلی هر بار یک تکه می‌گیرد."
3703
+ "zh": "判定器把多块放进一个请求,状态只计费一次。",
3704
+ "en": "The judge takes many chunks in one request, with the shared context billed once.",
3705
+ "zh-TW": "判定器把多塊放進一個請求,狀態只計費一次。",
3706
+ "ja-JP": "判定器は多くのチャンクを 1 回のリクエストで受け取り、状態の課金は 1 回だけです。",
3707
+ "ko-KR": "판정기는 여러 청크를 한 번의 요청으로 받으며 상태는 한 번만 과금됩니다.",
3708
+ "de-DE": "Der Judge nimmt viele Abschnitte in einer Anfrage, der Zustand wird nur einmal berechnet.",
3709
+ "fr-FR": "Le juge reçoit de nombreux fragments en une seule requête, l’état n’étant facturé qu’une fois.",
3710
+ "es-ES": "El juez recibe muchos fragmentos en una sola solicitud y el estado se factura una vez.",
3711
+ "pt-BR": "O juiz recebe muitos blocos numa só solicitação, com o estado cobrado uma vez.",
3712
+ "ru-RU": "Судья принимает много частей одним запросом, и состояние оплачивается один раз.",
3713
+ "uk-UA": "Суддя приймає багато частин одним запитом, і стан оплачується один раз.",
3714
+ "tr-TR": "Hakem birçok parçayı tek istekte alır ve durum bir kez ücretlendirilir.",
3715
+ "fa-IR": "داور چندین تکه را در یک درخواست می‌گیرد و هزینهٔ وضعیت فقط یک بار حساب می‌شود."
3716
3716
  }
3717
3717
  },
3718
3718
  {
@@ -7092,19 +7092,6 @@
7092
7092
  "zh": "默认不催:它回应上一次催促时一个工具也没调(比如「工作已经做完了」),就是用停下来作了答,不再说它没做完。什么也没说就停下的一轮照样会被催。",
7093
7093
  "en": "Off by default: when the agent answered the last nudge without calling a tool (\"The work is already complete\"), it answered by stopping, and isn't told again that it said it would do more. A run that stops without a message is still sent back."
7094
7094
  }
7095
- },
7096
- {
7097
- "key": "localPromised",
7098
- "kind": "boolean",
7099
- "default": false,
7100
- "label": {
7101
- "zh": "让本地判定器判断「说了没做」",
7102
- "en": "Let the local judge decide whether work was left undone"
7103
- },
7104
- "help": {
7105
- "zh": "默认不让:这一问交给判定器列表里的下一个;只有本地判定器(Laya)时就不因此催。基准测试里 Laya 两次把做完了的「已完成,已创建 …」当成还要接着做。",
7106
- "en": "Off by default: this question goes to the next judge in your list, and with the local judge (Laya) alone no such nudge is sent. In a benchmark run Laya twice read a finished \"Done. Created …\" as a promise of more work."
7107
- }
7108
7095
  }
7109
7096
  ]
7110
7097
  },