programmatic subagents
0
fork

Configure Feed

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

feat(cli): global/local init skeleton and authoring instructions in help

+217 -43
+1 -1
packages/cli/package.json
··· 1 1 { 2 2 "name": "@mill/cli", 3 - "version": "0.1.2", 3 + "version": "0.1.3", 4 4 "bin": { 5 5 "mill": "./src/bin/mill.ts" 6 6 },
+101 -4
packages/cli/src/public/index.api.test.ts
··· 873 873 } 874 874 }); 875 875 876 - it("creates init skeleton in cwd", async () => { 876 + it("creates minimal init skeleton in cwd", async () => { 877 877 const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-init-")); 878 878 const stdout: Array<string> = []; 879 879 const stderr: Array<string> = []; ··· 897 897 expect(stdout[0]).toContain("mill.config.ts"); 898 898 899 899 const configSource = await readFile(join(tempDirectory, "mill.config.ts"), "utf-8"); 900 - expect(configSource).toContain("defineConfig"); 901 - expect(configSource).toContain("defaultExecutor"); 902 - expect(configSource).toContain("executors"); 900 + expect(configSource).toContain("export default {"); 901 + expect(configSource).toContain("authoring"); 902 + expect(configSource).not.toContain("defineConfig"); 903 + } finally { 904 + await rm(tempDirectory, { recursive: true, force: true }); 905 + } 906 + }); 907 + 908 + it("creates minimal global init skeleton in ~/.mill/config.ts", async () => { 909 + const tempDirectory = await mkdtemp(join(tmpdir(), "mill-cli-init-global-")); 910 + const homeDirectory = join(tempDirectory, "home"); 911 + const stdout: Array<string> = []; 912 + const stderr: Array<string> = []; 913 + 914 + try { 915 + await mkdir(homeDirectory, { recursive: true }); 916 + 917 + const code = await runCli(["init", "--global"], { 918 + cwd: tempDirectory, 919 + homeDirectory, 920 + io: { 921 + stdout: (line) => { 922 + stdout.push(line); 923 + }, 924 + stderr: (line) => { 925 + stderr.push(line); 926 + }, 927 + }, 928 + }); 929 + 930 + expect(code).toBe(0); 931 + expect(stderr).toHaveLength(0); 932 + expect(stdout[0]).toContain(join(homeDirectory, ".mill", "config.ts")); 933 + 934 + const configSource = await readFile(join(homeDirectory, ".mill", "config.ts"), "utf-8"); 935 + expect(configSource).toContain("export default {"); 936 + expect(configSource).toContain("authoring"); 937 + expect(configSource).not.toContain("defineConfig"); 903 938 } finally { 904 939 await rm(tempDirectory, { recursive: true, force: true }); 905 940 } 941 + }); 942 + 943 + it("loads authoring instructions from resolved config into root help", async () => { 944 + const stdout: Array<string> = []; 945 + const stderr: Array<string> = []; 946 + 947 + const code = await runCli([], { 948 + cwd: "/workspace/repo", 949 + homeDirectory: "/Users/tester", 950 + pathExists: async (path) => path === "/Users/tester/.mill/config.ts", 951 + loadConfigModule: async () => ({ 952 + default: { 953 + authoring: { 954 + instructions: "CUSTOM_AUTHORING_INSTRUCTIONS", 955 + }, 956 + }, 957 + }), 958 + io: { 959 + stdout: (line) => { 960 + stdout.push(line); 961 + }, 962 + stderr: (line) => { 963 + stderr.push(line); 964 + }, 965 + }, 966 + }); 967 + 968 + expect(code).toBe(0); 969 + expect(stderr).toHaveLength(0); 970 + expect(stdout[0]).toContain("CUSTOM_AUTHORING_INSTRUCTIONS"); 971 + }); 972 + 973 + it("loads authoring instructions from resolved config into command help", async () => { 974 + const stdout: Array<string> = []; 975 + const stderr: Array<string> = []; 976 + 977 + const code = await runCli(["run", "--help"], { 978 + cwd: "/workspace/repo", 979 + homeDirectory: "/Users/tester", 980 + pathExists: async (path) => path === "/Users/tester/.mill/config.ts", 981 + loadConfigModule: async () => ({ 982 + default: { 983 + authoring: { 984 + instructions: "CUSTOM_AUTHORING_IN_COMMAND_HELP", 985 + }, 986 + }, 987 + }), 988 + io: { 989 + stdout: (line) => { 990 + stdout.push(line); 991 + }, 992 + stderr: (line) => { 993 + stderr.push(line); 994 + }, 995 + }, 996 + }); 997 + 998 + expect(code).toBe(0); 999 + expect(stderr).toHaveLength(0); 1000 + expect(stdout.join("\n")).toContain( 1001 + "Authoring (from config): CUSTOM_AUTHORING_IN_COMMAND_HELP", 1002 + ); 906 1003 }); 907 1004 908 1005 it("wait timeout is deterministic with typed JSON error contract", async () => {
+115 -38
packages/cli/src/public/index.api.ts
··· 12 12 inspectRun, 13 13 listRuns, 14 14 processDriver, 15 + resolveConfig, 15 16 runProgramSync, 16 17 runWorker, 17 18 submitRun, ··· 96 97 97 98 const joinPath = (base: string, child: string): string => 98 99 normalizePath(base) === "/" ? `/${child}` : `${normalizePath(base)}/${child}`; 100 + 101 + const dirname = (path: string): string => { 102 + const normalized = normalizePath(path); 103 + 104 + if (normalized === "/") { 105 + return "/"; 106 + } 107 + 108 + const index = normalized.lastIndexOf("/"); 109 + 110 + if (index <= 0) { 111 + return "/"; 112 + } 113 + 114 + return normalized.slice(0, index); 115 + }; 99 116 100 117 const workerPidPath = (runsDirectory: string, runId: string): string => 101 118 joinPath(joinPath(runsDirectory, runId), "worker.pid"); ··· 287 304 }; 288 305 289 306 const INIT_CONFIG_TEMPLATE = [ 290 - 'import { defineConfig, processDriver } from "@mill/core";', 291 - 'import { createPiDriverRegistration } from "@mill/driver-pi";', 292 - 'import { createClaudeDriverRegistration } from "@mill/driver-claude";', 293 - 'import { createCodexDriverRegistration } from "@mill/driver-codex";', 294 - "", 295 - "export default defineConfig({", 296 - ' defaultDriver: "pi",', 297 - ' defaultExecutor: "direct",', 298 - ' defaultModel: "openai-codex/gpt-5.3-codex",', 299 - " drivers: {", 300 - " pi: processDriver(createPiDriverRegistration()),", 301 - " claude: processDriver(createClaudeDriverRegistration()),", 302 - " codex: processDriver(createCodexDriverRegistration()),", 303 - " },", 304 - " executors: {", 305 - " direct: {", 306 - ' description: "Local direct executor",', 307 - " runtime: {", 308 - ' name: "direct",', 309 - " runProgram: ({ execute }) => execute,", 310 - " },", 311 - " },", 312 - " // Future: add sandboxed executors here.", 313 - " },", 314 - " extensions: [],", 307 + "export default {", 308 + " // Optional: override model/driver/executor defaults.", 309 + ' // defaultModel: "openai-codex/gpt-5.3-codex",', 315 310 " authoring: {", 316 - ' instructions: "Use systemPrompt for WHO and prompt for WHAT.",', 311 + ' instructions: "Use systemPrompt for WHO (role/method), prompt for WHAT (explicit task + scope + validation). Prefer codex for synthesis, cerebras for fast retrieval.",', 317 312 " },", 318 - "});", 313 + "};", 319 314 ].join("\n"); 320 315 321 - const initCommand = async (options: RunCliOptions, io: CliIo): Promise<number> => { 316 + interface InitCommandInput { 317 + readonly global: boolean; 318 + } 319 + 320 + const initCommand = async ( 321 + command: InitCommandInput, 322 + options: RunCliOptions, 323 + io: CliIo, 324 + ): Promise<number> => { 322 325 const cwd = options.cwd ?? process.cwd(); 323 - const configPath = `${cwd}/mill.config.ts`; 326 + const homeDirectory = options.homeDirectory ?? process.env.HOME; 327 + 328 + if (command.global && (homeDirectory === undefined || homeDirectory.length === 0)) { 329 + io.stderr("Unable to resolve home directory for --global init."); 330 + return 1; 331 + } 332 + 333 + const configPath = command.global 334 + ? joinPath(homeDirectory as string, ".mill/config.ts") 335 + : `${cwd}/mill.config.ts`; 324 336 325 337 await runWithBunContext( 326 338 Effect.flatMap(FileSystem.FileSystem, (fileSystem) => 327 - fileSystem.writeFileString(configPath, `${INIT_CONFIG_TEMPLATE}\n`), 339 + Effect.zipRight( 340 + fileSystem.makeDirectory(dirname(configPath), { recursive: true }), 341 + fileSystem.writeFileString(configPath, `${INIT_CONFIG_TEMPLATE}\n`), 342 + ), 328 343 ), 329 344 ); 330 345 ··· 718 733 (command) => toCliEffect(lsCommand(command, options, io)), 719 734 ).pipe(CliCommand.withDescription("List runs.")); 720 735 721 - const init = CliCommand.make("init", {}, () => toCliEffect(initCommand(options, io))).pipe( 722 - CliCommand.withDescription("Create a starter mill.config.ts."), 736 + const init = CliCommand.make( 737 + "init", 738 + { 739 + global: Options.boolean("global"), 740 + }, 741 + (command) => toCliEffect(initCommand(command, options, io)), 742 + ).pipe( 743 + CliCommand.withDescription( 744 + "Create a starter config (local mill.config.ts or ~/.mill/config.ts with --global).", 745 + ), 723 746 ); 724 747 725 748 const discovery = CliCommand.make( ··· 747 770 ); 748 771 }; 749 772 750 - const HELP_TEXT = `mill - orchestration runtime for AI agents 773 + const buildHelpText = ( 774 + authoringInstructions: string, 775 + ): string => `mill - orchestration runtime for AI agents 751 776 752 777 Usage: mill <command> [options] 753 778 ··· 759 784 inspect <ref> Inspect run or spawn detail 760 785 cancel <runId> Cancel a running execution 761 786 ls List runs 762 - init Create starter mill.config.ts 787 + init [--global] Create starter config (local or ~/.mill/config.ts) 763 788 discovery Emit discovery metadata 764 789 765 790 Global options: --json, --driver <name>, --runs-dir <path> ··· 787 812 Authoring: 788 813 systemPrompt = WHO the agent is (personality, methodology, output format) 789 814 prompt = WHAT to do now (specific files, concrete task) 790 - Prefer cheaper models for search, stronger models for synthesis. 815 + From config: ${authoringInstructions} 791 816 792 817 Run mill <command> --help for details.`; 793 818 819 + const HELP_FLAGS = new Set(["--help", "-h"]); 820 + 821 + const COMMAND_NAMES = new Set([ 822 + "run", 823 + "status", 824 + "wait", 825 + "watch", 826 + "inspect", 827 + "cancel", 828 + "ls", 829 + "init", 830 + "discovery", 831 + "_worker", 832 + ]); 833 + 794 834 const isHelpRequest = (argv: ReadonlyArray<string>): boolean => { 795 835 if (argv.length === 0) return true; 796 836 797 - return argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h"); 837 + return argv.length === 1 && HELP_FLAGS.has(argv[0] ?? ""); 838 + }; 839 + 840 + const isCommandHelpRequest = (argv: ReadonlyArray<string>): boolean => { 841 + const commandName = argv[0]; 842 + 843 + if (commandName === undefined || !COMMAND_NAMES.has(commandName)) { 844 + return false; 845 + } 846 + 847 + return argv.slice(1).some((argument) => HELP_FLAGS.has(argument)); 848 + }; 849 + 850 + const resolveAuthoringInstructionsForHelp = async (options: RunCliOptions): Promise<string> => { 851 + try { 852 + const resolvedConfig = await resolveConfig({ 853 + defaults: defaultConfig, 854 + cwd: options.cwd, 855 + homeDirectory: options.homeDirectory, 856 + pathExists: options.pathExists, 857 + loadConfigModule: options.loadConfigModule, 858 + }); 859 + 860 + return resolvedConfig.config.authoring.instructions; 861 + } catch { 862 + return defaultConfig.authoring.instructions; 863 + } 798 864 }; 799 865 800 866 export const runCli = async ( ··· 805 871 const io = resolvedOptions.io ?? defaultIo; 806 872 807 873 if (isHelpRequest(argv)) { 808 - io.stdout(HELP_TEXT); 874 + const authoringInstructions = await resolveAuthoringInstructionsForHelp(resolvedOptions); 875 + io.stdout(buildHelpText(authoringInstructions)); 809 876 return 0; 810 877 } 811 878 879 + const commandHelpRequest = isCommandHelpRequest(argv); 880 + const authoringInstructions = commandHelpRequest 881 + ? await resolveAuthoringInstructionsForHelp(resolvedOptions) 882 + : undefined; 883 + 812 884 const command = createCli(resolvedOptions, io); 813 885 const run = CliCommand.run(command, { 814 886 name: "mill", ··· 829 901 ); 830 902 831 903 const compactHelp = CliConfig.layer({ showBuiltIns: false, showTypes: false }); 904 + const exitCode = await runWithBunContext(Effect.provide(codeEffect, compactHelp)); 832 905 833 - return runWithBunContext(Effect.provide(codeEffect, compactHelp)); 906 + if (commandHelpRequest && exitCode === 0 && authoringInstructions !== undefined) { 907 + io.stdout(`Authoring (from config): ${authoringInstructions}`); 908 + } 909 + 910 + return exitCode; 834 911 };