programmatic subagents
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli)!: enforce strict active driver resolution

+495 -87
+236 -36
packages/cli/src/public/index.api.test.ts
··· 5 5 import * as Schema from "@effect/schema/Schema"; 6 6 import { runCli } from "./index.api"; 7 7 8 + const TEST_HARNESS_ENV = { 9 + CODEX_THREAD_ID: "test-thread-id", 10 + } as const; 11 + 12 + const runCliForTest = async ( 13 + argv: ReadonlyArray<string>, 14 + options?: Parameters<typeof runCli>[1], 15 + ): Promise<number> => { 16 + const previousDepth = process.env.MILL_RUN_DEPTH; 17 + 18 + delete process.env.MILL_RUN_DEPTH; 19 + 20 + try { 21 + return await runCli(argv, { 22 + env: TEST_HARNESS_ENV, 23 + ...(options ?? {}), 24 + }); 25 + } finally { 26 + if (previousDepth === undefined) { 27 + delete process.env.MILL_RUN_DEPTH; 28 + } else { 29 + process.env.MILL_RUN_DEPTH = previousDepth; 30 + } 31 + } 32 + }; 33 + 8 34 const RunSyncEnvelope = Schema.parseJson( 9 35 Schema.Struct({ 10 36 run: Schema.Struct({ ··· 120 146 const stdout: Array<string> = []; 121 147 const stderr: Array<string> = []; 122 148 123 - const code = await runCli(["discovery", "--json"], { 149 + const code = await runCliForTest(["discovery", "--json"], { 124 150 cwd: "/workspace/repo", 125 151 homeDirectory: "/Users/tester", 126 152 pathExists: async () => false, ··· 151 177 ' agent: "scout",', 152 178 ' systemPrompt: "You are concise.",', 153 179 ' prompt: "Say hello",', 180 + ' model: "openai-codex/gpt-5.3-codex",', 154 181 "});", 155 182 "globalThis.__millLastText = scan.text;", 156 183 ].join("\n"), ··· 161 188 const runStderr: Array<string> = []; 162 189 163 190 try { 164 - const runCode = await runCli(["run", programPath, "--sync", "--json"], { 191 + const runCode = await runCliForTest(["run", programPath, "--sync", "--json"], { 165 192 cwd: tempDirectory, 166 193 homeDirectory, 167 194 pathExists: async () => false, ··· 181 208 182 209 const runPayload = Schema.decodeUnknownSync(RunSyncEnvelope)(runStdout[0]); 183 210 expect(runPayload.run.status).toBe("complete"); 184 - expect(runPayload.run.driver).toBe("pi"); 211 + expect(runPayload.run.driver).toBe("codex"); 185 212 expect(runPayload.run.executor).toBe("direct"); 186 213 expect(runPayload.result.status).toBe("complete"); 187 214 expect(runPayload.result.spawns).toHaveLength(1); ··· 189 216 const statusStdout: Array<string> = []; 190 217 const statusStderr: Array<string> = []; 191 218 192 - const statusCode = await runCli(["status", runPayload.run.id, "--json"], { 219 + const statusCode = await runCliForTest(["status", runPayload.run.id, "--json"], { 193 220 cwd: tempDirectory, 194 221 homeDirectory, 195 222 pathExists: async () => false, ··· 210 237 const statusPayload = Schema.decodeUnknownSync(StatusEnvelope)(statusStdout[0]); 211 238 expect(statusPayload.id).toBe(runPayload.run.id); 212 239 expect(statusPayload.status).toBe("complete"); 213 - expect(statusPayload.driver).toBe("pi"); 240 + expect(statusPayload.driver).toBe("codex"); 214 241 expect(statusPayload.executor).toBe("direct"); 215 242 } finally { 216 243 await rm(tempDirectory, { recursive: true, force: true }); ··· 229 256 ' agent: "scout",', 230 257 ' systemPrompt: "You are concise.",', 231 258 ' prompt: "Say hello",', 259 + ' model: "openai-codex/gpt-5.3-codex",', 232 260 "});", 233 261 "return scan.text;", 234 262 ].join("\n"), ··· 237 265 238 266 try { 239 267 const runStdout: Array<string> = []; 240 - const runCode = await runCli( 268 + const runCode = await runCliForTest( 241 269 ["run", programPath, "--sync", "--json", "--driver", "pi", "--executor", "direct"], 242 270 { 243 271 cwd: tempDirectory, ··· 247 275 default: { 248 276 defaultDriver: "claude", 249 277 defaultExecutor: "direct", 250 - defaultModel: "google-gemini-cli/gemini-2.0-flash", 251 278 }, 252 279 }), 253 280 io: { ··· 282 309 ' agent: "scout",', 283 310 ' systemPrompt: "You are concise.",', 284 311 ' prompt: "Say hello",', 312 + ' model: "openai-codex/gpt-5.3-codex",', 285 313 "});", 286 314 "return scan.text;", 287 315 ].join("\n"), ··· 290 318 291 319 try { 292 320 const runStdout: Array<string> = []; 293 - const runCode = await runCli(["run", programPath, "--sync", "--json"], { 321 + const runCode = await runCliForTest(["run", programPath, "--sync", "--json"], { 294 322 cwd: tempDirectory, 295 323 homeDirectory, 296 324 pathExists: async (path) => path === join(tempDirectory, "mill.config.ts"), ··· 319 347 } 320 348 }, 15_000); 321 349 350 + it("prefers Claude harness inference over Codex markers when both are present", async () => { 351 + const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-harness-priority-")); 352 + const homeDirectory = join(tempDirectory, "home"); 353 + const programPath = join(tempDirectory, "program.ts"); 354 + 355 + await writeFile( 356 + programPath, 357 + [ 358 + "const scan = await mill.spawn({", 359 + ' agent: "scout",', 360 + ' systemPrompt: "You are concise.",', 361 + ' prompt: "Say hello",', 362 + ' model: "openai-codex/gpt-5.3-codex",', 363 + "});", 364 + "return scan.text;", 365 + ].join("\n"), 366 + "utf-8", 367 + ); 368 + 369 + try { 370 + const runStdout: Array<string> = []; 371 + const runCode = await runCliForTest(["run", programPath, "--sync", "--json"], { 372 + cwd: tempDirectory, 373 + homeDirectory, 374 + pathExists: async () => false, 375 + env: { 376 + CLAUDECODE: "1", 377 + CODEX_THREAD_ID: "test-thread-id", 378 + }, 379 + io: { 380 + stdout: (line) => { 381 + runStdout.push(line); 382 + }, 383 + stderr: () => undefined, 384 + }, 385 + }); 386 + 387 + expect(runCode).toBe(0); 388 + const payload = Schema.decodeUnknownSync(RunSyncEnvelope)(runStdout[0]); 389 + expect(payload.run.driver).toBe("claude"); 390 + expect(payload.result.spawns[0]?.driver).toBe("claude"); 391 + } finally { 392 + await rm(tempDirectory, { recursive: true, force: true }); 393 + } 394 + }, 15_000); 395 + 396 + it("fails with a helpful error when no active driver can be resolved", async () => { 397 + const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-driver-missing-")); 398 + const homeDirectory = join(tempDirectory, "home"); 399 + const programPath = join(tempDirectory, "program.ts"); 400 + 401 + await writeFile( 402 + programPath, 403 + [ 404 + "const scan = await mill.spawn({", 405 + ' agent: "scout",', 406 + ' systemPrompt: "You are concise.",', 407 + ' prompt: "Say hello",', 408 + ' model: "openai-codex/gpt-5.3-codex",', 409 + "});", 410 + "return scan.text;", 411 + ].join("\n"), 412 + "utf-8", 413 + ); 414 + 415 + try { 416 + const stdout: Array<string> = []; 417 + const stderr: Array<string> = []; 418 + 419 + const code = await runCliForTest(["run", programPath, "--sync", "--json"], { 420 + cwd: tempDirectory, 421 + homeDirectory, 422 + pathExists: async () => false, 423 + env: {}, 424 + io: { 425 + stdout: (line) => { 426 + stdout.push(line); 427 + }, 428 + stderr: (line) => { 429 + stderr.push(line); 430 + }, 431 + }, 432 + }); 433 + 434 + expect(code).toBe(1); 435 + expect(stdout).toHaveLength(0); 436 + expect(stderr.join("\n")).toContain("Unable to resolve active driver"); 437 + } finally { 438 + await rm(tempDirectory, { recursive: true, force: true }); 439 + } 440 + }); 441 + 442 + it("fails when configured defaultDriver is unavailable", async () => { 443 + const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-driver-invalid-")); 444 + const homeDirectory = join(tempDirectory, "home"); 445 + const programPath = join(tempDirectory, "program.ts"); 446 + 447 + await writeFile( 448 + programPath, 449 + [ 450 + "const scan = await mill.spawn({", 451 + ' agent: "scout",', 452 + ' systemPrompt: "You are concise.",', 453 + ' prompt: "Say hello",', 454 + ' model: "openai-codex/gpt-5.3-codex",', 455 + "});", 456 + "return scan.text;", 457 + ].join("\n"), 458 + "utf-8", 459 + ); 460 + 461 + try { 462 + const stdout: Array<string> = []; 463 + const stderr: Array<string> = []; 464 + 465 + const code = await runCliForTest(["run", programPath, "--sync", "--json"], { 466 + cwd: tempDirectory, 467 + homeDirectory, 468 + pathExists: async (path) => path === join(tempDirectory, "mill.config.ts"), 469 + loadConfigModule: async () => ({ 470 + default: { 471 + defaultDriver: "missing-driver", 472 + }, 473 + }), 474 + io: { 475 + stdout: (line) => { 476 + stdout.push(line); 477 + }, 478 + stderr: (line) => { 479 + stderr.push(line); 480 + }, 481 + }, 482 + }); 483 + 484 + expect(code).toBe(1); 485 + expect(stdout).toHaveLength(0); 486 + expect(stderr.join("\n")).toContain("Resolved active driver 'missing-driver'"); 487 + expect(stderr.join("\n")).toContain("Available drivers: claude, codex, pi"); 488 + } finally { 489 + await rm(tempDirectory, { recursive: true, force: true }); 490 + } 491 + }); 492 + 322 493 it("submits run asynchronously by default and writes worker artifacts", async () => { 323 494 const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-async-run-")); 324 495 const homeDirectory = join(tempDirectory, "home"); ··· 331 502 ' agent: "scout",', 332 503 ' systemPrompt: "You are concise.",', 333 504 ' prompt: "Say hello",', 505 + ' model: "openai-codex/gpt-5.3-codex",', 334 506 "});", 335 507 "globalThis.__millAsyncText = scan.text;", 336 508 ].join("\n"), ··· 341 513 const runStdout: Array<string> = []; 342 514 const runStderr: Array<string> = []; 343 515 344 - const runCode = await runCli(["run", programPath, "--json"], { 516 + const runCode = await runCliForTest(["run", programPath, "--json"], { 345 517 cwd: tempDirectory, 346 518 homeDirectory, 347 519 pathExists: async () => false, ··· 363 535 expect(submittedRun.runId.length).toBeGreaterThan(0); 364 536 365 537 const waitStdout: Array<string> = []; 366 - const waitCode = await runCli(["wait", submittedRun.runId, "--timeout", "5", "--json"], { 538 + const waitCode = await runCliForTest(["wait", submittedRun.runId, "--timeout", "5", "--json"], { 367 539 cwd: tempDirectory, 368 540 homeDirectory, 369 541 pathExists: async () => false, ··· 404 576 ' agent: "scout",', 405 577 ' systemPrompt: "You are concise.",', 406 578 ' prompt: "Say hello",', 579 + ' model: "openai-codex/gpt-5.3-codex",', 407 580 "});", 408 581 "globalThis.__millWaitText = scan.text;", 409 582 ].join("\n"), ··· 412 585 413 586 try { 414 587 const runStdout: Array<string> = []; 415 - const runCode = await runCli(["run", programPath, "--sync", "--json"], { 588 + const runCode = await runCliForTest(["run", programPath, "--sync", "--json"], { 416 589 cwd: tempDirectory, 417 590 homeDirectory, 418 591 pathExists: async () => false, ··· 429 602 430 603 const waitJsonStdout: Array<string> = []; 431 604 const waitJsonStderr: Array<string> = []; 432 - const waitJsonCode = await runCli(["wait", runPayload.run.id, "--timeout", "2", "--json"], { 605 + const waitJsonCode = await runCliForTest(["wait", runPayload.run.id, "--timeout", "2", "--json"], { 433 606 cwd: tempDirectory, 434 607 homeDirectory, 435 608 pathExists: async () => false, ··· 451 624 452 625 const waitHumanStdout: Array<string> = []; 453 626 const waitHumanStderr: Array<string> = []; 454 - const waitHumanCode = await runCli(["wait", runPayload.run.id, "--timeout", "2"], { 627 + const waitHumanCode = await runCliForTest(["wait", runPayload.run.id, "--timeout", "2"], { 455 628 cwd: tempDirectory, 456 629 homeDirectory, 457 630 pathExists: async () => false, ··· 486 659 ' agent: "scout",', 487 660 ' systemPrompt: "You are concise.",', 488 661 ' prompt: "Say hello",', 662 + ' model: "openai-codex/gpt-5.3-codex",', 489 663 "});", 490 664 "return scan.text;", 491 665 ].join("\n"), ··· 494 668 495 669 try { 496 670 const runStdout: Array<string> = []; 497 - const runCode = await runCli(["run", programPath, "--sync", "--json"], { 671 + const runCode = await runCliForTest(["run", programPath, "--sync", "--json"], { 498 672 cwd: tempDirectory, 499 673 homeDirectory, 500 674 pathExists: async () => false, ··· 510 684 const runPayload = Schema.decodeUnknownSync(RunSyncEnvelope)(runStdout[0]); 511 685 512 686 const watchStdout: Array<string> = []; 513 - const watchCode = await runCli( 687 + const watchCode = await runCliForTest( 514 688 ["watch", "--run", runPayload.run.id, "--channel", "all", "--json"], 515 689 { 516 690 cwd: tempDirectory, ··· 537 711 ).toBe(true); 538 712 539 713 const listStdout: Array<string> = []; 540 - const listCode = await runCli(["ls", "--json"], { 714 + const listCode = await runCliForTest(["ls", "--json"], { 541 715 cwd: tempDirectory, 542 716 homeDirectory, 543 717 pathExists: async () => false, ··· 570 744 571 745 try { 572 746 const runStdout: Array<string> = []; 573 - const runCode = await runCli(["run", programPath, "--json"], { 747 + const runCode = await runCliForTest(["run", programPath, "--json"], { 574 748 cwd: tempDirectory, 575 749 homeDirectory, 576 750 pathExists: async () => false, ··· 586 760 const submittedRun = Schema.decodeUnknownSync(RunSubmitEnvelope)(runStdout[0]); 587 761 588 762 const cancelStdout1: Array<string> = []; 589 - const cancelCode1 = await runCli(["cancel", submittedRun.runId, "--json"], { 763 + const cancelCode1 = await runCliForTest(["cancel", submittedRun.runId, "--json"], { 590 764 cwd: tempDirectory, 591 765 homeDirectory, 592 766 pathExists: async () => false, ··· 604 778 expect(firstCancel.status).toBe("cancelled"); 605 779 606 780 const cancelStdout2: Array<string> = []; 607 - const cancelCode2 = await runCli(["cancel", submittedRun.runId, "--json"], { 781 + const cancelCode2 = await runCliForTest(["cancel", submittedRun.runId, "--json"], { 608 782 cwd: tempDirectory, 609 783 homeDirectory, 610 784 pathExists: async () => false, ··· 638 812 expect(terminalEvents[0]?.type).toBe("run:cancelled"); 639 813 640 814 const statusAfterCancelStdout: Array<string> = []; 641 - const statusAfterCancelCode = await runCli(["status", submittedRun.runId, "--json"], { 815 + const statusAfterCancelCode = await runCliForTest(["status", submittedRun.runId, "--json"], { 642 816 cwd: tempDirectory, 643 817 homeDirectory, 644 818 pathExists: async () => false, ··· 670 844 ' agent: "scout",', 671 845 ' systemPrompt: "You are concise.",', 672 846 ' prompt: "Say hello",', 847 + ' model: "openai-codex/gpt-5.3-codex",', 673 848 "});", 674 849 "return scan.text;", 675 850 ].join("\n"), ··· 678 853 679 854 try { 680 855 const runStdout: Array<string> = []; 681 - const runCode = await runCli(["run", programPath, "--json"], { 856 + const runCode = await runCliForTest(["run", programPath, "--json"], { 682 857 cwd: tempDirectory, 683 858 homeDirectory, 684 859 pathExists: async () => false, ··· 698 873 ]; 699 874 700 875 setTimeout(() => { 701 - void runCli(workerArgs, { 876 + void runCliForTest(workerArgs, { 702 877 cwd: input.cwd, 703 878 homeDirectory, 704 879 pathExists: async () => false, ··· 721 896 const submittedRun = Schema.decodeUnknownSync(RunSubmitEnvelope)(runStdout[0]); 722 897 723 898 const watchStdout: Array<string> = []; 724 - const watchCode = await runCli( 899 + const watchCode = await runCliForTest( 725 900 ["watch", "--run", submittedRun.runId, "--channel", "io", "--json"], 726 901 { 727 902 cwd: tempDirectory, ··· 759 934 const stderr: Array<string> = []; 760 935 761 936 try { 762 - const code = await runCli(["init"], { 937 + const code = await runCliForTest(["init"], { 763 938 cwd: tempDirectory, 764 939 homeDirectory: join(tempDirectory, "home"), 765 940 io: { ··· 794 969 try { 795 970 await mkdir(homeDirectory, { recursive: true }); 796 971 797 - const code = await runCli(["init", "--global"], { 972 + const code = await runCliForTest(["init", "--global"], { 798 973 cwd: tempDirectory, 799 974 homeDirectory, 800 975 io: { ··· 820 995 } 821 996 }); 822 997 998 + it("shows driver resolution guidance in root help when active driver is unresolved", async () => { 999 + const stdout: Array<string> = []; 1000 + const stderr: Array<string> = []; 1001 + 1002 + const code = await runCliForTest([], { 1003 + cwd: "/workspace/repo", 1004 + homeDirectory: "/Users/tester", 1005 + pathExists: async () => false, 1006 + env: {}, 1007 + io: { 1008 + stdout: (line) => { 1009 + stdout.push(line); 1010 + }, 1011 + stderr: (line) => { 1012 + stderr.push(line); 1013 + }, 1014 + }, 1015 + }); 1016 + 1017 + expect(code).toBe(0); 1018 + expect(stderr).toHaveLength(0); 1019 + expect(stdout[0]).toContain("Models:"); 1020 + expect(stdout[0]).toContain("unavailable: Unable to resolve active driver"); 1021 + }); 1022 + 823 1023 it("uses config authoring instructions in root help when configured", async () => { 824 1024 const stdout: Array<string> = []; 825 1025 const stderr: Array<string> = []; 826 1026 827 - const code = await runCli([], { 1027 + const code = await runCliForTest([], { 828 1028 cwd: "/workspace/repo", 829 1029 homeDirectory: "/Users/tester", 830 1030 pathExists: async (path) => path === "/Users/tester/.mill/config.ts", ··· 848 1048 expect(code).toBe(0); 849 1049 expect(stderr).toHaveLength(0); 850 1050 expect(stdout[0]).toContain("Models:"); 851 - expect(stdout[0]).toContain("pi (provider/model-id):"); 852 - expect(stdout[0]).not.toContain("codex (provider/model-id):"); 1051 + expect(stdout[0]).toContain("codex (provider/model-id):"); 1052 + expect(stdout[0]).not.toContain("pi (provider/model-id):"); 853 1053 expect(stdout[0]).toContain("Authoring:\n CUSTOM_AUTHORING_INSTRUCTIONS"); 854 1054 expect(stdout[0]).not.toContain("systemPrompt = WHO the agent is"); 855 1055 }); ··· 858 1058 const stdout: Array<string> = []; 859 1059 const stderr: Array<string> = []; 860 1060 861 - const code = await runCli([], { 1061 + const code = await runCliForTest([], { 862 1062 cwd: "/workspace/repo", 863 1063 homeDirectory: "/Users/tester", 864 1064 pathExists: async (path) => path === "/Users/tester/.mill/config.ts", ··· 891 1091 const stdout: Array<string> = []; 892 1092 const stderr: Array<string> = []; 893 1093 894 - const code = await runCli(["run", "--help"], { 1094 + const code = await runCliForTest(["run", "--help"], { 895 1095 cwd: "/workspace/repo", 896 1096 homeDirectory: "/Users/tester", 897 1097 pathExists: async (path) => path === "/Users/tester/.mill/config.ts", ··· 918 1118 "Authoring (from config): CUSTOM_AUTHORING_IN_COMMAND_HELP", 919 1119 ); 920 1120 expect(stdout.join("\n")).toContain("Models:"); 921 - expect(stdout.join("\n")).toContain("pi (provider/model-id):"); 922 - expect(stdout.join("\n")).not.toContain("codex (provider/model-id):"); 1121 + expect(stdout.join("\n")).toContain("codex (provider/model-id):"); 1122 + expect(stdout.join("\n")).not.toContain("pi (provider/model-id):"); 923 1123 expect(stdout.join("\n")).not.toContain("systemPrompt = WHO the agent is"); 924 1124 }); 925 1125 ··· 927 1127 const stdout: Array<string> = []; 928 1128 const stderr: Array<string> = []; 929 1129 930 - const code = await runCli(["run", "--help", "--driver", "codex"], { 1130 + const code = await runCliForTest(["run", "--help", "--driver", "codex"], { 931 1131 cwd: "/workspace/repo", 932 1132 homeDirectory: "/Users/tester", 933 1133 pathExists: async () => false, ··· 952 1152 const stdout: Array<string> = []; 953 1153 const stderr: Array<string> = []; 954 1154 955 - const code = await runCli(["run", "--help"], { 1155 + const code = await runCliForTest(["run", "--help"], { 956 1156 cwd: "/workspace/repo", 957 1157 homeDirectory: "/Users/tester", 958 1158 pathExists: async (path) => path === "/Users/tester/.mill/config.ts", ··· 1032 1232 const jsonStdout: Array<string> = []; 1033 1233 const jsonStderr: Array<string> = []; 1034 1234 1035 - const jsonCode = await runCli( 1235 + const jsonCode = await runCliForTest( 1036 1236 ["wait", runId, "--timeout", "1", "--json", "--runs-dir", runsDirectory], 1037 1237 { 1038 1238 cwd: tempDirectory, ··· 1058 1258 const humanStdout: Array<string> = []; 1059 1259 const humanStderr: Array<string> = []; 1060 1260 1061 - const humanCode = await runCli( 1261 + const humanCode = await runCliForTest( 1062 1262 ["wait", runId, "--timeout", "1", "--runs-dir", runsDirectory], 1063 1263 { 1064 1264 cwd: tempDirectory,
+180 -49
packages/cli/src/public/index.api.ts
··· 36 36 readonly loadConfigModule?: (path: string) => Promise<unknown>; 37 37 readonly launchWorker?: (input: LaunchWorkerInput) => Promise<void>; 38 38 readonly io?: CliIo; 39 + readonly env?: Readonly<Record<string, string | undefined>>; 39 40 } 40 41 41 42 interface CliExit { ··· 102 103 }); 103 104 104 105 const defaultConfig = defineConfig({ 105 - defaultDriver: "pi", 106 + defaultDriver: "", 106 107 defaultExecutor: "direct", 107 - defaultModel: "openai-codex/gpt-5.3-codex", 108 108 maxRunDepth: 1, 109 109 drivers: { 110 110 pi: processDriver(createPiDriverRegistration()), ··· 238 238 239 239 const toCliEffect = (program: Promise<number>) => 240 240 Effect.flatMap( 241 - Effect.promise(() => program), 241 + Effect.tryPromise({ 242 + try: () => program, 243 + catch: (error) => error, 244 + }), 242 245 (code) => 243 246 code === 0 244 247 ? Effect.void ··· 256 259 return String(error); 257 260 }; 258 261 262 + type ActiveDriverSource = "flag" | "config" | "harness"; 263 + 264 + interface ActiveDriverResolution { 265 + readonly name: string; 266 + readonly source: ActiveDriverSource; 267 + readonly resolvedConfig: Awaited<ReturnType<typeof resolveConfig>>; 268 + } 269 + 270 + const normalizeNonEmptyText = (value: string | undefined): string | undefined => { 271 + if (value === undefined) { 272 + return undefined; 273 + } 274 + 275 + const trimmed = value.trim(); 276 + return trimmed.length > 0 ? trimmed : undefined; 277 + }; 278 + 279 + const inferHarnessDriver = (env: Readonly<Record<string, string | undefined>>): string | undefined => { 280 + if (env.CLAUDECODE === "1") { 281 + return "claude"; 282 + } 283 + 284 + if ( 285 + normalizeNonEmptyText(env.CODEX_THREAD_ID) !== undefined || 286 + normalizeNonEmptyText(env.CODEX_SANDBOX) !== undefined || 287 + normalizeNonEmptyText(env.CODEX_SANDBOX_NETWORK_DISABLED) !== undefined 288 + ) { 289 + return "codex"; 290 + } 291 + 292 + return undefined; 293 + }; 294 + 295 + const sourceLabel = (source: ActiveDriverSource): string => { 296 + if (source === "flag") { 297 + return "--driver"; 298 + } 299 + 300 + if (source === "config") { 301 + return "mill.config.ts defaultDriver"; 302 + } 303 + 304 + return "harness inference"; 305 + }; 306 + 307 + const resolveActiveDriverSelection = ( 308 + requestedDriverName: string | undefined, 309 + resolvedConfig: Awaited<ReturnType<typeof resolveConfig>>, 310 + env: Readonly<Record<string, string | undefined>>, 311 + ): Omit<ActiveDriverResolution, "resolvedConfig"> => { 312 + const requested = normalizeNonEmptyText(requestedDriverName); 313 + const configured = normalizeNonEmptyText(resolvedConfig.config.defaultDriver); 314 + const inferred = inferHarnessDriver(env); 315 + 316 + const source: ActiveDriverSource | undefined = 317 + requested !== undefined ? "flag" : configured !== undefined ? "config" : inferred !== undefined ? "harness" : undefined; 318 + const selected = requested ?? configured ?? inferred; 319 + const available = Object.keys(resolvedConfig.config.drivers).sort((left, right) => 320 + left.localeCompare(right), 321 + ); 322 + 323 + if (selected === undefined || source === undefined) { 324 + throw new Error( 325 + "Unable to resolve active driver. Provide --driver <name>, set defaultDriver in mill.config.ts, or run from a supported harness (CLAUDECODE=1 => claude, CODEX_THREAD_ID/CODEX_SANDBOX/CODEX_SANDBOX_NETWORK_DISABLED => codex).", 326 + ); 327 + } 328 + 329 + if (!available.includes(selected)) { 330 + const renderedAvailable = available.length > 0 ? available.join(", ") : "(none)"; 331 + 332 + throw new Error( 333 + `Resolved active driver '${selected}' from ${sourceLabel(source)} is unavailable. Available drivers: ${renderedAvailable}.`, 334 + ); 335 + } 336 + 337 + return { 338 + name: selected, 339 + source, 340 + }; 341 + }; 342 + 343 + const resolveActiveDriver = async ( 344 + options: RunCliOptions, 345 + requestedDriverName: string | undefined, 346 + ): Promise<ActiveDriverResolution> => { 347 + const resolvedConfig = await resolveConfig({ 348 + defaults: defaultConfig, 349 + cwd: options.cwd, 350 + homeDirectory: options.homeDirectory, 351 + pathExists: options.pathExists, 352 + loadConfigModule: options.loadConfigModule, 353 + }); 354 + 355 + const selection = resolveActiveDriverSelection( 356 + requestedDriverName, 357 + resolvedConfig, 358 + options.env ?? process.env, 359 + ); 360 + 361 + return { 362 + ...selection, 363 + resolvedConfig, 364 + }; 365 + }; 366 + 259 367 interface RunCommandInput { 260 368 readonly program: string; 261 369 readonly json: boolean; ··· 283 391 } 284 392 } 285 393 394 + const activeDriver = await resolveActiveDriver(options, fromOption(command.driver)); 395 + 286 396 const runInput = { 287 397 defaults: defaultConfig, 288 398 programPath: command.program, 289 399 cwd: options.cwd, 290 400 homeDirectory: options.homeDirectory, 291 401 runsDirectory: fromOption(command.runsDir) ?? options.runsDirectory, 292 - driverName: fromOption(command.driver), 402 + driverName: activeDriver.name, 293 403 executorName: fromOption(command.executor), 294 404 pathExists: options.pathExists, 295 405 loadConfigModule: options.loadConfigModule, ··· 340 450 options: RunCliOptions, 341 451 io: CliIo, 342 452 ): Promise<number> => { 453 + const activeDriver = await resolveActiveDriver(options, fromOption(command.driver)); 454 + 343 455 const output = await runWorker({ 344 456 defaults: defaultConfig, 345 457 runId: command.runId, ··· 347 459 cwd: options.cwd, 348 460 homeDirectory: options.homeDirectory, 349 461 runsDirectory: fromOption(command.runsDir) ?? options.runsDirectory, 350 - driverName: fromOption(command.driver), 462 + driverName: activeDriver.name, 351 463 executorName: fromOption(command.executor), 352 464 pathExists: options.pathExists, 353 465 loadConfigModule: options.loadConfigModule, ··· 362 474 363 475 const INIT_CONFIG_TEMPLATE = [ 364 476 "export default {", 365 - " // Optional: override model/driver/executor defaults.", 366 - ' // defaultModel: "openai-codex/gpt-5.3-codex",', 477 + " // Optional: override driver/executor defaults.", 367 478 " // maxRunDepth: 1, // recursion guard for nested `mill run`", 368 479 " authoring: {", 369 480 ' instructions: "Use systemPrompt for WHO (role/method), prompt for WHAT (explicit task + scope + validation). Prefer codex for synthesis, cerebras for fast retrieval.",', ··· 417 528 options: RunCliOptions, 418 529 io: CliIo, 419 530 ): Promise<number> => { 531 + const activeDriver = await resolveActiveDriver(options, fromOption(command.driver)); 532 + 420 533 const output = await getRunStatus({ 421 534 defaults: defaultConfig, 422 535 runId: command.runId, 423 536 cwd: options.cwd, 424 537 homeDirectory: options.homeDirectory, 425 538 runsDirectory: fromOption(command.runsDir) ?? options.runsDirectory, 426 - driverName: fromOption(command.driver), 539 + driverName: activeDriver.name, 427 540 pathExists: options.pathExists, 428 541 loadConfigModule: options.loadConfigModule, 429 542 }); ··· 456 569 } 457 570 458 571 const timeoutSeconds = command.timeout; 572 + const activeDriver = await resolveActiveDriver(options, fromOption(command.driver)); 459 573 460 574 const [waitResult] = await Promise.allSettled([ 461 575 waitForRun({ ··· 465 579 cwd: options.cwd, 466 580 homeDirectory: options.homeDirectory, 467 581 runsDirectory: fromOption(command.runsDir) ?? options.runsDirectory, 468 - driverName: fromOption(command.driver), 582 + driverName: activeDriver.name, 469 583 pathExists: options.pathExists, 470 584 loadConfigModule: options.loadConfigModule, 471 585 }), ··· 551 665 options: RunCliOptions, 552 666 io: CliIo, 553 667 ): Promise<number> => { 668 + const activeDriver = await resolveActiveDriver(options, fromOption(command.driver)); 669 + 554 670 await watchRun({ 555 671 defaults: defaultConfig, 556 672 runId: fromOption(command.run), ··· 561 677 cwd: options.cwd, 562 678 homeDirectory: options.homeDirectory, 563 679 runsDirectory: fromOption(command.runsDir) ?? options.runsDirectory, 564 - driverName: fromOption(command.driver), 680 + driverName: activeDriver.name, 565 681 pathExists: options.pathExists, 566 682 loadConfigModule: options.loadConfigModule, 567 683 onEvent: (line) => { ··· 584 700 options: RunCliOptions, 585 701 io: CliIo, 586 702 ): Promise<number> => { 703 + const activeDriver = await resolveActiveDriver(options, fromOption(command.driver)); 704 + 587 705 const cancelled = await cancelRun({ 588 706 defaults: defaultConfig, 589 707 runId: command.runId, 590 708 cwd: options.cwd, 591 709 homeDirectory: options.homeDirectory, 592 710 runsDirectory: fromOption(command.runsDir) ?? options.runsDirectory, 593 - driverName: fromOption(command.driver), 711 + driverName: activeDriver.name, 594 712 pathExists: options.pathExists, 595 713 loadConfigModule: options.loadConfigModule, 596 714 }); ··· 619 737 options: RunCliOptions, 620 738 io: CliIo, 621 739 ): Promise<number> => { 740 + const activeDriver = await resolveActiveDriver(options, fromOption(command.driver)); 741 + 622 742 const runs = await listRuns({ 623 743 defaults: defaultConfig, 624 744 status: fromOption(command.status), 625 745 cwd: options.cwd, 626 746 homeDirectory: options.homeDirectory, 627 747 runsDirectory: fromOption(command.runsDir) ?? options.runsDirectory, 628 - driverName: fromOption(command.driver), 748 + driverName: activeDriver.name, 629 749 pathExists: options.pathExists, 630 750 loadConfigModule: options.loadConfigModule, 631 751 }); ··· 771 891 772 892 type ResolvedModelCatalogHelp = 773 893 | { readonly source: "resolved"; readonly entries: ReadonlyArray<DriverModelCatalogEntry> } 774 - | { readonly source: "unavailable" }; 894 + | { readonly source: "unavailable"; readonly message: string }; 775 895 776 896 interface ResolvedHelpContext { 777 897 readonly authoring: ResolvedAuthoringHelp; ··· 785 905 786 906 const renderModelCatalogHelp = (modelCatalog: ResolvedModelCatalogHelp): string => { 787 907 if (modelCatalog.source === "unavailable") { 788 - return "Models:\n (unavailable: failed to resolve config or driver catalogs)"; 908 + return `Models:\n (unavailable: ${modelCatalog.message})`; 789 909 } 790 910 791 911 if (modelCatalog.entries.length === 0) { ··· 829 949 agent: "scout", 830 950 systemPrompt: "You are a code risk analyst.", 831 951 prompt: "Review src/auth and summarize top security risks.", 952 + model: "openai-codex/gpt-5.3-codex", 832 953 }); 833 954 const plan = await mill.spawn({ 834 955 agent: "planner", 835 956 systemPrompt: "You turn findings into an execution-ready plan.", 836 957 prompt: \`Create remediation steps from:\\n\\n\${scan.text}\`, 958 + model: "anthropic/claude-sonnet-4-6", 837 959 }); 838 960 839 961 Parallel fan-out: 840 962 const [security, perf] = await Promise.all([ 841 - mill.spawn({ agent: "security", systemPrompt: "...", prompt: "Review src/auth/" }), 842 - mill.spawn({ agent: "perf", systemPrompt: "...", prompt: "Profile src/api/" }), 963 + mill.spawn({ agent: "security", systemPrompt: "...", prompt: "Review src/auth/", model: "anthropic/claude-sonnet-4-6" }), 964 + mill.spawn({ agent: "perf", systemPrompt: "...", prompt: "Profile src/api/", model: "cerebras/zai-glm-4.7" }), 843 965 ]); 844 966 845 967 ${renderAuthoringHelp(helpContext.authoring)} ··· 902 1024 options: RunCliOptions, 903 1025 selectedDriverName?: string, 904 1026 ): Promise<ResolvedHelpContext> => { 1027 + let authoring: ResolvedAuthoringHelp = { 1028 + source: "static", 1029 + }; 1030 + 905 1031 try { 906 1032 const resolvedConfig = await resolveConfig({ 907 1033 defaults: defaultConfig, ··· 915 1041 const hasAuthoringOverride = 916 1042 resolvedConfig.source !== "defaults" && instructions !== defaultConfig.authoring.instructions; 917 1043 918 - const driverEntriesUnsorted = await Runtime.runPromise(runtime)( 919 - Effect.forEach(Object.entries(resolvedConfig.config.drivers), ([driverName, registration]) => 920 - Effect.map(registration.codec.modelCatalog, (models) => ({ 921 - driverName, 922 - modelFormat: registration.modelFormat, 923 - models: Array.from(new Set(models)), 924 - })), 925 - ), 926 - ); 1044 + authoring = hasAuthoringOverride 1045 + ? { 1046 + source: "config", 1047 + instructions, 1048 + } 1049 + : { 1050 + source: "static", 1051 + }; 927 1052 928 - const driverEntries = [...driverEntriesUnsorted].sort((left, right) => 929 - left.driverName.localeCompare(right.driverName), 1053 + const activeDriver = resolveActiveDriverSelection( 1054 + selectedDriverName, 1055 + resolvedConfig, 1056 + options.env ?? process.env, 930 1057 ); 1058 + const registration = resolvedConfig.config.drivers[activeDriver.name]; 931 1059 932 - const preferredDriver = selectedDriverName ?? resolvedConfig.config.defaultDriver; 933 - const selectedDriverEntry = driverEntries.find((entry) => entry.driverName === preferredDriver); 1060 + if (registration === undefined) { 1061 + throw new Error( 1062 + `Resolved active driver '${activeDriver.name}' from ${sourceLabel(activeDriver.source)} is unavailable.`, 1063 + ); 1064 + } 1065 + 1066 + const models = await Runtime.runPromise(runtime)( 1067 + Effect.map(registration.codec.modelCatalog, (catalog) => Array.from(new Set(catalog))), 1068 + ); 934 1069 935 1070 return { 936 - authoring: hasAuthoringOverride 937 - ? { 938 - source: "config", 939 - instructions, 940 - } 941 - : { 942 - source: "static", 1071 + authoring, 1072 + modelCatalog: { 1073 + source: "resolved", 1074 + entries: [ 1075 + { 1076 + driverName: activeDriver.name, 1077 + modelFormat: registration.modelFormat, 1078 + models, 943 1079 }, 1080 + ], 1081 + }, 1082 + }; 1083 + } catch (error) { 1084 + return { 1085 + authoring, 944 1086 modelCatalog: { 945 - source: "resolved", 946 - entries: selectedDriverEntry === undefined ? driverEntries : [selectedDriverEntry], 1087 + source: "unavailable", 1088 + message: formatUnknownError(error), 947 1089 }, 948 1090 }; 949 - } catch { 950 - // fall through to static authoring + unavailable model catalogs 951 1091 } 952 - 953 - return { 954 - authoring: { 955 - source: "static", 956 - }, 957 - modelCatalog: { 958 - source: "unavailable", 959 - }, 960 - }; 961 1092 }; 962 1093 963 1094 export const runCli = async (
+79 -2
packages/cli/src/public/index.e2e.test.ts
··· 105 105 }), 106 106 ); 107 107 108 + const withNeutralRunDepthEnv = (command: Command.Command): Command.Command => 109 + Command.env(command, { 110 + MILL_RUN_DEPTH: "", 111 + }); 112 + 108 113 const commandOutput = (command: Command.Command): Promise<string> => 109 - Runtime.runPromise(runtime)(Effect.provide(Command.string(command), BunContext.layer)); 114 + Runtime.runPromise(runtime)( 115 + Effect.provide(Command.string(withNeutralRunDepthEnv(command)), BunContext.layer), 116 + ); 110 117 111 118 const commandExitCode = (command: Command.Command): Promise<number> => 112 - Runtime.runPromise(runtime)(Effect.provide(Command.exitCode(command), BunContext.layer)); 119 + Runtime.runPromise(runtime)( 120 + Effect.provide(Command.exitCode(withNeutralRunDepthEnv(command)), BunContext.layer), 121 + ); 113 122 114 123 describe("mill help (e2e)", () => { 115 124 it("does not expose discovery subcommand", async () => { ··· 187 196 } 188 197 }, 20_000); 189 198 199 + it("fails when no active driver can be resolved", async () => { 200 + const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-driver-unresolved-e2e-")); 201 + const runsDirectory = join(tempDirectory, "runs"); 202 + const programPath = join(tempDirectory, "program.ts"); 203 + 204 + await writeFile(programPath, "return 'no-driver';\n", "utf-8"); 205 + 206 + try { 207 + const exitCode = await commandExitCode( 208 + Command.env( 209 + Command.make( 210 + "bun", 211 + "run", 212 + "packages/cli/src/bin/mill.ts", 213 + "run", 214 + programPath, 215 + "--sync", 216 + "--json", 217 + "--runs-dir", 218 + runsDirectory, 219 + ), 220 + { 221 + CLAUDECODE: "", 222 + CODEX_THREAD_ID: "", 223 + CODEX_SANDBOX: "", 224 + CODEX_SANDBOX_NETWORK_DISABLED: "", 225 + }, 226 + ), 227 + ); 228 + 229 + expect(exitCode).toBe(1); 230 + } finally { 231 + await rm(tempDirectory, { recursive: true, force: true }); 232 + } 233 + }); 234 + 190 235 it("submits async run by default, then status/wait observes completion", async () => { 191 236 const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-async-e2e-")); 192 237 const runsDirectory = join(tempDirectory, "runs"); ··· 210 255 "run", 211 256 programPath, 212 257 "--json", 258 + "--driver", 259 + "pi", 213 260 "--runs-dir", 214 261 runsDirectory, 215 262 ), ··· 226 273 "status", 227 274 submitPayload.runId, 228 275 "--json", 276 + "--driver", 277 + "pi", 229 278 "--runs-dir", 230 279 runsDirectory, 231 280 ), ··· 244 293 "--timeout", 245 294 "5", 246 295 "--json", 296 + "--driver", 297 + "pi", 247 298 "--runs-dir", 248 299 runsDirectory, 249 300 ), ··· 272 323 submitPayload.runId, 273 324 "--program", 274 325 join(submitPayload.paths.runDir, "program.ts"), 326 + "--driver", 327 + "pi", 275 328 "--runs-dir", 276 329 runsDirectory, 277 330 ), ··· 323 376 programPath, 324 377 "--sync", 325 378 "--json", 379 + "--driver", 380 + "pi", 326 381 "--runs-dir", 327 382 runsDirectory, 328 383 ), ··· 343 398 "status", 344 399 runPayload.run.id, 345 400 "--json", 401 + "--driver", 402 + "pi", 346 403 "--runs-dir", 347 404 runsDirectory, 348 405 ), ··· 362 419 "--timeout", 363 420 "2", 364 421 "--json", 422 + "--driver", 423 + "pi", 365 424 "--runs-dir", 366 425 runsDirectory, 367 426 ), ··· 416 475 "run", 417 476 completeProgramPath, 418 477 "--json", 478 + "--driver", 479 + "pi", 419 480 "--runs-dir", 420 481 runsDirectory, 421 482 ), ··· 429 490 "run", 430 491 cancelProgramPath, 431 492 "--json", 493 + "--driver", 494 + "pi", 432 495 "--runs-dir", 433 496 runsDirectory, 434 497 ), ··· 445 508 "cancel", 446 509 cancelRun.runId, 447 510 "--json", 511 + "--driver", 512 + "pi", 448 513 "--runs-dir", 449 514 runsDirectory, 450 515 ), ··· 464 529 "--timeout", 465 530 "8", 466 531 "--json", 532 + "--driver", 533 + "pi", 467 534 "--runs-dir", 468 535 runsDirectory, 469 536 ), ··· 482 549 "--timeout", 483 550 "8", 484 551 "--json", 552 + "--driver", 553 + "pi", 485 554 "--runs-dir", 486 555 runsDirectory, 487 556 ), ··· 501 570 "--channel", 502 571 "all", 503 572 "--json", 573 + "--driver", 574 + "pi", 504 575 "--runs-dir", 505 576 runsDirectory, 506 577 ), ··· 533 604 "--channel", 534 605 "events", 535 606 "--json", 607 + "--driver", 608 + "pi", 536 609 "--runs-dir", 537 610 runsDirectory, 538 611 ), ··· 557 630 "packages/cli/src/bin/mill.ts", 558 631 "ls", 559 632 "--json", 633 + "--driver", 634 + "pi", 560 635 "--runs-dir", 561 636 runsDirectory, 562 637 ), ··· 632 707 "--timeout", 633 708 "1", 634 709 "--json", 710 + "--driver", 711 + "pi", 635 712 "--runs-dir", 636 713 runsDirectory, 637 714 ),