···8686| `@mill/driver-pi` | Process driver for pi agent |
8787| `@mill/driver-claude` | Driver for Claude |
8888| `@mill/driver-codex` | Driver for Codex |
8989-| `@mill/pi-mill` | Pi extension integrating mill as execution backend |
8989+| `pi-mill` | Pi extension integrating mill as execution backend |
90909191## Architecture
9292
···11-# @mill/pi-mill
11+# pi-mill
2233-A pi extension that provides the same `subagent` tool + TUI monitor workflow as your existing setup, but executes each child task through **mill** (`mill run --sync --json`) instead of spawning `pi` directly.
33+A [pi](https://pi.dev) extension that adds a `subagent` tool, letting your AI coding agent spawn and orchestrate child agents through [mill](https://github.com/laulauland/mill).
4455-## What stays the same
55+When the orchestrating agent needs to delegate work — run tasks in parallel, assign specialized roles, or break a problem into sub-tasks — it writes a short TypeScript program using `mill.spawn()`. Each spawn becomes a mill run, which means driver selection, model routing, and session management all come from your mill config rather than being hardcoded.
6677-- `subagent` tool contract (`task` + `code`)
88-- Program-mode orchestration with `factory.spawn(...)`
99-- Async return (immediate run id, completion notification)
1010-- `/mill` overlay monitor
1111-- `pi --mill` standalone monitor
1212-- Status widget + batched completion notifications
1313-- Bundled skills (`mill-basics`, `mill-ralph-loop`, `mill-worktree`)
77+## Install
1481515-## What changed
99+From npm:
16101717-- Child execution is now delegated to `mill`.
1818-- Each `factory.spawn(...)` compiles to a tiny temporary mill program with one `mill.spawn(...)` call.
1919-- Driver/executor/model behavior comes from your mill defaults and config resolution.
1111+```bash
1212+pi install npm:pi-mill
1313+```
20142121-## Install as a pi package
1515+From a local checkout:
22162317```bash
2424-pi install /absolute/path/to/mill/packages/pi-mill
1818+pi install /path/to/mill/packages/pi-mill
1919+```
2020+2121+## Prerequisites
2222+2323+1. `mill` must be on your `PATH` (or set a custom command in config).
2424+2. A `mill.config.ts` with at least one driver/executor configured.
2525+2626+## How it works
2727+2828+The extension registers a `subagent` tool that accepts two parameters: a `task` label and a `code` string containing TypeScript.
2929+3030+The code runs with a `mill` global (similar to `process` or `console`). The core method is `mill.spawn()`:
3131+3232+```ts
3333+// Sequential — one agent after another
3434+const analysis = await mill.spawn({
3535+ agent: "analyzer",
3636+ systemPrompt: "You analyze codebases for architectural patterns.",
3737+ prompt: "Analyze the auth module in src/auth/",
3838+ model: "anthropic/claude-sonnet-4-6",
3939+});
4040+4141+const fix = await mill.spawn({
4242+ agent: "fixer",
4343+ systemPrompt: "You fix code issues.",
4444+ prompt: `Fix the issues found: ${analysis.text}`,
4545+ model: "openai-codex/gpt-5.3-codex",
4646+});
4747+4848+// Parallel — multiple agents at once
4949+const [tests, docs] = await Promise.all([
5050+ mill.spawn({
5151+ agent: "test-writer",
5252+ systemPrompt: "You write tests.",
5353+ prompt: "Write tests for src/auth/",
5454+ model: "anthropic/claude-sonnet-4-6",
5555+ }),
5656+ mill.spawn({
5757+ agent: "documenter",
5858+ systemPrompt: "You write documentation.",
5959+ prompt: "Document the auth module.",
6060+ model: "cerebras/zai-glm-4.7",
6161+ }),
6262+]);
2563```
26642727-(or add as a local package in your pi settings).
6565+Each `mill.spawn()` submits an async mill run (`mill run --json`) and then follows completion via mill APIs (`watch` + `inspect`). Model selection, driver routing, and execution behavior all come from your mill configuration.
6666+6767+Runs are **async by default** — the tool returns a `runId` immediately and delivers results via notification when complete.
6868+6969+## Monitoring
28702929-## Mill prerequisites
7171+- `/mill` — opens an overlay inside pi showing all active and completed runs
7272+- `pi --mill` — standalone full-screen monitor for watching runs from a separate terminal
7373+- A status widget shows run progress inline during conversations
30743131-1. `mill` must be on your `PATH` (or configure a custom command below).
3232-2. Configure your global/project `mill.config.ts` with real drivers/executors as needed.
7575+Cancelling runs works via either monitor (mapped to `mill cancel`).
33763434-## Extension config
7777+## Configuration
35783636-Edit `index.ts`:
7979+Edit the `config` export in `index.ts`:
37803881```ts
3982export const config = {
···4184 millCommand: "mill",
4285 millArgs: [],
4386 millRunsDir: undefined,
4444- prompt: "...optional extra guidance for the tool description...",
8787+ prompt: "...",
4588};
4689```
47904848-- `maxDepth`: subagent nesting limit (`PI_FACTORY_DEPTH` guard)
4949-- `millCommand`: executable name/path for mill
5050-- `millArgs`: extra args prepended to every mill invocation
5151-- `millRunsDir`: optional override for `--runs-dir`
5252-- `prompt`: additional model/tool guidance appended to tool description
9191+| Option | Description |
9292+|---|---|
9393+| `maxDepth` | Subagent nesting limit. `1` = agents can spawn subagents, but those subagents cannot spawn their own. `0` = disabled. |
9494+| `millCommand` | Executable name or path for mill. |
9595+| `millArgs` | Extra args prepended to every mill invocation. |
9696+| `millRunsDir` | Override for `--runs-dir`. |
9797+| `prompt` | Additional guidance appended to the tool description (model selection hints, project conventions, etc). |
53985454-## Notes
9999+## Context flow
551005656-- Cancelling via `/mill` or `pi --mill` still works (PID-based).
5757-- `ExecutionResult.sessionPath` now contains mill driver `sessionRef` when available.
5858-- This package intentionally keeps the old UX while switching execution backend to mill.
101101+Each subagent receives the parent session path and can use `search_thread` to explore the orchestrator's conversation for context. Results (including each subagent's `sessionPath`) flow back to the orchestrator via `result.text`.
+1-1
packages/pi-mill/contract.ts
···55 task: Type.String({ description: "Label/description for this program run." }),
66 code: Type.String({
77 description:
88- "TypeScript script using the `factory` global. Use factory.spawn() to orchestrate agents. The script runs as a top-level module — use await and Promise.all directly.",
88+ "TypeScript script using the `mill` global. Use mill.spawn() to orchestrate agents. The script runs as a top-level module — use await and Promise.all directly.",
99 }),
1010});
1111
+15-15
packages/pi-mill/executors/program-executor.ts
···44import { FactoryError, toErrorDetails } from "../errors.js";
55import type { ObservabilityStore } from "../observability.js";
66import {
77- createFactory,
77+ createMillRuntime,
88 patchConsole,
99 patchPromiseAll,
1010 prepareProgramModule,
···197197 });
198198 };
199199200200- let runtime: ReturnType<typeof createFactory> | null = null;
200200+ let runtime: ReturnType<typeof createMillRuntime> | null = null;
201201 try {
202202 // Write program source as early as possible so failed preflight/confirmation runs
203203 // still keep a legible copy of the attempted program.
···227227 emit("running");
228228 obs.push(runId, "info", "program:start", { codeBytes: code.length });
229229230230- runtime = createFactory(ctx, runId, obs, {
230230+ runtime = createMillRuntime(ctx, runId, obs, {
231231 defaultSignal: input.signal,
232232 onTaskUpdate: (result) => {
233233 resultsByTask.set(result.taskId, result);
···244244 const restorePromise = patchPromiseAll(obs, runId);
245245 const restoreConsole = patchConsole(obs, runId);
246246247247- // Inject the factory global
248248- const prev = (globalThis as any).factory;
249249- (globalThis as any).factory = runtime;
247247+ // Inject runtime global.
248248+ const prevMill = (globalThis as any).mill;
249249+ const restoreGlobals = () => {
250250+ if (prevMill === undefined) delete (globalThis as any).mill;
251251+ else (globalThis as any).mill = prevMill;
252252+ };
253253+254254+ (globalThis as any).mill = runtime;
250255251256 let importPromise: Promise<unknown>;
252257 try {
···254259 // Prevent unhandled rejection if importPromise rejects before being awaited
255260 importPromise.catch(() => {});
256261 } catch (e) {
257257- // Restore immediately on synchronous throw
258258- if (prev === undefined) delete (globalThis as any).factory;
259259- else (globalThis as any).factory = prev;
262262+ restoreGlobals();
260263 restorePromise();
261264 restoreConsole();
262265 throw e;
···264267265268 if (input.signal) {
266269 if (input.signal.aborted) {
267267- if (prev === undefined) delete (globalThis as any).factory;
268268- else (globalThis as any).factory = prev;
270270+ restoreGlobals();
269271 restorePromise();
270272 restoreConsole();
271273 throw new FactoryError({
···284286 await Promise.race([importPromise, cancelled]);
285287 } finally {
286288 if (onAbort) input.signal?.removeEventListener("abort", onAbort);
287287- if (prev === undefined) delete (globalThis as any).factory;
288288- else (globalThis as any).factory = prev;
289289+ restoreGlobals();
289290 restorePromise();
290291 restoreConsole();
291292 }
···293294 try {
294295 await importPromise;
295296 } finally {
296296- if (prev === undefined) delete (globalThis as any).factory;
297297- else (globalThis as any).factory = prev;
297297+ restoreGlobals();
298298 restorePromise();
299299 restoreConsole();
300300 }
+63-19
packages/pi-mill/index.ts
···2424 runId: summary.runId,
2525 status: summary.status,
2626 task: (summary.metadata as any)?.task,
2727+ mill: {
2828+ command: (summary.metadata as any)?.millCommand,
2929+ args: (summary.metadata as any)?.millArgs,
3030+ runsDir: (summary.metadata as any)?.millRunsDir,
3131+ },
2732 startedAt: summary.observability?.startedAt,
2833 completedAt: summary.observability?.endedAt ?? Date.now(),
2934 results: summary.results.map((r) => ({
···4247}
43484449/** Write a partial run.json so external monitors (pi --mill) can see active runs. */
4545-function writeRunningMarker(runId: string, task: string, artifactsDir: string): void {
5050+function writeRunningMarker(
5151+ runId: string,
5252+ task: string,
5353+ artifactsDir: string,
5454+ millConfig: { command: string; args: string[]; runsDir?: string },
5555+): void {
4656 try {
4757 const data = {
4858 runId,
4959 status: "running",
5060 task,
6161+ mill: {
6262+ command: millConfig.command,
6363+ args: millConfig.args,
6464+ runsDir: millConfig.runsDir,
6565+ },
5166 startedAt: Date.now(),
5267 results: [],
5368 };
···252267function loadHistoricalRuns(ctx: ExtensionContext, registry: RunRegistry): void {
253268 const sessionDir = ctx.sessionManager.getSessionDir();
254269 if (!sessionDir) return;
255255- const factoryDir = path.join(sessionDir, ".factory");
256256- if (!fs.existsSync(factoryDir)) return;
270270+ const millDir = path.join(sessionDir, ".mill");
271271+ if (!fs.existsSync(millDir)) return;
257272 try {
258258- for (const entry of fs.readdirSync(factoryDir)) {
259259- const runJsonPath = path.join(factoryDir, entry, "run.json");
273273+ for (const entry of fs.readdirSync(millDir)) {
274274+ const runJsonPath = path.join(millDir, entry, "run.json");
260275 if (!fs.existsSync(runJsonPath)) continue;
261276 try {
262277 const data = JSON.parse(fs.readFileSync(runJsonPath, "utf-8"));
278278+ const artifactsDir = path.join(millDir, entry);
263279 registry.loadHistorical({
264280 runId: data.runId,
265281 status: data.status ?? "done",
···268284 status: data.status ?? "done",
269285 results: data.results ?? [],
270286 error: data.error,
271271- metadata: { task: data.task },
287287+ metadata: {
288288+ task: data.task,
289289+ millCommand: data.mill?.command,
290290+ millArgs: data.mill?.args,
291291+ millRunsDir: data.mill?.runsDir,
292292+ },
293293+ observability: {
294294+ status: data.status ?? "done",
295295+ events: [],
296296+ artifacts: [],
297297+ artifactsDir,
298298+ startedAt: data.startedAt ?? Date.now(),
299299+ endedAt: data.completedAt,
300300+ },
272301 },
273302 startedAt: data.startedAt ?? Date.now(),
274303 completedAt: data.completedAt,
···297326 millRunsDir: undefined,
298327 /** Extra text appended to the tool description. Use for model selection hints, project conventions, etc. */
299328 prompt:
300300- "Use openai/gpt-5.3-codex for most subagent operations, especially if they entail making changes across multiple files. If you need to search you can use faster models like cerebras/zai-glm-4.7. If you need to look at and reason over images (a screenshot is referenced) use google-gemini-cli/gemini-3-flash-preview to see the changes.",
329329+ "Use openai-codex/gpt-5.3-codex for most subagent operations, especially if they entail making changes across multiple files. If you need to search you can use faster models like cerebras/zai-glm-4.7. If you need to look at and reason over images (a screenshot is referenced) use google-gemini-cli/gemini-3-flash-preview to see the changes.",
301330};
302331303332// ────────────────────────────────────────────────────────────────────────
···393422 });
394423395424 pi.on("session_shutdown", async () => {
396396- // Don't cancel active runs — children are detached and will continue
425425+ // Don't cancel active runs on extension shutdown.
397426 stopPolling();
398427 });
399428···428457 label: "Subagent",
429458 description: [
430459 "Spawn subagents for delegated or orchestrated work.",
431431- "Execution backend: mill (mill run --sync --json). Configure drivers/executors/models via mill.config.ts.",
460460+ "Execution backend: mill async APIs (submit + watch + inspect). Configure drivers/executors/models via mill.config.ts.",
432461 `Enabled models: ${modelsText}`,
433433- "Write a TypeScript script. `factory` is a global (like `process` or `console`). Use factory.spawn() to orchestrate agents.",
434434- "factory.spawn() returns a Promise<ExecutionResult>. Use `await` for sequential, `Promise.all` for parallel.",
462462+ "Write a TypeScript script. `mill` is a global (like `process` or `console`). Use mill.spawn() to orchestrate agents.",
463463+ "mill.spawn() returns a Promise<ExecutionResult>. Use `await` for sequential, `Promise.all` for parallel.",
435464 "Each spawn needs: agent, systemPrompt, prompt, model. cwd defaults to process.cwd().",
436465 "systemPrompt defines WHO the agent is (behavior, principles, methodology). prompt defines WHAT it should do now (specific files, specific work). Don't put task details in systemPrompt.",
437466 "Context flow: each subagent gets the parent session path and can use search_thread to explore it. Each subagent's session is persisted and available via result.sessionPath. Result text is auto-populated on result.text.",
438467 "Async by default: returns immediately with a runId. Results are delivered via notification when complete. Do NOT poll or check for results — just continue with other work and the notification will arrive automatically.",
439468 "Model selection: use provider/model-id format (e.g. 'anthropic/claude-opus-4-6', 'cerebras/zai-glm-4.7'). Match model capability to task complexity. Use smaller/faster models for simple tasks, stronger models for complex reasoning. Vary your choices across the enabled models — don't default to one.",
440440- "Available types: Factory, ExecutionResult, SpawnInput, UsageStats.",
469469+ "Available types: Mill, ExecutionResult, SpawnInput, UsageStats.",
441470 ...(config.prompt ? [config.prompt] : []),
442471 ].join(" "),
443472 parameters: SubagentSchema,
···490519491520 const abort = new AbortController();
492521493493- // Don't wire the parent tool signal — subagent runs are detached and
494494- // should survive turn cancellation. Use "c" in /mill or pi --mill
495495- // to explicitly cancel a run.
522522+ // Don't wire the parent tool signal — subagent runs should survive turn
523523+ // cancellation. Use "c" in /mill or pi --mill to explicitly cancel a run.
496524497525 const promise = executeProgram({
498526 ctx,
···522550523551 // Write running marker so external monitors (pi --mill) see active runs
524552 const runArtifactsDir = observability.get(runId)?.artifactsDir;
525525- if (runArtifactsDir) writeRunningMarker(runId, params.task, runArtifactsDir);
553553+ if (runArtifactsDir) {
554554+ writeRunningMarker(runId, params.task, runArtifactsDir, {
555555+ command: config.millCommand,
556556+ args: config.millArgs,
557557+ runsDir: config.millRunsDir,
558558+ });
559559+ }
526560527561 // Wire completion: update observability, widget, and notify
528562 promise.then(
···539573 const fullSummary = {
540574 ...summary,
541575 observability: observability.toSummary(runId),
542542- metadata: { task: params.task },
576576+ metadata: {
577577+ task: params.task,
578578+ millCommand: config.millCommand,
579579+ millArgs: config.millArgs,
580580+ millRunsDir: config.millRunsDir,
581581+ },
543582 };
544583 registry.complete(runId, fullSummary);
545584 widget.update(registry.getVisible(), ctx);
···560599 results: [],
561600 error: details,
562601 observability: observability.toSummary(runId),
563563- metadata: { task: params.task },
602602+ metadata: {
603603+ task: params.task,
604604+ millCommand: config.millCommand,
605605+ millArgs: config.millArgs,
606606+ millRunsDir: config.millRunsDir,
607607+ },
564608 };
565609 registry.fail(runId, details);
566610 notifyCompletion(pi, registry, failedSummary);
···585629 ];
586630 if (artifactsDir) {
587631 lines.push(`Artifacts: ${artifactsDir}`);
588588- lines.push(`Status: ${artifactsDir}/run.json (written on completion)`);
632632+ lines.push(`Status: ${artifactsDir}/run.json (running marker + final summary)`);
589633 lines.push(`Sessions: ${artifactsDir}/sessions/`);
590634 }
591635 return {
+9-8
packages/pi-mill/monitor.ts
···524524 if (matchesKey(data, "c")) {
525525 const run = runs[this.selectedRunIndex];
526526 if (run && run.status === "running") {
527527- if (this.registry) {
528528- // In-session: use abort controller via registry
527527+ const artifactsDir = run.summary.observability?.artifactsDir;
528528+529529+ if (this.registry && run.abort) {
530530+ // In-session active run: cancel through abort controller.
531531+ this.registry.cancel(run.runId);
532532+ } else if (artifactsDir) {
533533+ // Historical/standalone: cancel through persisted run metadata.
534534+ cancelRunByPidFiles(artifactsDir);
535535+ } else if (this.registry) {
529536 this.registry.cancel(run.runId);
530530- } else {
531531- // Standalone mode: kill by PID files
532532- const artifactsDir = run.summary.observability?.artifactsDir;
533533- if (artifactsDir) {
534534- cancelRunByPidFiles(artifactsDir);
535535- }
536537 }
537538 }
538539 return;
···7070 throw new Error(`Failed to create workspace ${t.name}: ${result.stderr}`);
7171 }
72727373- factory.observe.log("info", `Created workspace: ${t.name}`, { path: wtPath });
7373+ mill.observe.log("info", `Created workspace: ${t.name}`, { path: wtPath });
7474 }
75757676 // 2. Install dependencies in each worktree
7777 await Promise.all(
7878 worktrees.map((wt, i) =>
7979- factory.spawn({
7979+ mill.spawn({
8080 agent: "installer",
8181 systemPrompt:
8282 "Install project dependencies. Run the appropriate install command (npm install, pnpm install, bun install, etc.) and verify it succeeds.",
···9191 // 3. Dispatch parallel agents
9292 const results = await Promise.all(
9393 tasks.map((t, i) =>
9494- factory.spawn({
9494+ mill.spawn({
9595 agent: t.name,
9696 systemPrompt: t.systemPrompt,
9797 prompt: t.prompt,
···105105 // 4. Check results
106106 const failed = results.filter((r) => r.exitCode !== 0);
107107 if (failed.length > 0) {
108108- factory.observe.log("warning", "Some agents failed", {
108108+ mill.observe.log("warning", "Some agents failed", {
109109 failed: failed.map((r) => r.agent),
110110 });
111111 }
112112113113 // 5. Merge results back
114114- const mergeResult = await factory.spawn({
114114+ const mergeResult = await mill.spawn({
115115 agent: "merger",
116116 systemPrompt: `You merge parallel workstream results using jj.
117117Use 'jj log' to see all changes across workspaces.
···131131 const summaryContent = results
132132 .map((r) => `## ${r.agent}\n**Status:** ${r.exitCode === 0 ? "pass" : "fail"}\n\n${r.text}`)
133133 .join("\n\n---\n\n");
134134- factory.observe.artifact("worktree-report.md", summaryContent);
134134+ mill.observe.artifact("worktree-report.md", summaryContent);
135135} finally {
136136 // 7. Cleanup — always runs
137137 for (const wt of worktrees) {
···143143 if (fs.existsSync(wt)) {
144144 fs.rmSync(wt, { recursive: true, force: true });
145145 }
146146- factory.observe.log("info", `Cleaned up workspace`, { path: wt });
146146+ mill.observe.log("info", `Cleaned up workspace`, { path: wt });
147147 }
148148}
149149```
···233233 throw new Error(`Failed to create worktree ${t.name}: ${result.stderr}`);
234234 }
235235236236- factory.observe.log("info", `Created worktree: ${t.name}`, { path: wtPath, branch });
236236+ mill.observe.log("info", `Created worktree: ${t.name}`, { path: wtPath, branch });
237237 }
238238239239 // 2. Install dependencies
240240 await Promise.all(
241241 worktrees.map((wt, i) =>
242242- factory.spawn({
242242+ mill.spawn({
243243 agent: "installer",
244244 systemPrompt: "Install project dependencies.",
245245 prompt: "Run the install command for this project (npm install, etc.)",
···253253 // 3. Dispatch agents
254254 const results = await Promise.all(
255255 tasks.map((t, i) =>
256256- factory.spawn({
256256+ mill.spawn({
257257 agent: t.name,
258258 systemPrompt: t.systemPrompt,
259259 prompt: `${t.prompt}\n\nCommit your changes to the current branch when complete.`,
···269269 .map((r, i) => ({ result: r, worktree: worktrees[i] }))
270270 .filter(({ result }) => result.exitCode === 0);
271271272272- await factory.spawn({
272272+ await mill.spawn({
273273 agent: "merger",
274274 systemPrompt: `You merge git branches from parallel workstreams.
275275Merge each feature branch into ${baseBranch}.
···359359 // Install deps in parallel
360360 await Promise.all(
361361 worktrees.map((wt, i) =>
362362- factory.spawn({
362362+ mill.spawn({
363363 agent: "installer",
364364 systemPrompt: "Install deps.",
365365 prompt: "npm install",
···373373 // Parallel implementation
374374 const results = await Promise.all(
375375 tasks.map((t, i) =>
376376- factory.spawn({
376376+ mill.spawn({
377377 agent: t.name,
378378 systemPrompt: t.systemPrompt,
379379 prompt: t.prompt,
···386386387387 // Synthesize — merge and verify
388388 const context = results.map((r) => `[${r.agent}]\n${r.text}`).join("\n\n");
389389- const synthesis = await factory.spawn({
389389+ const synthesis = await mill.spawn({
390390 agent: "integrator",
391391 systemPrompt: `You integrate parallel workstreams.
3923921. Use jj to merge all workspace changes into the main workspace.
···446446// Dedicated install step
447447await Promise.all(
448448 worktrees.map((wt) =>
449449- factory.spawn({
449449+ mill.spawn({
450450 agent: "installer",
451451 systemPrompt: "Install dependencies.",
452452 prompt: "npm install",
···459459// Then dispatch real work
460460await Promise.all(
461461 tasks.map((t, i) =>
462462- factory.spawn({
462462+ mill.spawn({
463463 agent: t.name,
464464 systemPrompt: t.systemPrompt,
465465 prompt: t.prompt,
···507507Not ideal for:
508508509509- Tasks that heavily overlap in the same files
510510-- Read-only analysis (just use `Promise.all` with `factory.spawn` and same `cwd`)
510510+- Read-only analysis (just use `Promise.all` with `mill.spawn` and same `cwd`)
511511- Very small changes (worktree overhead isn't worth it)
512512- Repos with huge `node_modules` or build artifacts (disk cost)
513513
+6
packages/skills/README.md
···11+# pi-mill-skills
22+33+Shared skills package for mill orchestration guidance.
44+55+Primary skill:
66+- `mill/` — reusable for pi, Claude Code, and Codex workflows.
+24
packages/skills/mill/SKILL.md
···11+---
22+name: mill
33+description: "Write mill orchestration programs for parallel/sequential agent workflows, iterative Ralph Loop execution, and worktree-isolated development."
44+---
55+66+# mill
77+88+Use this skill whenever you are writing or reviewing a mill-based orchestration program (including pi-mill extension flows).
99+1010+## Core rules
1111+1212+1. Keep `systemPrompt` (WHO/how) separate from `prompt` (WHAT/task).
1313+2. Use `await` for sequential steps and `Promise.all` for independent parallel work.
1414+3. Always pass an explicit `model` in `provider/model-id` format.
1515+4. Check `exitCode`, `stopReason`, and `errorMessage` before trusting results.
1616+5. Use `mill.observe.log(...)` for progress and diagnostics.
1717+1818+## Available patterns
1919+2020+- General orchestration patterns: `./references/patterns.md`
2121+- Iterative Ralph Loop pattern: `./references/ralph-loop.md`
2222+- Worktree-isolated parallel development: `./references/worktree.md`
2323+2424+Prefer these patterns before inventing new orchestration scaffolding.