···11+# @mill/pi-mill
22+33+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.
44+55+## What stays the same
66+77+- `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`)
1414+1515+## What changed
1616+1717+- 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.
2020+2121+## Install as a pi package
2222+2323+```bash
2424+pi install /absolute/path/to/mill/packages/pi-mill
2525+```
2626+2727+(or add as a local package in your pi settings).
2828+2929+## Mill prerequisites
3030+3131+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.
3333+3434+## Extension config
3535+3636+Edit `index.ts`:
3737+3838+```ts
3939+export const config = {
4040+ maxDepth: 1,
4141+ millCommand: "mill",
4242+ millArgs: [],
4343+ millRunsDir: undefined,
4444+ prompt: "...optional extra guidance for the tool description...",
4545+};
4646+```
4747+4848+- `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
5353+5454+## Notes
5555+5656+- 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.
+30
packages/pi-mill/contract.ts
···11+import { Type, type Static } from "@sinclair/typebox";
22+import { FactoryError } from "./errors.js";
33+44+export const SubagentSchema = Type.Object({
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.",
99+ }),
1010+});
1111+1212+export type SubagentParams = Static<typeof SubagentSchema>;
1313+1414+export function validateParams(params: SubagentParams): SubagentParams {
1515+ if (!params.task?.trim()) {
1616+ throw new FactoryError({
1717+ code: "INVALID_INPUT",
1818+ message: "'task' is required.",
1919+ recoverable: true,
2020+ });
2121+ }
2222+ if (!params.code?.trim()) {
2323+ throw new FactoryError({
2424+ code: "INVALID_INPUT",
2525+ message: "'code' is required and must be non-empty.",
2626+ recoverable: true,
2727+ });
2828+ }
2929+ return params;
3030+}
···11+import * as fs from "node:fs";
22+import * as path from "node:path";
33+import * as os from "node:os";
44+import type { RunRecord, RunStatus } from "./registry.js";
55+import type { RunSummary, ExecutionResult, UsageStats } from "./types.js";
66+77+/**
88+ * Filesystem scanner for standalone --mill mode.
99+ * Reads run.json files from ~/.pi/agent/sessions/<session-dir>/.factory/<run-id>/run.json
1010+ */
1111+1212+/** Convert a cwd path to the session directory name pi uses. */
1313+export function cwdToSessionDir(cwd: string): string {
1414+ // /Users/foo/Code/project → --Users-foo-Code-project--
1515+ return "--" + cwd.slice(1).replace(/\//g, "-") + "--";
1616+}
1717+1818+/** Shape of run.json on disk (written by writeRunJson in index.ts). */
1919+interface RunJsonData {
2020+ runId: string;
2121+ status?: RunStatus;
2222+ task?: string;
2323+ startedAt?: number;
2424+ completedAt?: number;
2525+ results?: Array<{
2626+ agent: string;
2727+ task: string;
2828+ model?: string;
2929+ exitCode: number;
3030+ text: string;
3131+ sessionPath?: string;
3232+ usage?: UsageStats;
3333+ stopReason?: string;
3434+ errorMessage?: string;
3535+ }>;
3636+ error?: { code: string; message: string; recoverable: boolean };
3737+}
3838+3939+/** Parse a single run.json into a RunRecord (without promise/abort). */
4040+function parseRunJson(data: RunJsonData): Omit<RunRecord, "promise" | "abort"> {
4141+ const status: RunStatus = data.status ?? "done";
4242+ const results: ExecutionResult[] = (data.results ?? []).map((r) => ({
4343+ taskId: "",
4444+ agent: r.agent ?? "unknown",
4545+ task: r.task ?? "",
4646+ exitCode: r.exitCode ?? -1,
4747+ messages: [],
4848+ stderr: "",
4949+ usage: r.usage ?? {
5050+ input: 0,
5151+ output: 0,
5252+ cacheRead: 0,
5353+ cacheWrite: 0,
5454+ cost: 0,
5555+ contextTokens: 0,
5656+ turns: 0,
5757+ },
5858+ model: r.model,
5959+ stopReason: r.stopReason,
6060+ errorMessage: r.errorMessage,
6161+ text: r.text ?? "",
6262+ sessionPath: r.sessionPath,
6363+ }));
6464+6565+ const summary: RunSummary = {
6666+ runId: data.runId,
6767+ status,
6868+ results,
6969+ error: data.error as RunSummary["error"],
7070+ metadata: { task: data.task },
7171+ };
7272+7373+ return {
7474+ runId: data.runId,
7575+ status,
7676+ summary,
7777+ startedAt: data.startedAt ?? Date.now(),
7878+ completedAt: data.completedAt,
7979+ acknowledged: true,
8080+ task: data.task,
8181+ };
8282+}
8383+8484+/** Get the sessions base directory. */
8585+export function getSessionsBase(): string {
8686+ return path.join(os.homedir(), ".pi", "agent", "sessions");
8787+}
8888+8989+/**
9090+ * Scan all run.json files under a session's .factory directory.
9191+ * If sessionDirName is provided, scans only that session.
9292+ * If not provided, scans all sessions.
9393+ */
9494+export function scanRuns(
9595+ sessionsBase: string,
9696+ sessionDirName?: string,
9797+): Omit<RunRecord, "promise" | "abort">[] {
9898+ const records: Omit<RunRecord, "promise" | "abort">[] = [];
9999+100100+ const sessionDirs = sessionDirName ? [sessionDirName] : listSessionDirs(sessionsBase);
101101+102102+ for (const dir of sessionDirs) {
103103+ const factoryDir = path.join(sessionsBase, dir, ".factory");
104104+ if (!fs.existsSync(factoryDir)) continue;
105105+106106+ try {
107107+ for (const entry of fs.readdirSync(factoryDir)) {
108108+ const runJsonPath = path.join(factoryDir, entry, "run.json");
109109+ if (!fs.existsSync(runJsonPath)) continue;
110110+ try {
111111+ const raw = fs.readFileSync(runJsonPath, "utf-8");
112112+ const data: RunJsonData = JSON.parse(raw);
113113+ records.push(parseRunJson(data));
114114+ } catch {
115115+ // Skip malformed run.json files
116116+ }
117117+ }
118118+ } catch {
119119+ // Skip inaccessible directories
120120+ }
121121+ }
122122+123123+ return records;
124124+}
125125+126126+/**
127127+ * Cancel a subagent by reading its PID file and sending SIGTERM (then SIGKILL after 3s).
128128+ * Returns true if the signal was sent, false if the PID file was missing or the process was already gone.
129129+ */
130130+export function cancelByPidFile(outputDir: string, taskId: string): boolean {
131131+ const pidPath = path.join(outputDir, `${taskId}.pid`);
132132+ try {
133133+ const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim(), 10);
134134+ if (isNaN(pid)) return false;
135135+ process.kill(pid, "SIGTERM");
136136+ setTimeout(() => {
137137+ try {
138138+ process.kill(pid, "SIGKILL");
139139+ } catch {}
140140+ }, 3000);
141141+ return true;
142142+ } catch {
143143+ return false;
144144+ }
145145+}
146146+147147+/**
148148+ * Cancel all running subagents for a run by scanning for PID files in the run's sessions directory.
149149+ * Returns the number of processes signalled.
150150+ */
151151+export function cancelRunByPidFiles(artifactsDir: string): number {
152152+ const sessionsDir = path.join(artifactsDir, "sessions");
153153+ let cancelled = 0;
154154+ try {
155155+ if (!fs.existsSync(sessionsDir)) return 0;
156156+ for (const entry of fs.readdirSync(sessionsDir)) {
157157+ if (!entry.endsWith(".pid")) continue;
158158+ const taskId = entry.replace(/\.pid$/, "");
159159+ if (cancelByPidFile(sessionsDir, taskId)) cancelled++;
160160+ }
161161+ } catch {}
162162+ return cancelled;
163163+}
164164+165165+/** List all session directory names under the sessions base. */
166166+function listSessionDirs(sessionsBase: string): string[] {
167167+ try {
168168+ if (!fs.existsSync(sessionsBase)) return [];
169169+ return fs.readdirSync(sessionsBase).filter((d) => {
170170+ try {
171171+ return fs.statSync(path.join(sessionsBase, d)).isDirectory();
172172+ } catch {
173173+ return false;
174174+ }
175175+ });
176176+ } catch {
177177+ return [];
178178+ }
179179+}
+225
packages/pi-mill/skills/mill-basics/SKILL.md
···11+---
22+name: mill-basics
33+description: "Write pi-mill programs to orchestrate multi-agent workflows. Use when spawning subagents, coordinating parallel/sequential tasks, building agent-driven automation, or applying common orchestration patterns like fan-out, pipelines, and synthesis."
44+---
55+66+# Mill Basics
77+88+Pi-mill enables writing scripts that orchestrate multiple AI agents. Scripts use the `factory` global to spawn subagents, coordinate work, and compose results.
99+1010+## systemPrompt vs prompt
1111+1212+These two fields have distinct roles — don't mix them:
1313+1414+- **systemPrompt**: Defines WHO the agent is and HOW it should behave. Personality, methodology, principles, output format, tool usage conventions.
1515+- **prompt**: Defines WHAT the agent should do right now. The concrete assignment — specific files to read, bugs to fix, features to implement, commands to run.
1616+1717+```typescript
1818+// BAD: work leaked into systemPrompt
1919+{ systemPrompt: "Review src/auth/ for security issues", prompt: "Do the review" }
2020+2121+// BAD: systemPrompt is too weak
2222+{ systemPrompt: "Lint.", prompt: "Run lint on src/ and fix errors" }
2323+2424+// GOOD: clean separation
2525+{
2626+ systemPrompt: "You are a security-focused code reviewer. Look for OWASP Top 10 vulnerabilities, injection flaws, and auth bypasses. Report findings with severity ratings.",
2727+ prompt: "Review src/auth/ for security issues. Focus on the login flow and session management."
2828+}
2929+```
3030+3131+## Program Structure
3232+3333+Factory programs are top-level TypeScript scripts. The `factory` global is available — no imports or exports needed:
3434+3535+```typescript
3636+const result = await factory.spawn({
3737+ agent: "researcher",
3838+ systemPrompt:
3939+ "You are a research assistant. You find accurate, up-to-date information and cite sources. You present findings in a structured format.",
4040+ prompt:
4141+ "Find information about TypeScript 5.0 — new features, breaking changes, and migration notes.",
4242+ model: "anthropic/claude-opus-4-6",
4343+});
4444+4545+console.log(result.text);
4646+```
4747+4848+The script runs as a module — use `await` at top level, `Promise.all` for parallelism, and standard imports.
4949+5050+## Mill API
5151+5252+### spawn
5353+5454+Create a subagent task:
5555+5656+```typescript
5757+const result = await factory.spawn({
5858+ agent: "code-reviewer", // Role label (for logging/display)
5959+ systemPrompt: "You review code...", // WHO: behavior, principles, methodology
6060+ prompt: "Review main.ts for...", // WHAT: the specific work to do now
6161+ model: "anthropic/claude-opus-4-6", // Model in provider/model-id format
6262+ cwd: "/path/to/project", // Working directory (defaults to process.cwd())
6363+ step: 1, // Optional step number
6464+ signal: abortSignal, // Optional cancellation
6565+});
6666+```
6767+6868+Returns `Promise<ExecutionResult>`. Use `await` for one agent, `Promise.all` for parallel execution.
6969+7070+### Parallel execution
7171+7272+```typescript
7373+const [security, coverage] = await Promise.all([
7474+ factory.spawn({
7575+ agent: "security",
7676+ systemPrompt: "You are a security reviewer...",
7777+ prompt: "Review src/auth/",
7878+ model: "anthropic/claude-opus-4-6",
7979+ }),
8080+ factory.spawn({
8181+ agent: "coverage",
8282+ systemPrompt: "You analyze test coverage...",
8383+ prompt: "Check coverage for src/auth/",
8484+ model: "anthropic/claude-sonnet-4-6",
8585+ }),
8686+]);
8787+```
8888+8989+### Observe
9090+9191+```typescript
9292+factory.observe.log("info", "Starting analysis", { fileCount: 42 });
9393+factory.observe.log("warning", "Slow response", { duration: 5000 });
9494+factory.observe.log("error", "Task failed", { taskId: "task-3" });
9595+9696+const artifactPath = factory.observe.artifact("summary.md", reportContent);
9797+```
9898+9999+### Shutdown
100100+101101+```typescript
102102+await factory.shutdown(true); // Cancel all running tasks
103103+await factory.shutdown(false); // Wait for running tasks to complete naturally
104104+```
105105+106106+## Execution Results
107107+108108+Each subagent returns an `ExecutionResult`:
109109+110110+```typescript
111111+interface ExecutionResult {
112112+ taskId: string;
113113+ agent: string;
114114+ task: string; // Original execution prompt string
115115+ exitCode: number;
116116+117117+ text: string;
118118+ sessionPath?: string;
119119+120120+ messages: unknown[];
121121+122122+ usage: UsageStats;
123123+ model?: string;
124124+ stopReason?: string;
125125+ errorMessage?: string;
126126+ stderr: string;
127127+128128+ step?: number;
129129+}
130130+```
131131+132132+## Context flow
133133+134134+### Context DOWN (Parent -> Subagent)
135135+136136+The parent session path is appended to the subagent system prompt automatically. Subagents can use `search_thread` to read parent context.
137137+138138+### Context UP (Subagent -> Program)
139139+140140+1. `result.text` for quick chaining
141141+2. `result.sessionPath` for deep review
142142+143143+```typescript
144144+const research = await factory.spawn({
145145+ agent: "researcher",
146146+ systemPrompt: "You are a thorough technical researcher.",
147147+ prompt: "Research Rust async patterns and common pitfalls.",
148148+ model: "anthropic/claude-opus-4-6",
149149+});
150150+151151+const summary = await factory.spawn({
152152+ agent: "summarizer",
153153+ systemPrompt: "You write concise executive summaries.",
154154+ prompt: `Summarize this research:\n\n${research.text}`,
155155+ model: "anthropic/claude-haiku-4-5",
156156+});
157157+158158+const review = await factory.spawn({
159159+ agent: "reviewer",
160160+ systemPrompt: "You are a technical reviewer. Verify claims and flag unsupported assertions.",
161161+ prompt: `Review research session at ${research.sessionPath} for technical accuracy.`,
162162+ model: "anthropic/claude-opus-4-6",
163163+});
164164+```
165165+166166+## Error handling
167167+168168+Check `exitCode` / `stopReason` / `errorMessage` and escalate:
169169+170170+```typescript
171171+const result = await factory.spawn({ ... });
172172+173173+const failed =
174174+ result.exitCode !== 0 ||
175175+ result.stopReason === "error" ||
176176+ Boolean(result.errorMessage);
177177+178178+if (failed) {
179179+ factory.observe.log("error", "Task failed", {
180180+ taskId: result.taskId,
181181+ exitCode: result.exitCode,
182182+ stopReason: result.stopReason,
183183+ error: result.errorMessage,
184184+ stderr: result.stderr,
185185+ });
186186+ throw new Error(`Task ${result.taskId} failed: ${result.errorMessage || "unknown error"}`);
187187+}
188188+```
189189+190190+## Async model
191191+192192+Programs run asynchronously by default when invoked via tool call: immediate `runId`, completion via notification.
193193+194194+Inside your program, use `await` and `Promise.all` normally:
195195+196196+```typescript
197197+const [r1, r2] = await Promise.all([
198198+ factory.spawn({
199199+ agent: "a",
200200+ systemPrompt: "...",
201201+ prompt: "...",
202202+ model: "anthropic/claude-opus-4-6",
203203+ }),
204204+ factory.spawn({ agent: "b", systemPrompt: "...", prompt: "...", model: "cerebras/zai-glm-4.7" }),
205205+]);
206206+console.log(r1.text, r2.text);
207207+```
208208+209209+## Detached processes
210210+211211+Subagent processes are detached:
212212+213213+- Closing pi or cancelling a turn does **not** kill running subagents
214214+- Output is written to `.stdout.jsonl` files
215215+- PID files enable cancel via `/mill` or `pi --mill`
216216+217217+## Key principles
218218+219219+1. Programs coordinate, subagents execute
220220+2. Use `result.text` for fast chaining
221221+3. Use `result.sessionPath` for deep context
222222+4. Check failure signals (`exitCode`, `stopReason`, `errorMessage`)
223223+5. Log progress with `factory.observe.log()`
224224+225225+See [patterns.md](patterns.md) for common orchestration patterns.
+117
packages/pi-mill/skills/mill-basics/patterns.md
···11+# Mill Patterns
22+33+Common orchestration patterns for pi-mill programs.
44+55+## Parallel Review
66+77+Fan out independent tasks, collect results:
88+99+```ts
1010+const results = await Promise.all([
1111+ factory.spawn({
1212+ agent: "security",
1313+ systemPrompt:
1414+ "You are a security reviewer. You look for injection flaws, auth bypasses, and data exposure. Report findings with severity ratings.",
1515+ prompt: "Review src/auth/ for security vulnerabilities.",
1616+ model: "anthropic/claude-opus-4-6",
1717+ step: 0,
1818+ }),
1919+ factory.spawn({
2020+ agent: "perf",
2121+ systemPrompt:
2222+ "You are a performance analyst. You identify bottlenecks, unnecessary allocations, and O(n²) patterns.",
2323+ prompt: "Profile src/api/ for performance issues.",
2424+ model: "anthropic/claude-sonnet-4-6",
2525+ step: 1,
2626+ }),
2727+]);
2828+```
2929+3030+## Sequential Pipeline
3131+3232+Each step feeds into the next via `result.text`:
3333+3434+```ts
3535+const analysis = await factory.spawn({
3636+ agent: "analyzer",
3737+ systemPrompt:
3838+ "You analyze codebases systematically. You map structure, dependencies, and public interfaces.",
3939+ prompt: "Map all API endpoints in the codebase — list routes, methods, and handlers.",
4040+ model: "anthropic/claude-opus-4-6",
4141+ step: 0,
4242+});
4343+4444+const plan = await factory.spawn({
4545+ agent: "planner",
4646+ systemPrompt: "You design thorough test plans. You prioritize critical paths and edge cases.",
4747+ prompt: `Design integration tests covering the API endpoints found:\n\n${analysis.text}`,
4848+ model: "anthropic/claude-sonnet-4-6",
4949+ step: 1,
5050+});
5151+```
5252+5353+## Fan-out then Synthesize
5454+5555+Parallel investigation followed by a single summarizer:
5656+5757+```ts
5858+const reviews = await Promise.all([
5959+ factory.spawn({
6060+ agent: "frontend",
6161+ systemPrompt:
6262+ "You are a frontend specialist. You review UI code for accessibility, performance, and UX issues.",
6363+ prompt: "Review the frontend code.",
6464+ model: "anthropic/claude-sonnet-4-6",
6565+ step: 0,
6666+ }),
6767+ factory.spawn({
6868+ agent: "backend",
6969+ systemPrompt:
7070+ "You are a backend specialist. You review server code for correctness, scalability, and error handling.",
7171+ prompt: "Review the backend code.",
7272+ model: "mistral/devstral-2512",
7373+ step: 1,
7474+ }),
7575+ factory.spawn({
7676+ agent: "infra",
7777+ systemPrompt:
7878+ "You are an infrastructure specialist. You review configs, deployments, and operational concerns.",
7979+ prompt: "Review the infrastructure.",
8080+ model: "anthropic/claude-haiku-4-5",
8181+ step: 2,
8282+ }),
8383+]);
8484+8585+const context = reviews.map((r) => `[${r.agent}]\n${r.text}`).join("\n\n");
8686+const summary = await factory.spawn({
8787+ agent: "synthesizer",
8888+ systemPrompt:
8989+ "You synthesize multiple perspectives into clear, actionable summaries. You deduplicate, prioritize, and highlight conflicts.",
9090+ prompt: `Synthesize these reviews into an actionable summary:\n${context}`,
9191+ model: "anthropic/claude-opus-4-6",
9292+ step: 3,
9393+});
9494+```
9595+9696+## Model Selection
9797+9898+Models use `provider/model-id` format. Match capability to task complexity:
9999+100100+- **Fast/cheap** -- `cerebras/zai-glm-4.7` for file search, formatting, grep-like work
101101+- **Fast + vision** -- `google-gemini-cli/gemini-3-flash-preview` when the agent needs to look at images or screenshots
102102+- **Mid-tier coding** -- `mistral/devstral-2512` for code review, refactoring, focused implementation
103103+- **Mid-tier general** -- `anthropic/claude-haiku-4-5` for analysis, summarization, planning
104104+- **Frontier** -- `anthropic/claude-opus-4-6` for complex multi-step reasoning, large changes across many files
105105+- **Frontier coding** -- `openai-codex/gpt-5.3-codex` for heavy implementation tasks
106106+- **Strong all-rounder** -- `anthropic/claude-sonnet-4-6` for tasks that need solid reasoning without frontier cost
107107+108108+Override `model` per-agent when tasks vary in complexity. Don't default everything to one model.
109109+110110+## Context Chaining
111111+112112+Each result has:
113113+114114+- `result.text` — final assistant output, use directly in subsequent prompts
115115+- `result.sessionPath` — full session file, explorable via `search_thread`
116116+117117+Pass context between agents by including `result.text` in the next agent's prompt string. For deep investigation, point agents at each other's `sessionPath`.
+515
packages/pi-mill/skills/mill-ralph-loop/SKILL.md
···11+---
22+name: mill-ralph-loop
33+description: Iterative task execution using the Ralph Loop pattern (named after Ralph Wiggum). Use when you need to repeatedly run an agent until a condition is met—fixing all lint errors, passing all tests, or exhausting PRD tasks. The filesystem serves as memory between iterations.
44+---
55+66+# Ralph Loop Pattern
77+88+The Ralph Loop (named after Ralph Wiggum) is an agentic pattern where you run an AI agent in a continuous loop until a task is complete. Each iteration starts relatively fresh, with the filesystem serving as persistent memory.
99+1010+## Core Characteristics
1111+1212+1. **Same systemPrompt repeated** — The agent receives consistent instructions each iteration
1313+2. **Filesystem as memory** — Code changes persist on disk between iterations
1414+3. **Fresh context** — Each iteration reduces context pollution vs. single long conversation
1515+4. **Exit condition** — Loop ends when tests pass, lint is clean, or work is exhausted
1616+5. **Simple orchestrator** — Just `while (!done) { run agent }`
1717+1818+## Basic Structure
1919+2020+```typescript
2121+const maxIterations = 10;
2222+let iteration = 0;
2323+let done = false;
2424+2525+while (!done && iteration < maxIterations) {
2626+ iteration++;
2727+ factory.observe.log("info", `Iteration ${iteration}`, { maxIterations });
2828+2929+ const result = await factory.spawn({
3030+ agent: "worker",
3131+ systemPrompt: "You are fixing issues iteratively",
3232+ prompt: "Fix the next issue",
3333+ model: "anthropic/claude-sonnet-4-6",
3434+ step: iteration,
3535+ });
3636+3737+ // Check exit condition
3838+ done = result.exitCode === 0 && result.text.includes("all clean");
3939+4040+ if (result.exitCode !== 0) {
4141+ factory.observe.log("error", "Agent failed", { iteration, error: result.errorMessage });
4242+ break;
4343+ }
4444+}
4545+```
4646+4747+## Pattern 1: Fix All Lint Errors
4848+4949+Repeatedly run an agent until lint is clean:
5050+5151+```typescript
5252+import { spawnSync } from "node:child_process";
5353+5454+const maxIterations = 20;
5555+let iteration = 0;
5656+5757+while (iteration < maxIterations) {
5858+ iteration++;
5959+6060+ const lintResult = spawnSync("npm", ["run", "lint"], {
6161+ cwd: process.cwd(),
6262+ encoding: "utf-8",
6363+ });
6464+6565+ if (lintResult.status === 0) {
6666+ factory.observe.log("info", "Lint clean!", { iterations: iteration });
6767+ break;
6868+ }
6969+7070+ factory.observe.log("info", `Iteration ${iteration}`, {
7171+ exitCode: lintResult.status,
7272+ errorCount: (lintResult.stdout.match(/error/gi) || []).length,
7373+ });
7474+7575+ const result = await factory.spawn({
7676+ agent: "linter",
7777+ systemPrompt: `You fix lint errors iteratively.
7878+Run 'npm run lint' to see current errors.
7979+Fix one or more errors, focusing on the most common patterns.
8080+Make minimal, focused changes.`,
8181+ prompt: `Fix lint errors. Current output:\n\n${lintResult.stdout}\n${lintResult.stderr}`,
8282+ model: "mistral/devstral-2512",
8383+ step: iteration,
8484+ });
8585+8686+ if (result.exitCode !== 0) {
8787+ factory.observe.log("error", "Agent failed", { iteration });
8888+ break;
8989+ }
9090+}
9191+```
9292+9393+## Pattern 2: With Progress Tracking
9494+9595+Accumulate state across iterations to show progress:
9696+9797+```typescript
9898+import { spawnSync } from "node:child_process";
9999+100100+interface ProgressState {
101101+ fixedIssues: string[];
102102+ lastErrorCount: number;
103103+ stagnantIterations: number;
104104+}
105105+106106+const maxIterations = 20;
107107+let iteration = 0;
108108+109109+const progress: ProgressState = {
110110+ fixedIssues: [],
111111+ lastErrorCount: Infinity,
112112+ stagnantIterations: 0,
113113+};
114114+115115+while (iteration < maxIterations) {
116116+ iteration++;
117117+118118+ const lintResult = spawnSync("npm", ["run", "lint"], {
119119+ cwd: process.cwd(),
120120+ encoding: "utf-8",
121121+ });
122122+123123+ const errorCount = (lintResult.stdout.match(/error/gi) || []).length;
124124+125125+ if (lintResult.status === 0) {
126126+ factory.observe.log("info", "All issues fixed!", {
127127+ iterations: iteration,
128128+ fixedIssues: progress.fixedIssues,
129129+ });
130130+ break;
131131+ }
132132+133133+ // Track progress
134134+ if (errorCount >= progress.lastErrorCount) {
135135+ progress.stagnantIterations++;
136136+ } else {
137137+ progress.stagnantIterations = 0;
138138+ }
139139+140140+ // Exit if stagnant
141141+ if (progress.stagnantIterations >= 3) {
142142+ factory.observe.log("warning", "No progress for 3 iterations", { errorCount });
143143+ break;
144144+ }
145145+146146+ factory.observe.log("info", `Iteration ${iteration}`, {
147147+ errorCount,
148148+ lastErrorCount: progress.lastErrorCount,
149149+ fixed: progress.fixedIssues.length,
150150+ });
151151+152152+ progress.lastErrorCount = errorCount;
153153+154154+ const result = await factory.spawn({
155155+ agent: "fixer",
156156+ systemPrompt: `You fix lint errors iteratively.
157157+Track your progress and avoid repeating unsuccessful approaches.
158158+Previous fixes: ${progress.fixedIssues.join(", ") || "none yet"}
159159+Error count: ${errorCount} (was ${progress.lastErrorCount === Infinity ? "unknown" : progress.lastErrorCount})`,
160160+ prompt: `Fix lint errors:\n\n${lintResult.stdout}\n${lintResult.stderr}`,
161161+ model: "anthropic/claude-sonnet-4-6",
162162+ step: iteration,
163163+ });
164164+165165+ if (result.exitCode === 0) {
166166+ const fixMatch = result.text.match(/fixed?:?\s*(.+)/i);
167167+ if (fixMatch) {
168168+ progress.fixedIssues.push(fixMatch[1]);
169169+ }
170170+ }
171171+}
172172+```
173173+174174+## Pattern 3: Loop Until Tests Pass
175175+176176+Run agent repeatedly until test suite passes:
177177+178178+```typescript
179179+import { spawnSync } from "node:child_process";
180180+181181+const testCommand = "npm test";
182182+const [cmd, ...args] = testCommand.split(" ");
183183+const maxIterations = 10;
184184+let iteration = 0;
185185+186186+while (iteration < maxIterations) {
187187+ iteration++;
188188+189189+ const testResult = spawnSync(cmd, args, {
190190+ cwd: process.cwd(),
191191+ encoding: "utf-8",
192192+ timeout: 60000,
193193+ });
194194+195195+ if (testResult.status === 0) {
196196+ factory.observe.log("info", "Tests passing!", { iterations: iteration });
197197+ break;
198198+ }
199199+200200+ factory.observe.log("info", `Iteration ${iteration}`, {
201201+ exitCode: testResult.status,
202202+ timeout: testResult.signal === "SIGTERM",
203203+ });
204204+205205+ const failureOutput = [testResult.stdout, testResult.stderr]
206206+ .filter(Boolean)
207207+ .join("\n")
208208+ .slice(-5000); // Last 5KB to avoid huge prompt payloads
209209+210210+ const result = await factory.spawn({
211211+ agent: "test-fixer",
212212+ systemPrompt: `You fix failing tests iteratively.
213213+Analyze test output, identify the root cause, and make minimal fixes.
214214+Run the tests again to verify your changes.
215215+Focus on one failure at a time if there are multiple.`,
216216+ prompt: `Fix failing tests. Output from '${testCommand}':\n\n${failureOutput}`,
217217+ model: "anthropic/claude-opus-4-6",
218218+ step: iteration,
219219+ });
220220+221221+ if (result.exitCode !== 0) {
222222+ factory.observe.log("error", "Agent failed", { iteration });
223223+ break;
224224+ }
225225+}
226226+```
227227+228228+## Pattern 4: Exhaustive PRD Implementation
229229+230230+Work through Product Requirements Document tasks until all are complete:
231231+232232+```typescript
233233+import fs from "node:fs";
234234+235235+interface PRDTask {
236236+ id: string;
237237+ description: string;
238238+ completed: boolean;
239239+}
240240+241241+const prdPath = "./PRD.md";
242242+const tasksPath = "./tasks.json";
243243+const maxIterations = 50;
244244+245245+// Load or initialize tasks
246246+let tasks: PRDTask[];
247247+if (fs.existsSync(tasksPath)) {
248248+ tasks = JSON.parse(fs.readFileSync(tasksPath, "utf-8"));
249249+} else {
250250+ const prdContent = fs.readFileSync(prdPath, "utf-8");
251251+ tasks = parsePRD(prdContent);
252252+ fs.writeFileSync(tasksPath, JSON.stringify(tasks, null, 2));
253253+}
254254+255255+let iteration = 0;
256256+257257+while (iteration < maxIterations) {
258258+ const nextTask = tasks.find((t) => !t.completed);
259259+ if (!nextTask) {
260260+ factory.observe.log("info", "All tasks completed!", { iterations: iteration });
261261+ break;
262262+ }
263263+264264+ iteration++;
265265+ factory.observe.log("info", `Iteration ${iteration}: ${nextTask.id}`, {
266266+ remaining: tasks.filter((t) => !t.completed).length,
267267+ });
268268+269269+ const result = await factory.spawn({
270270+ agent: "implementer",
271271+ systemPrompt: `You implement PRD tasks iteratively.
272272+Read the PRD at ${prdPath}.
273273+Complete tasks one at a time.
274274+Mark tasks complete by updating ${tasksPath}.`,
275275+ prompt: `Implement: ${nextTask.id} - ${nextTask.description}\n\nCompleted so far:\n${tasks
276276+ .filter((t) => t.completed)
277277+ .map((t) => `+ ${t.id}`)
278278+ .join("\n")}`,
279279+ model: "openai-codex/gpt-5.3-codex",
280280+ step: iteration,
281281+ });
282282+283283+ if (result.exitCode !== 0) {
284284+ factory.observe.log("error", "Agent failed", { iteration, task: nextTask.id });
285285+ break;
286286+ }
287287+288288+ // Reload tasks (agent may have updated them)
289289+ if (fs.existsSync(tasksPath)) {
290290+ tasks = JSON.parse(fs.readFileSync(tasksPath, "utf-8"));
291291+ }
292292+}
293293+294294+function parsePRD(content: string): PRDTask[] {
295295+ const matches = content.matchAll(/^[-*]\s*\[\s*\]\s*(.+)$/gm);
296296+ const tasks: PRDTask[] = [];
297297+ let id = 1;
298298+299299+ for (const match of matches) {
300300+ tasks.push({
301301+ id: `TASK-${id++}`,
302302+ description: match[1].trim(),
303303+ completed: false,
304304+ });
305305+ }
306306+307307+ return tasks;
308308+}
309309+```
310310+311311+## Pattern 5: Combined Safety Checks
312312+313313+Comprehensive safety and exit logic:
314314+315315+```typescript
316316+import { spawnSync } from "node:child_process";
317317+318318+const maxIterations = 20;
319319+const maxStagnantIterations = 3;
320320+const maxFailedIterations = 2;
321321+const checkCommand = "npm run lint";
322322+323323+let iteration = 0;
324324+let stagnantCount = 0;
325325+let failedCount = 0;
326326+let lastCheckOutput = "";
327327+328328+while (iteration < maxIterations) {
329329+ iteration++;
330330+331331+ // Periodic check
332332+ const [cmd, ...args] = checkCommand.split(" ");
333333+ const checkResult = spawnSync(cmd, args, {
334334+ cwd: process.cwd(),
335335+ encoding: "utf-8",
336336+ });
337337+338338+ if (checkResult.status === 0) {
339339+ factory.observe.log("info", "Check passed!", { iterations: iteration });
340340+ break;
341341+ }
342342+343343+ // Track stagnation
344344+ const currentOutput = checkResult.stdout + checkResult.stderr;
345345+ if (currentOutput === lastCheckOutput) {
346346+ stagnantCount++;
347347+ factory.observe.log("warning", "No change detected", { stagnantCount });
348348+ } else {
349349+ stagnantCount = 0;
350350+ }
351351+ lastCheckOutput = currentOutput;
352352+353353+ if (stagnantCount >= maxStagnantIterations) {
354354+ factory.observe.log("error", "Stagnant iterations exceeded", { stagnantCount });
355355+ break;
356356+ }
357357+358358+ factory.observe.log("info", `Iteration ${iteration}`, {
359359+ stagnantCount,
360360+ failedCount,
361361+ max: maxIterations,
362362+ });
363363+364364+ const result = await factory.spawn({
365365+ agent: "worker",
366366+ systemPrompt: "You are fixing issues iteratively",
367367+ prompt: "Continue fixing issues",
368368+ model: "anthropic/claude-sonnet-4-6",
369369+ step: iteration,
370370+ });
371371+372372+ if (result.exitCode !== 0) {
373373+ failedCount++;
374374+ factory.observe.log("error", "Agent failed", { iteration, failedCount });
375375+376376+ if (failedCount >= maxFailedIterations) {
377377+ factory.observe.log("error", "Failed iterations exceeded", { failedCount });
378378+ break;
379379+ }
380380+ } else {
381381+ failedCount = 0;
382382+ }
383383+}
384384+```
385385+386386+## Best Practices
387387+388388+### 1. **Set max iterations**
389389+390390+Always have an upper bound to prevent infinite loops:
391391+392392+```typescript
393393+const maxIterations = 20; // Sensible default
394394+```
395395+396396+### 2. **Detect stagnation**
397397+398398+Track if the agent is making progress:
399399+400400+```typescript
401401+if (currentState === lastState) {
402402+ stagnantCount++;
403403+ if (stagnantCount >= 3) break;
404404+}
405405+```
406406+407407+### 3. **Use bash exit conditions**
408408+409409+Shell out to authoritative checks (tests, lint, build):
410410+411411+```typescript
412412+const result = spawnSync("npm", ["test"], { encoding: "utf-8" });
413413+if (result.status === 0) break;
414414+```
415415+416416+### 4. **Provide context to agent**
417417+418418+Include iteration number, progress, previous attempts:
419419+420420+```typescript
421421+prompt: `Iteration ${iteration}/${maxIterations}
422422+Fixed so far: ${fixed.join(", ")}
423423+Current errors: ${errorCount}
424424+...`;
425425+```
426426+427427+### 5. **Log everything**
428428+429429+Observability is critical for debugging loops:
430430+431431+```typescript
432432+factory.observe.log("info", "Loop state", {
433433+ iteration,
434434+ errorCount,
435435+ stagnantCount,
436436+ lastChange,
437437+});
438438+```
439439+440440+### 6. **Limit context size**
441441+442442+Truncate large outputs to avoid prompt bloat:
443443+444444+```typescript
445445+const recentOutput = fullOutput.slice(-5000); // Last 5KB
446446+```
447447+448448+### 7. **Allow early exit**
449449+450450+If the goal is achieved, return immediately:
451451+452452+```typescript
453453+if (testsPassing) break;
454454+```
455455+456456+## When to Use Ralph Loop
457457+458458+Good for:
459459+460460+- Fixing lint/type errors iteratively
461461+- Making tests pass one by one
462462+- Implementing PRD tasks sequentially
463463+- Refactoring with incremental validation
464464+- Code generation with iterative refinement
465465+466466+Not ideal for:
467467+468468+- Tasks requiring deep context across iterations
469469+- Complex multi-step reasoning within a single problem
470470+- When the agent needs to remember detailed discussions
471471+- Parallel work (use `Promise.all` with `factory.spawn` instead)
472472+473473+## Advanced: Nested Loops
474474+475475+You can nest Ralph Loops for hierarchical work:
476476+477477+```typescript
478478+const modules = ["src/auth", "src/api", "src/db"];
479479+480480+for (const module of modules) {
481481+ factory.observe.log("info", `Processing module: ${module}`);
482482+483483+ let iteration = 0;
484484+ while (iteration < 10) {
485485+ iteration++;
486486+487487+ const result = await factory.spawn({
488488+ agent: "module-fixer",
489489+ systemPrompt: `Fix issues in ${module}`,
490490+ prompt: "Run checks and fix issues",
491491+ model: "mistral/devstral-2512",
492492+ step: iteration,
493493+ });
494494+495495+ const check = spawnSync("npm", ["run", "lint", module], {
496496+ cwd: process.cwd(),
497497+ encoding: "utf-8",
498498+ });
499499+500500+ if (check.status === 0) break;
501501+ }
502502+}
503503+```
504504+505505+## Summary
506506+507507+The Ralph Loop is a simple but powerful pattern:
508508+509509+- **While loop** around `await factory.spawn()`
510510+- **Filesystem persistence** between iterations
511511+- **Bash exit conditions** for authoritative checks
512512+- **Progress tracking** to detect stagnation
513513+- **Max iterations** for safety
514514+515515+It works because the agent sees fresh context each iteration, making progress incrementally while the filesystem accumulates changes. Perfect for iterative tasks where "run it again" is a valid strategy.
+524
packages/pi-mill/skills/mill-worktree/SKILL.md
···11+---
22+name: mill-worktree
33+description: "Worktree-based parallel development with pi-mill. Use when multiple agents need to edit code simultaneously without conflicts—each agent gets its own working directory via jj workspace or git worktree."
44+---
55+66+# Worktree-Based Parallel Development
77+88+When multiple agents need to edit files simultaneously, they'll conflict if they share a working directory. The solution: give each agent its own worktree. Each has a full working copy but shares the underlying repository. Agents work in complete isolation—own directory, own state, no file conflicts.
99+1010+## Why Worktrees?
1111+1212+- **No merge conflicts during work** — Each agent has its own copy of every file
1313+- **Full toolchain access** — Each worktree can run its own dev server, tests, linter
1414+- **Atomic merges** — Combine results after all agents finish
1515+- **Clean rollback** — Discard a worktree if an agent fails
1616+1717+## Jujutsu (jj) Variant
1818+1919+### Core Commands
2020+2121+```bash
2222+# Create a workspace (like git worktree add)
2323+jj workspace add /tmp/worktree-auth
2424+2525+# List workspaces
2626+jj workspace list
2727+2828+# Remove workspace tracking (doesn't delete files)
2929+jj workspace forget <workspace-name>
3030+3131+# Delete the directory
3232+rm -rf /tmp/worktree-auth
3333+```
3434+3535+### Basic Pattern
3636+3737+```typescript
3838+import { spawnSync } from "node:child_process";
3939+import fs from "node:fs";
4040+4141+const baseCwd = process.cwd();
4242+const tasks = [
4343+ {
4444+ name: "auth",
4545+ prompt: "Implement auth module",
4646+ systemPrompt:
4747+ "You are a software engineer. Implement the requested changes. Run tests to verify your work.",
4848+ },
4949+ {
5050+ name: "api",
5151+ prompt: "Implement API endpoints",
5252+ systemPrompt:
5353+ "You are a software engineer. Implement the requested changes. Run tests to verify your work.",
5454+ },
5555+];
5656+const worktrees: string[] = [];
5757+5858+try {
5959+ // 1. Create worktrees
6060+ for (const t of tasks) {
6161+ const wtPath = `/tmp/pi-worktree-${t.name}-${Date.now()}`;
6262+ worktrees.push(wtPath);
6363+6464+ const result = spawnSync("jj", ["workspace", "add", wtPath], {
6565+ cwd: baseCwd,
6666+ encoding: "utf-8",
6767+ });
6868+6969+ if (result.status !== 0) {
7070+ throw new Error(`Failed to create workspace ${t.name}: ${result.stderr}`);
7171+ }
7272+7373+ factory.observe.log("info", `Created workspace: ${t.name}`, { path: wtPath });
7474+ }
7575+7676+ // 2. Install dependencies in each worktree
7777+ await Promise.all(
7878+ worktrees.map((wt, i) =>
7979+ factory.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.",
8383+ prompt: "Install dependencies in this workspace.",
8484+ model: "cerebras/zai-glm-4.7",
8585+ cwd: wt,
8686+ step: i,
8787+ }),
8888+ ),
8989+ );
9090+9191+ // 3. Dispatch parallel agents
9292+ const results = await Promise.all(
9393+ tasks.map((t, i) =>
9494+ factory.spawn({
9595+ agent: t.name,
9696+ systemPrompt: t.systemPrompt,
9797+ prompt: t.prompt,
9898+ model: "anthropic/claude-opus-4-6",
9999+ cwd: worktrees[i],
100100+ step: i,
101101+ }),
102102+ ),
103103+ );
104104+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", {
109109+ failed: failed.map((r) => r.agent),
110110+ });
111111+ }
112112+113113+ // 5. Merge results back
114114+ const mergeResult = await factory.spawn({
115115+ agent: "merger",
116116+ systemPrompt: `You merge parallel workstream results using jj.
117117+Use 'jj log' to see all changes across workspaces.
118118+Create a merge commit that combines all successful changes.
119119+Resolve any conflicts if they arise.
120120+The main workspace is at: ${baseCwd}`,
121121+ prompt: `Merge changes from ${worktrees.length} parallel workstreams.
122122+Workspaces: ${worktrees.join(", ")}
123123+Failed agents: ${failed.map((r) => r.agent).join(", ") || "none"}
124124+Use jj to combine the changes into the main workspace.`,
125125+ model: "anthropic/claude-sonnet-4-6",
126126+ cwd: baseCwd,
127127+ step: tasks.length,
128128+ });
129129+130130+ // 6. Write summary
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);
135135+} finally {
136136+ // 7. Cleanup — always runs
137137+ for (const wt of worktrees) {
138138+ const name = wt.split("/").pop() || "";
139139+ spawnSync("jj", ["workspace", "forget", name], {
140140+ cwd: baseCwd,
141141+ encoding: "utf-8",
142142+ });
143143+ if (fs.existsSync(wt)) {
144144+ fs.rmSync(wt, { recursive: true, force: true });
145145+ }
146146+ factory.observe.log("info", `Cleaned up workspace`, { path: wt });
147147+ }
148148+}
149149+```
150150+151151+### jj Merge Strategies
152152+153153+After parallel work, you have multiple jj changes to combine. Common approaches:
154154+155155+**Rebase onto each other (sequential):**
156156+157157+```bash
158158+# In the main workspace, rebase changes into a sequence
159159+jj rebase -s <change-auth> -d <change-api>
160160+jj rebase -s <change-ui> -d <change-auth>
161161+```
162162+163163+**Create a merge commit:**
164164+165165+```bash
166166+# Create a new change with multiple parents
167167+jj new <change-auth> <change-api> <change-ui> -m "Merge parallel workstreams"
168168+```
169169+170170+**Squash into one:**
171171+172172+```bash
173173+# If you want a single combined change
174174+jj new <change-auth> <change-api> <change-ui>
175175+jj squash
176176+```
177177+178178+## Git Worktree Variant
179179+180180+For repositories using git instead of jj:
181181+182182+### Core Commands
183183+184184+```bash
185185+# Create a worktree on a new branch
186186+git worktree add /tmp/worktree-auth -b feature/auth
187187+188188+# List worktrees
189189+git worktree list
190190+191191+# Remove worktree (cleans up git metadata)
192192+git worktree remove /tmp/worktree-auth
193193+194194+# Force remove if dirty
195195+git worktree remove --force /tmp/worktree-auth
196196+```
197197+198198+### Basic Pattern
199199+200200+```typescript
201201+import { spawnSync } from "node:child_process";
202202+import fs from "node:fs";
203203+204204+const baseCwd = process.cwd();
205205+const baseBranch = "main";
206206+const tasks = [
207207+ {
208208+ name: "auth",
209209+ prompt: "Implement auth module",
210210+ systemPrompt: "Implement the requested changes. Commit your work when done.",
211211+ },
212212+ {
213213+ name: "payments",
214214+ prompt: "Implement payments",
215215+ systemPrompt: "Implement the requested changes. Commit your work when done.",
216216+ },
217217+];
218218+const worktrees: Array<{ path: string; branch: string }> = [];
219219+220220+try {
221221+ // 1. Create worktrees with dedicated branches
222222+ for (const t of tasks) {
223223+ const branch = `worktree/${t.name}-${Date.now()}`;
224224+ const wtPath = `/tmp/pi-worktree-${t.name}-${Date.now()}`;
225225+ worktrees.push({ path: wtPath, branch });
226226+227227+ const result = spawnSync("git", ["worktree", "add", wtPath, "-b", branch, baseBranch], {
228228+ cwd: baseCwd,
229229+ encoding: "utf-8",
230230+ });
231231+232232+ if (result.status !== 0) {
233233+ throw new Error(`Failed to create worktree ${t.name}: ${result.stderr}`);
234234+ }
235235+236236+ factory.observe.log("info", `Created worktree: ${t.name}`, { path: wtPath, branch });
237237+ }
238238+239239+ // 2. Install dependencies
240240+ await Promise.all(
241241+ worktrees.map((wt, i) =>
242242+ factory.spawn({
243243+ agent: "installer",
244244+ systemPrompt: "Install project dependencies.",
245245+ prompt: "Run the install command for this project (npm install, etc.)",
246246+ model: "cerebras/zai-glm-4.7",
247247+ cwd: wt.path,
248248+ step: i,
249249+ }),
250250+ ),
251251+ );
252252+253253+ // 3. Dispatch agents
254254+ const results = await Promise.all(
255255+ tasks.map((t, i) =>
256256+ factory.spawn({
257257+ agent: t.name,
258258+ systemPrompt: t.systemPrompt,
259259+ prompt: `${t.prompt}\n\nCommit your changes to the current branch when complete.`,
260260+ model: "openai-codex/gpt-5.3-codex",
261261+ cwd: worktrees[i].path,
262262+ step: i,
263263+ }),
264264+ ),
265265+ );
266266+267267+ // 4. Merge branches back
268268+ const successful = results
269269+ .map((r, i) => ({ result: r, worktree: worktrees[i] }))
270270+ .filter(({ result }) => result.exitCode === 0);
271271+272272+ await factory.spawn({
273273+ agent: "merger",
274274+ systemPrompt: `You merge git branches from parallel workstreams.
275275+Merge each feature branch into ${baseBranch}.
276276+Handle conflicts if they arise. Prefer keeping both changes when possible.`,
277277+ prompt: `Merge these branches into ${baseBranch}:
278278+${successful.map(({ worktree }) => `- ${worktree.branch}`).join("\n")}`,
279279+ model: "anthropic/claude-sonnet-4-6",
280280+ cwd: baseCwd,
281281+ step: tasks.length,
282282+ });
283283+} finally {
284284+ // 5. Cleanup
285285+ for (const wt of worktrees) {
286286+ spawnSync("git", ["worktree", "remove", "--force", wt.path], {
287287+ cwd: baseCwd,
288288+ encoding: "utf-8",
289289+ });
290290+ spawnSync("git", ["branch", "-D", wt.branch], {
291291+ cwd: baseCwd,
292292+ encoding: "utf-8",
293293+ });
294294+ if (fs.existsSync(wt.path)) {
295295+ fs.rmSync(wt.path, { recursive: true, force: true });
296296+ }
297297+ }
298298+}
299299+```
300300+301301+## Dependency Installation
302302+303303+Each worktree needs its own `node_modules` (or equivalent). Common patterns:
304304+305305+```typescript
306306+// Detect package manager and install
307307+function installDeps(cwd: string): { status: number; stderr: string } {
308308+ if (fs.existsSync(`${cwd}/bun.lockb`)) {
309309+ return spawnSync("bun", ["install"], { cwd, encoding: "utf-8" });
310310+ } else if (fs.existsSync(`${cwd}/pnpm-lock.yaml`)) {
311311+ return spawnSync("pnpm", ["install", "--frozen-lockfile"], { cwd, encoding: "utf-8" });
312312+ } else if (fs.existsSync(`${cwd}/yarn.lock`)) {
313313+ return spawnSync("yarn", ["install", "--frozen-lockfile"], { cwd, encoding: "utf-8" });
314314+ } else {
315315+ return spawnSync("npm", ["ci"], { cwd, encoding: "utf-8" });
316316+ }
317317+}
318318+```
319319+320320+Or let each agent handle it — the installer agent in the examples above will figure out the right command.
321321+322322+## Advanced: Fan-Out with Worktrees + Synthesize
323323+324324+Combine the worktree pattern with fan-out-then-synthesize:
325325+326326+```typescript
327327+import { spawnSync } from "node:child_process";
328328+import fs from "node:fs";
329329+330330+const baseCwd = process.cwd();
331331+const worktrees: string[] = [];
332332+333333+const tasks = [
334334+ {
335335+ name: "api",
336336+ prompt: "Add pagination to /api/users endpoint",
337337+ systemPrompt: "You are a backend engineer.",
338338+ },
339339+ {
340340+ name: "ui",
341341+ prompt: "Add pagination controls to the users table",
342342+ systemPrompt: "You are a frontend engineer.",
343343+ },
344344+ {
345345+ name: "tests",
346346+ prompt: "Write integration tests for paginated user listing",
347347+ systemPrompt: "You are a QA engineer.",
348348+ },
349349+];
350350+351351+try {
352352+ // Setup worktrees
353353+ for (const t of tasks) {
354354+ const wt = `/tmp/pi-wt-${t.name}-${Date.now()}`;
355355+ worktrees.push(wt);
356356+ spawnSync("jj", ["workspace", "add", wt], { cwd: baseCwd, encoding: "utf-8" });
357357+ }
358358+359359+ // Install deps in parallel
360360+ await Promise.all(
361361+ worktrees.map((wt, i) =>
362362+ factory.spawn({
363363+ agent: "installer",
364364+ systemPrompt: "Install deps.",
365365+ prompt: "npm install",
366366+ model: "cerebras/zai-glm-4.7",
367367+ cwd: wt,
368368+ step: i,
369369+ }),
370370+ ),
371371+ );
372372+373373+ // Parallel implementation
374374+ const results = await Promise.all(
375375+ tasks.map((t, i) =>
376376+ factory.spawn({
377377+ agent: t.name,
378378+ systemPrompt: t.systemPrompt,
379379+ prompt: t.prompt,
380380+ model: "anthropic/claude-opus-4-6",
381381+ cwd: worktrees[i],
382382+ step: i,
383383+ }),
384384+ ),
385385+ );
386386+387387+ // Synthesize — merge and verify
388388+ const context = results.map((r) => `[${r.agent}]\n${r.text}`).join("\n\n");
389389+ const synthesis = await factory.spawn({
390390+ agent: "integrator",
391391+ systemPrompt: `You integrate parallel workstreams.
392392+1. Use jj to merge all workspace changes into the main workspace.
393393+2. Resolve any conflicts.
394394+3. Run the full test suite to verify integration.
395395+4. Fix any integration issues.
396396+Main workspace: ${baseCwd}`,
397397+ prompt: `Integrate these parallel changes:\n\n${context}`,
398398+ model: "anthropic/claude-opus-4-6",
399399+ cwd: baseCwd,
400400+ step: tasks.length,
401401+ });
402402+} finally {
403403+ for (const wt of worktrees) {
404404+ const name = wt.split("/").pop() || "";
405405+ spawnSync("jj", ["workspace", "forget", name], { cwd: baseCwd, encoding: "utf-8" });
406406+ if (fs.existsSync(wt)) fs.rmSync(wt, { recursive: true, force: true });
407407+ }
408408+}
409409+```
410410+411411+## Best Practices
412412+413413+### 1. **Always clean up in `finally`**
414414+415415+Worktrees leak disk space and repository state if not cleaned:
416416+417417+```typescript
418418+try {
419419+ // ... create worktrees, run agents
420420+} finally {
421421+ // ... forget workspaces, delete directories
422422+}
423423+```
424424+425425+### 2. **Use `/tmp` for worktree paths**
426426+427427+Keeps worktrees out of your project directory and OS handles cleanup on reboot:
428428+429429+```typescript
430430+const wtPath = `/tmp/pi-worktree-${name}-${Date.now()}`;
431431+```
432432+433433+### 3. **Include timestamps in paths**
434434+435435+Prevents collisions if you run the same program twice:
436436+437437+```typescript
438438+const wtPath = `/tmp/pi-wt-${name}-${Date.now()}`;
439439+```
440440+441441+### 4. **Install deps before dispatching agents**
442442+443443+Agents shouldn't waste tokens figuring out dependency installation. Do it as a setup step:
444444+445445+```typescript
446446+// Dedicated install step
447447+await Promise.all(
448448+ worktrees.map((wt) =>
449449+ factory.spawn({
450450+ agent: "installer",
451451+ systemPrompt: "Install dependencies.",
452452+ prompt: "npm install",
453453+ model: "cerebras/zai-glm-4.7",
454454+ cwd: wt,
455455+ }),
456456+ ),
457457+);
458458+459459+// Then dispatch real work
460460+await Promise.all(
461461+ tasks.map((t, i) =>
462462+ factory.spawn({
463463+ agent: t.name,
464464+ systemPrompt: t.systemPrompt,
465465+ prompt: t.prompt,
466466+ model: "anthropic/claude-opus-4-6",
467467+ cwd: worktrees[i],
468468+ }),
469469+ ),
470470+);
471471+```
472472+473473+### 5. **Scope agent work narrowly**
474474+475475+Each agent should work on a well-defined, non-overlapping area. If two agents edit the same files, merging becomes painful:
476476+477477+```
478478+Agent A: "Implement auth module in src/auth/"
479479+Agent B: "Implement payments in src/payments/"
480480+NOT: "Refactor the app" — too broad, will conflict
481481+```
482482+483483+### 6. **Verify after merge**
484484+485485+Always run tests/lint after merging parallel changes:
486486+487487+```typescript
488488+const verify = spawnSync("npm", ["test"], { cwd: baseCwd, encoding: "utf-8" });
489489+if (verify.status !== 0) {
490490+ // Fix integration issues
491491+}
492492+```
493493+494494+### 7. **Track worktree count**
495495+496496+Each worktree is a full working copy. On large repos, 5+ simultaneous worktrees can use significant disk space. Start with 2-3 parallel agents and scale up.
497497+498498+## When to Use Worktrees
499499+500500+Good for:
501501+502502+- Implementing multiple independent features in parallel
503503+- Parallel refactoring of separate modules
504504+- Running different test suites simultaneously
505505+- Any task where agents would otherwise conflict on files
506506+507507+Not ideal for:
508508+509509+- Tasks that heavily overlap in the same files
510510+- Read-only analysis (just use `Promise.all` with `factory.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+514514+## Summary
515515+516516+The worktree pattern gives each agent full isolation:
517517+518518+1. **Create** — `jj workspace add` or `git worktree add`
519519+2. **Install** — Dependencies in each worktree
520520+3. **Dispatch** — Parallel agents via `Promise.all`, each with own `cwd`
521521+4. **Merge** — Combine changes with jj/git
522522+5. **Cleanup** — Forget workspaces, delete directories
523523+524524+Agents never step on each other's toes. The merge step is where conflicts surface — and by scoping work to non-overlapping areas, you minimize that pain.