mu-agent 0.2.4 → 0.2.5

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,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.5] - 2026-10-24
6
+
7
+ **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.**
8
+
9
+ ### Changed
10
+
11
+ - 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`.
12
+ - `/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`).
13
+
14
+ ### Removed
15
+
16
+ - 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.
17
+ - `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.
18
+
5
19
  ## [0.2.4] - 2026-10-24
6
20
 
7
21
  **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 keep working on their 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) {
@@ -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
  }
@@ -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
  }
@@ -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
  {
@@ -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
  },