mu-agent 0.1.13 → 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.
@@ -74,8 +74,12 @@ if (layout === "package") {
74
74
  "dependencies",
75
75
  installed
76
76
  ? "installed"
77
- : `missing: cd ${root} && npm ci --ignore-scripts && npm run hydrate:model-data`,
77
+ : `missing: cd ${root} && npm ci --ignore-scripts && npm run hydrate:model-catalog`,
78
78
  );
79
+ // The model list pi reads at its start is generated, not committed (installHint in mu.mjs).
80
+ if (installed && !existsSync(join(root, "packages", "ai", "src", "providers", "data", ".manifest.json"))) {
81
+ row("fix", "model list", `not generated: cd ${root} && npm run hydrate:model-catalog`);
82
+ }
79
83
 
80
84
  // A link on POSIX, a small mu.cmd on Windows, where a link to a script cannot be run.
81
85
  const linked = linkState({
@@ -118,14 +122,32 @@ row(
118
122
  const config = readJson(join(agentDir, "mu.json")) ?? readJson(join(agentDir, "kyrn.json"));
119
123
  const tiers = (env("JUDGE") ? env("JUDGE").split(",") : (config?.tiers ?? ["jev"])).map((tier) => tier.trim());
120
124
  const mode = env("JUDGE_MODE") ?? config?.modes?.default ?? "active";
121
- row("--", "judges", `${tiers.join(" > ")} · default mode ${mode}${env("JUDGE") ? " (from MU_JUDGE)" : ""}`);
122
125
  const overrides = Object.entries(config?.modes ?? {}).filter(([id]) => id !== "default");
126
+ // The modes mu sets out of the box besides the default, as packages/kyrn-judge/src/config.ts has them: one of these in
127
+ // mu.json gives way to a default of off (DecisionEngine.getMode).
128
+ const BUILT_IN_MODES = {
129
+ "swarm.patch": "off",
130
+ "capability.disclosure": "off",
131
+ "notify.routing": "shadow",
132
+ "input.interjection": "shadow",
133
+ "turn.drift": "shadow",
134
+ };
135
+ // Whether any decision point asks a judge (DecisionEngine.asksJudge). With none, nothing is sent to one, and the rows
136
+ // about keys and judges that answer would only mislead.
137
+ const asksJudge = mode !== "off" || overrides.some(([id, value]) => value !== "off" && BUILT_IN_MODES[id] !== value);
138
+ row(
139
+ "--",
140
+ "judges",
141
+ `${tiers.join(" > ")} · default mode ${mode}${env("JUDGE") ? " (from MU_JUDGE)" : ""}${asksJudge ? "" : " · no decision point asks a judge, so nothing is sent to one"}`,
142
+ );
123
143
  if (overrides.length > 0) row("--", "modes", overrides.map(([id, value]) => `${id}=${value}`).join(" "));
124
144
  const ladder = config?.features?.swarm?.models ?? [];
125
145
  row("--", "subagents", ladder.length > 0 ? `model ladder ${ladder.join(" < ")}` : "use the session's model (no ladder configured)");
126
146
 
127
147
  const judgeSupport = localJudgeSupport({ platform: process.platform, env: process.env, wsl });
128
- if (tiers.some((tier) => tier.startsWith("laya")) && !judgeSupport.runs && !judgeSupport.url) {
148
+ if (!asksJudge) {
149
+ // Nothing asks the local judge either: whether it is up does not matter.
150
+ } else if (tiers.some((tier) => tier.startsWith("laya")) && !judgeSupport.runs && !judgeSupport.url) {
129
151
  // Not a failure of this installation: the sidecar is Core ML, and there is none for this platform.
130
152
  row("--", "local judge", `not available on ${platformName({ platform: process.platform, wsl })}: the Laya sidecar needs Core ML (macOS on Apple silicon)`);
131
153
  row("--", "", "Laya's decision points use the next judge or their fallbacks. Use Jev, an LLM judge or MU_LOCAL_JUDGE_URL=<a server with the same API>");
@@ -174,8 +196,11 @@ const CLASSIFIER_KEYS = {
174
196
  const fromFile = existsSync(envFile) ? parseEnvFile(readFileSync(envFile, "utf8")).entries : [];
175
197
  const has = (name) => Boolean(process.env[name]) || fromFile.some(([key, value]) => key === name && value !== "");
176
198
  const where = `to ${tilde(envFile)} or to your environment`;
177
- const jevTier = tiers.find((tier) => ["jev", "typesafe", "gateway"].includes((config?.judges?.[tier] ?? JEV_ROUTES[tier])?.type));
178
- if (jevTier) {
199
+ // mude speaks System One too, but on this machine and with a key of its own (its rows are below).
200
+ const jevTier = tiers.find(
201
+ (tier) => tier !== "mude" && ["jev", "typesafe", "gateway"].includes((config?.judges?.[tier] ?? JEV_ROUTES[tier])?.type),
202
+ );
203
+ if (jevTier && asksJudge) {
179
204
  const judge = config?.judges?.[jevTier] ?? JEV_ROUTES[jevTier];
180
205
  if (judge.type === "jev") {
181
206
  // The order the registry tries: TypeSafe directly, Jev on OpenRouter, the Vercel AI Gateway, OpenCode Zen,
@@ -215,15 +240,40 @@ if (jevTier) {
215
240
  }
216
241
  }
217
242
 
243
+ // mude on this Mac (mu judge mude): its server's /health, and the key mu sends it.
244
+ if (asksJudge && tiers.includes("mude")) {
245
+ const fallback = tiers.filter((tier) => tier !== "mude");
246
+ const behind = fallback.length > 0 ? `until it answers, ${fallback.join(" > ")} decide${fallback.length === 1 ? "s" : ""} (what they read leaves this machine)` : "until it answers, decisions take their fallbacks";
247
+ if (process.platform !== "darwin") {
248
+ row("--", "mude 1.0", `not available on ${platformName({ platform: process.platform, wsl })}: mude 1.0 runs on a Mac with Apple silicon`);
249
+ } else {
250
+ const entry = config?.judges?.mude;
251
+ const address = (typeof entry?.baseUrl === "string" ? entry.baseUrl : "http://127.0.0.1:8790/v1/systemone").replace(/\/v1\/systemone\/?$/, "");
252
+ const startedAt = performance.now();
253
+ try {
254
+ const response = await fetch(`${address}/health`, { signal: AbortSignal.timeout(2000) });
255
+ const health = await response.json();
256
+ const ms = Math.round(performance.now() - startedAt);
257
+ row(health.status === "ready" ? "ok" : "--", "mude 1.0", `${address} · ${health.status} · ${ms} ms`);
258
+ } catch {
259
+ row("fix", "mude 1.0", `${address} isn't answering: run mu judge mude start (first time: mu judge mude install)`);
260
+ row("--", "", behind);
261
+ }
262
+ if (!has("MU_JUDGE_MUDE_KEY")) row("fix", "mude 1.0 key", "MU_JUDGE_MUDE_KEY is not set, so mu can't ask mude 1.0: run mu judge mude enable");
263
+ }
264
+ }
265
+
218
266
  // Classifier models of pi's catalog (Jev on OpenCode Zen or Cloudflare, Clef, …), for the tiers and routes that use them.
219
267
  const classifierOf = (tier) =>
220
268
  tier.startsWith("classifier:")
221
269
  ? { type: "classifier", model: tier.slice("classifier:".length) }
222
270
  : (config?.judges?.[tier] ?? JEV_ROUTES[tier]);
223
- const askedTiers = [
224
- ...tiers,
225
- ...Object.values(config?.routes ?? {}).flatMap((value) => (Array.isArray(value) ? value : String(value).split(","))),
226
- ].map((tier) => String(tier).trim());
271
+ const askedTiers = asksJudge
272
+ ? [
273
+ ...tiers,
274
+ ...Object.values(config?.routes ?? {}).flatMap((value) => (Array.isArray(value) ? value : String(value).split(","))),
275
+ ].map((tier) => String(tier).trim())
276
+ : [];
227
277
  for (const model of new Set(askedTiers.map(classifierOf).filter((judge) => judge?.type === "classifier").map((judge) => judge.model))) {
228
278
  const provider = String(model ?? "").split("/")[0];
229
279
  // OpenCode's free models answer without a key (the client sends "public", as OpenCode's own does).
@@ -325,12 +375,16 @@ const canRelate = (tier) =>
325
375
  const browseTiers = config?.routes?.["browser.step"] ?? tiers;
326
376
  const routed = Object.entries(config?.routes ?? {});
327
377
  if (routed.length > 0) row("--", "routes", routed.map(([id, value]) => `${id} -> ${[value].flat().join(" > ")}`).join(" · "));
378
+ // mu's browser runs on the judge: with browser.step off it is switched off (src/extension/features/browser.ts).
379
+ const browseMode = config?.modes?.["browser.step"] ?? mode;
328
380
  row(
329
- [browseTiers].flat().some(canRelate) ? "ok" : "--",
381
+ browseMode !== "off" && [browseTiers].flat().some(canRelate) ? "ok" : "--",
330
382
  "browse",
331
- [browseTiers].flat().some(canRelate)
332
- ? `driven by ${[browseTiers].flat().join(" > ")}`
333
- : 'needs a judge that can match a goal to a page. In mu.json: "routes": { "browser.step": ["llm:openai/gpt-5.6-luna"] }',
383
+ browseMode === "off"
384
+ ? "off: mu's browser runs on the judge, and browser.step is off"
385
+ : [browseTiers].flat().some(canRelate)
386
+ ? `driven by ${[browseTiers].flat().join(" > ")}`
387
+ : 'needs a judge that can match a goal to a page. In mu.json: "routes": { "browser.step": ["llm:openai/gpt-5.6-luna"] }',
334
388
  );
335
389
 
336
390
  const marks = { ok: "\x1b[32m ok\x1b[0m", fix: "\x1b[33mfix\x1b[0m", "--": "\x1b[2m ·\x1b[0m" };
package/kyrn/bin/mu.mjs CHANGED
@@ -102,6 +102,8 @@ export function usage(platform) {
102
102
  " mu -c | -r continue the last session | pick a session to resume",
103
103
  " mu setup connect a model (paste an API key or pick a service) and choose a judge",
104
104
  " (see mu setup --help for scripting, reading a key from stdin and the supported services)",
105
+ " mu judge mude install run mude 1.0, mu's decision model optimized for coding, on this Mac as the judge (Apple silicon, 1.8 GB)",
106
+ " enable | disable | start | stop | status | uninstall (mu judge mude help)",
105
107
  " mu judge <cmd> manage the local judge, Laya (macOS on Apple silicon): setup | start | stop | status | run",
106
108
  " mu ledger [n] [--json] print the judge's verdicts from the last n sessions",
107
109
  " mu import <file>... import Claude Code, Codex and ZCode conversations; --list shows them (mu import --help)",
@@ -321,8 +323,8 @@ export function layoutOf({ root, platform, exists }) {
321
323
  }
322
324
 
323
325
  /**
324
- * The files the npm package runs: pi, the judgment layer, the sign-in of `mu auth`, the importer of `mu import` and the
325
- * wizard of `mu setup`.
326
+ * The files the npm package runs: pi, the judgment layer, the sign-in of `mu auth`, the importer of `mu import`, the
327
+ * wizard of `mu setup` and `mu judge mude`.
326
328
  */
327
329
  export function packageEntries({ root, platform }) {
328
330
  const path = pathFor(platform);
@@ -332,6 +334,7 @@ export function packageEntries({ root, platform }) {
332
334
  auth: path.join(root, "judge", "dist", "auth.js"),
333
335
  import: path.join(root, "judge", "dist", "import.js"),
334
336
  setup: path.join(root, "judge", "dist", "setup.js"),
337
+ mude: path.join(root, "judge", "dist", "mude.js"),
335
338
  };
336
339
  }
337
340
 
@@ -354,12 +357,26 @@ export function dependenciesInstalled({ root, platform, exists }) {
354
357
  }
355
358
  }
356
359
 
357
- export function installHint({ root, platform }) {
358
- const steps = [`cd ${root}`, "npm ci --ignore-scripts", "npm run hydrate:model-data"];
359
- // Windows PowerShell 5 has no `&&`.
360
+ /**
361
+ * Writes the model list pi reads at its start (packages/ai/src/providers/data, not committed) from the pinned catalog in
362
+ * the repository: offline and the same every time, as CI builds it.
363
+ */
364
+ const HYDRATE = "npm run hydrate:model-catalog";
365
+
366
+ /** A problem and the commands that fix it, one line to paste; Windows PowerShell 5 has no `&&`, so a list there. */
367
+ function stepsHint(problem, steps, platform) {
360
368
  return platform === "win32"
361
- ? `mu's dependencies aren't installed. Run:\n${steps.map((step) => ` ${step}`).join("\n")}`
362
- : `mu's dependencies aren't installed. Run: ${steps.join(" && ")}`;
369
+ ? `${problem}. Run:\n${steps.map((step) => ` ${step}`).join("\n")}`
370
+ : `${problem}. Run: ${steps.join(" && ")}`;
371
+ }
372
+
373
+ export function installHint({ root, platform }) {
374
+ return stepsHint("mu's dependencies aren't installed", [`cd ${root}`, "npm ci --ignore-scripts", HYDRATE], platform);
375
+ }
376
+
377
+ /** A checkout whose dependencies are in but whose model list was never written: a clone right after npm install. */
378
+ export function modelDataHint({ root, platform }) {
379
+ return stepsHint("mu's list of models isn't generated in this checkout yet", [`cd ${root}`, HYDRATE], platform);
363
380
  }
364
381
 
365
382
  /** What mu says on a Node that does not strip TypeScript types, which a checkout cannot run on. */
@@ -372,12 +389,16 @@ const NO_TYPE_STRIPPING =
372
389
  * pi's own source resolver (packages/coding-agent/src/experimental/source-resolver.ts: each workspace package
373
390
  * resolves to its sources, as tsconfig.json's paths say) and Node's compile cache (compile-cache.mjs), the way pi
374
391
  * runs its own checkout (pi-test.sh). A runtime that does not strip types, a checkout whose dependencies are not
375
- * installed and one without the resolver are each said, not left to a failed import.
392
+ * installed or whose model list is not generated, and one without the resolver are each said, not left to a failed
393
+ * import.
376
394
  */
377
395
  export function sourceRuntime({ root, platform, stripsTypes, exists }) {
378
396
  const path = pathFor(platform);
379
397
  if (!stripsTypes) return { error: NO_TYPE_STRIPPING };
380
398
  if (!dependenciesInstalled({ root, platform, exists })) return { error: installHint({ root, platform }) };
399
+ // Without it pi fails at its first import with ERR_MODULE_NOT_FOUND for a provider's .json.
400
+ if (!exists(path.join(root, "packages", "ai", "src", "providers", "data", ".manifest.json")))
401
+ return { error: modelDataHint({ root, platform }) };
381
402
  const resolver = path.join(root, "packages", "coding-agent", "src", "experimental", "source-resolver.ts");
382
403
  if (!exists(resolver)) return { error: `pi's source resolver is missing from this checkout (${resolver})` };
383
404
  // As URLs: `--import` reads a Windows path such as C:\... as a URL whose scheme is c:.
@@ -422,6 +443,28 @@ export function wantsLaya({ env, agentDir, platform, readFile }) {
422
443
  }
423
444
 
424
445
  /** The Core ML sidecar exists for macOS only. Anything else needs a server of its own behind MU_LOCAL_JUDGE_URL. */
446
+ /**
447
+ * Whether the configuration asks for mude (mu judge mude): MU_JUDGE naming it, else `tiers` or a route in mu.json (or
448
+ * kyrn.json when there is no mu.json). Its server is then started when a session starts, as Laya's is.
449
+ */
450
+ export function wantsMude({ env, agentDir, platform, readFile }) {
451
+ const judges = muEnv("JUDGE", env);
452
+ if (judges) return judges.split(",").some((judge) => judge.trim() === "mude");
453
+ const path = pathFor(platform);
454
+ for (const name of ["mu.json", "kyrn.json"]) {
455
+ let config;
456
+ try {
457
+ config = JSON.parse(readFile(path.join(agentDir, name)));
458
+ } catch (error) {
459
+ if (error instanceof SyntaxError) return false;
460
+ continue;
461
+ }
462
+ const named = (tiers) => Array.isArray(tiers) && tiers.includes("mude");
463
+ return named(config?.tiers) || Object.values(config?.routes ?? {}).some(named);
464
+ }
465
+ return false;
466
+ }
467
+
425
468
  export function localJudgeSupport({ platform, env, wsl = false }) {
426
469
  if (platform === "darwin") return { runs: true };
427
470
  const url = muEnv("LOCAL_JUDGE_URL", env);
@@ -562,6 +605,14 @@ export function planLaunch({
562
605
  }
563
606
  }
564
607
 
608
+ // mude's server, when the configuration asks for it: started without waiting, so a session never waits for the
609
+ // model to load (what mude cannot answer yet goes to the judges behind it). Only a Mac runs it.
610
+ let startMude;
611
+ if (execPath && platform === "darwin" && wantsMude({ env: childEnv, agentDir, platform, readFile: fs.readFile })) {
612
+ const mude = planMude({ platform, env: childEnv, argv: ["start", "--no-wait"], root, home, execPath, fs, stripsTypes });
613
+ if (!mude.error) startMude = { command: mude.command, args: mude.args, env: mude.env };
614
+ }
615
+
565
616
  return {
566
617
  command: execPath,
567
618
  args: [...entry, ...judgeArgs({ argv, layout, root, platform, agentDir, readFile: fs.readFile }), ...argv],
@@ -572,6 +623,7 @@ export function planLaunch({
572
623
  appDir,
573
624
  agentDir,
574
625
  startJudge,
626
+ startMude,
575
627
  notes,
576
628
  preface: argv[0] === "-h" || argv[0] === "--help" ? `${usage(platform)}\n\npi's help (mu passes all of this through):\n` : undefined,
577
629
  };
@@ -588,10 +640,17 @@ export function planLaunch({
588
640
  *
589
641
  * A checkout runs there as it runs on the command line (sourceRuntime). Returns { error } when pi cannot run.
590
642
  */
591
- export function planHost({ platform, env, argv, root, home, fs, wsl = false, stripsTypes = true }) {
643
+ export function planHost({ platform, env, argv, root, home, fs, wsl = false, stripsTypes = true, nodePath, nodeEnv = {} }) {
592
644
  const path = pathFor(platform);
593
645
  const launch = planLaunch({ platform, env, argv, root, home, execPath: "", fs, wsl, stripsTypes });
594
646
  if (launch.error !== undefined) return { error: launch.error };
647
+ // mude's server is started by a Node of the host's: `nodePath` with `nodeEnv` (Electron's own binary with
648
+ // ELECTRON_RUN_AS_NODE=1 in the desktop app). Without one, the host leaves it to the app.
649
+ let startMude;
650
+ if (nodePath && platform === "darwin" && wantsMude({ env: launch.env, agentDir: launch.agentDir, platform, readFile: fs.readFile })) {
651
+ const mude = planMude({ platform, env: { ...launch.env, ...nodeEnv }, argv: ["start", "--no-wait"], root, home, execPath: nodePath, fs, stripsTypes });
652
+ if (!mude.error) startMude = { command: mude.command, args: mude.args, env: mude.env };
653
+ }
595
654
  const common = {
596
655
  env: launch.env,
597
656
  layout: launch.layout,
@@ -599,6 +658,7 @@ export function planHost({ platform, env, argv, root, home, fs, wsl = false, str
599
658
  appDir: launch.appDir,
600
659
  agentDir: launch.agentDir,
601
660
  startJudge: launch.startJudge,
661
+ startMude,
602
662
  notes: launch.notes,
603
663
  };
604
664
  if (launch.layout === "package") {
@@ -643,6 +703,7 @@ function prepareFiles({ plan, platform, root, err }) {
643
703
  */
644
704
  export function prepareLaunch({ plan, platform, root, bin, err }) {
645
705
  prepareFiles({ plan, platform, root, err });
706
+ startMudeServer(plan);
646
707
  if (plan.startJudge) {
647
708
  const started = spawnSync(pathFor(platform).join(bin, "kyrn-judge-local"), ["start"], {
648
709
  stdio: "ignore",
@@ -652,12 +713,32 @@ export function prepareLaunch({ plan, platform, root, bin, err }) {
652
713
  }
653
714
  }
654
715
 
716
+ /**
717
+ * mude's server, started in the background (`mu judge mude start --no-wait`): nothing waits for it, and it outlives this
718
+ * process, shared by every mu session like Laya. When mude is not installed it does nothing (the doctor says so).
719
+ */
720
+ function startMudeServer(plan) {
721
+ if (!plan.startMude) return;
722
+ try {
723
+ const child = spawn(plan.startMude.command, plan.startMude.args, {
724
+ env: plan.startMude.env,
725
+ stdio: "ignore",
726
+ detached: true,
727
+ });
728
+ child.once("error", () => {});
729
+ child.unref();
730
+ } catch {
731
+ // mu works without it: the judges behind mude answer.
732
+ }
733
+ }
734
+
655
735
  /**
656
736
  * prepareLaunch for a program that must not stop while the local judge starts, such as the desktop app's main process:
657
737
  * the same steps and notes, with the local judge started through a child process it waits for without blocking.
658
738
  */
659
739
  export async function prepareLaunchAsync({ plan, platform, root, bin, err }) {
660
740
  prepareFiles({ plan, platform, root, err });
741
+ startMudeServer(plan);
661
742
  if (!plan.startJudge) return;
662
743
  const status = await new Promise((resolve) => {
663
744
  const child = spawn(pathFor(platform).join(bin, "kyrn-judge-local"), ["start"], {
@@ -775,6 +856,42 @@ export function planImport({ platform, env, argv, root, home, execPath, fs, stri
775
856
  return { command: execPath, args: [...entry, ...argv], env: childEnv, agentDir };
776
857
  }
777
858
 
859
+ // ---------------------------------------------------------------------------------------------------------
860
+ // mu judge mude
861
+ // ---------------------------------------------------------------------------------------------------------
862
+
863
+ /**
864
+ * `mu judge mude <command>` (packages/kyrn-judge/src/mude): mude on this Mac, from the checkout's TypeScript or the
865
+ * package's built judge/dist/mude.js. It is told where mu keeps things: the agent folder (mu.json), mu's home (the
866
+ * model and its environment go under local-judge/mude), the .env (its key) and the lock of its Python packages.
867
+ */
868
+ export function planMude({ platform, env, argv, root, home, execPath, fs, stripsTypes = true }) {
869
+ const path = pathFor(platform);
870
+ const layout = layoutOf({ root, platform, exists: fs.exists });
871
+ let entry;
872
+ if (layout === "package") {
873
+ const built = packageEntries({ root, platform }).mude;
874
+ if (!fs.exists(built)) {
875
+ return { error: `This mu-agent package has no mu judge mude (${built} is missing). Update it: npm i -g mu-agent` };
876
+ }
877
+ entry = [built];
878
+ } else {
879
+ const source = path.join(root, "packages", "kyrn-judge", "src", "mude", "cli.ts");
880
+ if (!fs.exists(source)) return { error: `mu judge mude is missing from this checkout (${source})` };
881
+ if (!stripsTypes) return { error: NO_TYPE_STRIPPING };
882
+ entry = ["--disable-warning=ExperimentalWarning", source];
883
+ }
884
+ const muDir = muHome({ home, platform, isDir: fs.isDir });
885
+ const agentDir = agentDirFor({ env, muDir, platform });
886
+ const childEnv = {};
887
+ for (const [key, value] of Object.entries(env)) if (typeof value === "string") childEnv[key] = value;
888
+ childEnv.MU_AGENT_DIR = agentDir;
889
+ childEnv.MU_MUDE_HOME = muDir;
890
+ childEnv.MU_MUDE_ENV_FILE = envFilePath({ layout, root, muDir, platform });
891
+ childEnv.MU_MUDE_LOCK = path.join(root, "kyrn", "local-judge", "mude-requirements.lock.txt");
892
+ return { command: execPath, args: [...entry, ...argv], env: childEnv, agentDir };
893
+ }
894
+
778
895
  // ---------------------------------------------------------------------------------------------------------
779
896
  // mu setup
780
897
  // ---------------------------------------------------------------------------------------------------------
@@ -1486,6 +1603,23 @@ export async function main(argv = process.argv.slice(2)) {
1486
1603
  },
1487
1604
  });
1488
1605
  }
1606
+ if (command === "judge" && rest[0] === "mude") {
1607
+ const plan = planMude({
1608
+ platform,
1609
+ env,
1610
+ argv: rest.slice(1),
1611
+ root,
1612
+ home,
1613
+ execPath: process.execPath,
1614
+ stripsTypes: Boolean(process.features.typescript),
1615
+ fs: { exists: existsSync, isDir, readFile: readText },
1616
+ });
1617
+ if (plan.error) {
1618
+ err(plan.error);
1619
+ return 1;
1620
+ }
1621
+ return handOver({ command: plan.command, args: plan.args, env: plan.env, strategy: "spawn" });
1622
+ }
1489
1623
  if (command === "judge") {
1490
1624
  const plan = planJudge({ platform, env, argv: rest, bin, wsl });
1491
1625
  if (plan.kind === "script") return handOver({ command: plan.command, args: plan.args, env, strategy });