···11+id: no-math-random-outside-random
22+language: TypeScript
33+severity: error
44+message: Use injected Random service instead of Math.random in internal runtime.
55+rule:
66+ pattern: "Math.random()"
+12
.ast-grep/rules/no-node-imports.yml
···11+id: no-node-imports
22+language: TypeScript
33+severity: error
44+message: "Direct node: imports are disallowed in application source modules."
55+files:
66+ - "**/src/**/*.ts"
77+rule:
88+ any:
99+ - pattern: "import $A from 'node:$B'"
1010+ - pattern: 'import $A from "node:$B"'
1111+ - pattern: "import { $$$A } from 'node:$B'"
1212+ - pattern: 'import { $$$A } from "node:$B"'
···11+id: no-promise-outside-public
22+language: TypeScript
33+severity: error
44+message: Promise contracts are allowed only in boundary APIs.
55+ignores:
66+ - "**/src/public/**"
77+ - "**/*.api.ts"
88+ - "**/*.d.ts"
99+rule:
1010+ any:
1111+ - pattern: "Promise<$T>"
1212+ - pattern: "new Promise($A)"
+14
.ast-grep/rules/no-public-import-internal.yml
···11+id: no-public-import-internal
22+language: TypeScript
33+severity: error
44+message: Public modules must not import private internal/runtime/domain modules directly.
55+files:
66+ - "**/src/public/**/*.ts"
77+rule:
88+ any:
99+ - pattern: "import $A from '../internal/$B'"
1010+ - pattern: "import $A from '../runtime/$B'"
1111+ - pattern: "import $A from '../domain/$B'"
1212+ - pattern: "import { $$$A } from '../internal/$B'"
1313+ - pattern: "import { $$$A } from '../runtime/$B'"
1414+ - pattern: "import { $$$A } from '../domain/$B'"
+6
.ast-grep/rules/no-raw-promise.yml
···11+id: no-raw-promise
22+language: TypeScript
33+severity: error
44+message: Raw Promise construction is disallowed; use Effect abstractions.
55+rule:
66+ pattern: "new Promise($A)"
···11+# mill v0 Architecture & Boundaries (Sections 8–18)
22+33+_Source: `SPEC.md` (verbatim split for cedar-style docs tree)._
44+55+## 8) Boundary contracts: public Promise API, internal Effect core
66+77+Rule of thumb (strict):
88+99+- **User-exposed surface**: Promise-based API + interfaces are allowed.
1010+- **Everything else**: Effect-first (`Effect`, `Stream`, `Layer`) + Schema-defined domain types.
1111+1212+Concretely:
1313+1414+- Public boundary (`src/public/**`, ambient `*.d.ts`):
1515+ - can expose `Promise<T>`
1616+ - can use `interface` for ergonomics
1717+- Internal/domain/runtime (`src/internal/**`, `src/domain/**`, `src/runtime/**`):
1818+ - no public Promise contracts
1919+ - domain shapes must be defined by `@effect/schema/Schema`
2020+ - no interface-based domain modelling
2121+2222+Effect contracts used internally:
2323+2424+- effects: `Effect.Effect<A, E, R>`
2525+- streams: `Stream.Stream<A, E, R>`
2626+- layers: `Layer.Layer<ROut, E, RIn>`
2727+- queue/pubsub for event fanout
2828+- schemas via `@effect/schema/Schema`
2929+3030+### 8.1 Domain schemas (representative)
3131+3232+```ts
3333+import * as Schema from "@effect/schema/Schema";
3434+3535+export const RunId = Schema.String.pipe(Schema.brand("RunId"));
3636+export type RunId = Schema.Schema.Type<typeof RunId>;
3737+3838+export const SpawnId = Schema.String.pipe(Schema.brand("SpawnId"));
3939+export type SpawnId = Schema.Schema.Type<typeof SpawnId>;
4040+4141+export const RunStatus = Schema.Literal("pending", "running", "complete", "failed", "cancelled");
4242+export type RunStatus = Schema.Schema.Type<typeof RunStatus>;
4343+4444+export const SpawnOptions = Schema.Struct({
4545+ agent: Schema.NonEmptyString,
4646+ systemPrompt: Schema.NonEmptyString,
4747+ prompt: Schema.NonEmptyString,
4848+ model: Schema.optional(Schema.NonEmptyString),
4949+});
5050+5151+export type SpawnOptions = Schema.Schema.Type<typeof SpawnOptions>;
5252+5353+export const SpawnResult = Schema.Struct({
5454+ text: Schema.String,
5555+ sessionRef: Schema.NonEmptyString,
5656+ agent: Schema.NonEmptyString,
5757+ model: Schema.NonEmptyString,
5858+ driver: Schema.NonEmptyString,
5959+ exitCode: Schema.Number,
6060+ stopReason: Schema.optional(Schema.String),
6161+ errorMessage: Schema.optional(Schema.String),
6262+});
6363+6464+export type SpawnResult = Schema.Schema.Type<typeof SpawnResult>;
6565+```
6666+6767+### 8.2 Error model
6868+6969+All errors are tagged Effect data errors.
7070+7171+```ts
7272+class ConfigError extends Data.TaggedError("ConfigError")<{ message: string }> {}
7373+class RunNotFoundError extends Data.TaggedError("RunNotFoundError")<{ runId: string }> {}
7474+class DriverError extends Data.TaggedError("DriverError")<{ driver: string; message: string }> {}
7575+class ProgramExecutionError extends Data.TaggedError("ProgramExecutionError")<{
7676+ runId: string;
7777+ message: string;
7878+}> {}
7979+class PersistenceError extends Data.TaggedError("PersistenceError")<{
8080+ path: string;
8181+ message: string;
8282+}> {}
8383+```
8484+8585+### 8.3 Core services
8686+8787+Service contracts may use `interface`, but only for **capabilities** (methods), not domain data modelling. Their methods remain Effect-typed.
8888+8989+```ts
9090+interface RunStore {
9191+ create(meta: RunMeta): Effect.Effect<void, PersistenceError>;
9292+ appendEvent(runId: RunId, event: MillEvent): Effect.Effect<void, PersistenceError>;
9393+ setStatus(runId: RunId, status: RunStatus): Effect.Effect<void, PersistenceError>;
9494+ setResult(runId: RunId, result: RunResult): Effect.Effect<void, PersistenceError>;
9595+ getRun(runId: RunId): Effect.Effect<RunRecord, RunNotFoundError | PersistenceError>;
9696+ listRuns(filter?: RunFilter): Effect.Effect<ReadonlyArray<RunRecord>, PersistenceError>;
9797+}
9898+9999+interface Driver {
100100+ readonly name: string;
101101+ readonly spawn: (
102102+ input: DriverSpawnInput,
103103+ ) => Effect.Effect<DriverSpawnHandle, DriverError, Scope.Scope>;
104104+}
105105+106106+interface DriverSpawnHandle {
107107+ readonly events: Stream.Stream<DriverEvent, DriverError>;
108108+ readonly raw: Stream.Stream<Uint8Array, never>;
109109+ readonly result: Effect.Effect<SpawnResult, DriverError>;
110110+ readonly cancel: Effect.Effect<void, never>;
111111+}
112112+113113+interface Executor {
114114+ readonly name: string;
115115+ readonly runProgram: (
116116+ input: ProgramRunInput,
117117+ ) => Effect.Effect<ProgramRunHandle, ProgramExecutionError, Scope.Scope>;
118118+}
119119+120120+interface ProgramRunHandle {
121121+ readonly events: Stream.Stream<MillEvent, ProgramExecutionError>;
122122+ readonly result: Effect.Effect<RunResult, ProgramExecutionError>;
123123+ readonly cancel: Effect.Effect<void, never>;
124124+}
125125+```
126126+127127+### 8.4 Engine service
128128+129129+```ts
130130+interface MillEngine {
131131+ submit(
132132+ input: SubmitRunInput,
133133+ ): Effect.Effect<SubmitRunOutput, ConfigError | PersistenceError | ProgramExecutionError>;
134134+ runSync(
135135+ input: SubmitRunInput,
136136+ ): Effect.Effect<RunResult, ConfigError | PersistenceError | ProgramExecutionError>;
137137+ status(runId: RunId): Effect.Effect<RunRecord, RunNotFoundError | PersistenceError>;
138138+ wait(
139139+ runId: RunId,
140140+ timeout: Duration.DurationInput,
141141+ ): Effect.Effect<RunRecord, RunNotFoundError | PersistenceError>;
142142+ watch(runId: RunId): Stream.Stream<MillEvent, RunNotFoundError | PersistenceError>;
143143+ cancel(runId: RunId): Effect.Effect<void, RunNotFoundError | PersistenceError>;
144144+ inspect(ref: RunOrSpawnRef): Effect.Effect<InspectResult, RunNotFoundError | PersistenceError>;
145145+}
146146+```
147147+148148+### 8.5 Effect runtime primitives used in mill
149149+150150+`mill` implementation uses these Effect modules as first-class building blocks:
151151+152152+- `Effect.gen`, `Effect.scoped`, `Effect.acquireRelease`, `Effect.timeout`, `Effect.retry`, `Effect.interrupt`
153153+- `Fiber` / `FiberSet` for supervised detached run workers
154154+- `Queue` for per-run ordered event buffering
155155+- `PubSub` for fanout to multiple live watchers
156156+- `Stream` for driver output decoding and watch subscriptions
157157+- `Ref` / `SynchronizedRef` for in-memory run registry snapshots
158158+- `Layer` + `Context.Tag` for all services (`RunStore`, `DriverRegistry`, `ExecutorRegistry`, `Clock`, etc.)
159159+- `Runtime` for bridging program-facing Promise API (`mill.spawn(): Promise<...>`) to internal Effects via **only** `Runtime.runPromise`
160160+161161+Target platform services:
162162+163163+- `@effect/platform/Command`
164164+- `@effect/platform/FileSystem`
165165+- `@effect/platform/Path`
166166+- `@effect/platform/Terminal`
167167+- `@effect/platform-bun` runtime layer for Bun-backed implementations
168168+169169+### 8.6 Package baseline (Effect v4 target)
170170+171171+`mill` pins to Effect v4-compatible package line:
172172+173173+```json
174174+{
175175+ "dependencies": {
176176+ "effect": "^4.x",
177177+ "@effect/platform": "^1.x",
178178+ "@effect/platform-bun": "^1.x",
179179+ "@effect/schema": "^1.x"
180180+ }
181181+}
182182+```
183183+184184+(Exact minor versions are implementation-time decisions; API usage must stay within documented stable modules.)
185185+186186+### 8.7 File layout + naming (boundary is visible in filenames)
187187+188188+```text
189189+src/
190190+ public/
191191+ mill.api.ts # Promise-based user API
192192+ discovery.api.ts # Promise-based CLI/discovery payload builders
193193+ types.ts # user-facing interfaces allowed
194194+ domain/
195195+ run.schema.ts # Schema-based domain models (no interfaces)
196196+ spawn.schema.ts
197197+ internal/
198198+ engine.effect.ts # internal Effect programs/services
199199+ run-store.effect.ts
200200+ driver.effect.ts
201201+ runtime/
202202+ worker.effect.ts
203203+```
204204+205205+Naming rules:
206206+207207+- `*.api.ts` => user boundary (Promise + interfaces allowed)
208208+- `*.schema.ts` => domain data contracts (`Schema` + `Schema.Type` exports)
209209+- `*.effect.ts` => internal runtime/effectful orchestration code
210210+211211+If a file defines domain entities and is not `*.schema.ts`, it is considered a spec violation.
212212+213213+### 8.8 Quick classification examples
214214+215215+Allowed (public boundary):
216216+217217+```ts
218218+// src/public/mill.api.ts
219219+export interface Mill {
220220+ spawn(input: SpawnInput): Promise<SpawnOutput>;
221221+}
222222+```
223223+224224+Required (internal):
225225+226226+```ts
227227+// src/internal/engine.effect.ts
228228+export const submit = (
229229+ input: SubmitRunInput,
230230+): Effect.Effect<SubmitRunOutput, SubmitError, RunStore | DriverRegistry> =>
231231+ Effect.gen(function* () {
232232+ // ...
233233+ });
234234+```
235235+236236+Required (domain):
237237+238238+```ts
239239+// src/domain/run.schema.ts
240240+export const RunRecord = Schema.Struct({
241241+ id: RunId,
242242+ status: RunStatus,
243243+ startedAt: Schema.String,
244244+});
245245+export type RunRecord = Schema.Schema.Type<typeof RunRecord>;
246246+```
247247+248248+Disallowed:
249249+250250+```ts
251251+// src/domain/run.ts
252252+export interface RunRecord {
253253+ // lint error
254254+ id: string;
255255+ status: string;
256256+}
257257+```
258258+259259+### 8.9 Promise bridge and decode boundaries
260260+261261+- Allowed bridge:
262262+ - `Runtime.runPromise` only
263263+- Disallowed bridges:
264264+ - `Effect.runPromise`
265265+ - `Effect.runPromiseExit`
266266+ - `Runtime.runPromiseExit`
267267+- Bridge location:
268268+ - boundary adapters only (`src/public/**`, CLI boundary entrypoints)
269269+270270+Decode policy:
271271+272272+- `JSON.parse` is only allowed in codec/schema decoding modules (`*.codec.ts`, `*.schema.ts`).
273273+- Parsed values must be validated with `Schema.decodeUnknown*` before use.
274274+- Ad-hoc parsing in engine/runtime/business modules is disallowed.
275275+276276+## 9) Event model
277277+278278+Two tiers:
279279+280280+### Tier 1 (structured, persisted)
281281+282282+Required core events:
283283+284284+- `run:start`
285285+- `run:status`
286286+- `run:complete`
287287+- `run:failed`
288288+- `run:cancelled`
289289+- `spawn:start`
290290+- `spawn:milestone`
291291+- `spawn:tool_call`
292292+- `spawn:error`
293293+- `spawn:complete`
294294+- `spawn:cancelled`
295295+296296+All tier-1 events must include:
297297+298298+- `schemaVersion` (integer, starts at `1`)
299299+- `runId`
300300+- event `type` (discriminant)
301301+- monotonic sequence number
302302+- timestamp
303303+304304+Encoding/decoding requirements:
305305+306306+- persisted event payloads are defined as a Schema discriminated union
307307+- writers encode from typed values
308308+- readers decode with `Schema.decodeUnknown*`
309309+- unknown schema versions are surfaced as typed decode errors
310310+311311+Tier 1 is written to `events.ndjson` and is the source for `watch`, `inspect`, and extensions.
312312+313313+### Tier 1 lifecycle invariants
314314+315315+Exactly one terminal event is allowed per run and per spawn:
316316+317317+- run terminal set: `run:complete | run:failed | run:cancelled`
318318+- spawn terminal set: `spawn:complete | spawn:error | spawn:cancelled`
319319+320320+Transition table:
321321+322322+```text
323323+run: pending -> running -> complete|failed|cancelled
324324+spawn: pending -> running -> complete|error|cancelled
325325+```
326326+327327+Terminal states have no outgoing transitions.
328328+`mill wait` resolves on first observed terminal event and treats additional terminal events as invariant violations.
329329+330330+### Tier 2 (raw passthrough, ephemeral)
331331+332332+- full raw bytes/text from driver process or remote stream
333333+- available live via `watch --raw`
334334+- not persisted by engine
335335+336336+## 10) Driver architecture
337337+338338+### 10.1 Generic driver + codec split
339339+340340+Core does not encode vendor semantics.
341341+342342+- `processDriver(...)` and `httpDriver(...)` are generic factories.
343343+- `codec` parses native output into `DriverEvent` + `SpawnResult`.
344344+345345+```ts
346346+interface DriverCodec {
347347+ readonly decodeEvent: (
348348+ chunk: Uint8Array,
349349+ ) => Effect.Effect<ReadonlyArray<DriverEvent>, CodecError>;
350350+ readonly decodeFinal: (
351351+ aggregate: ReadonlyArray<Uint8Array>,
352352+ ) => Effect.Effect<SpawnResult, CodecError>;
353353+ readonly modelCatalog: Effect.Effect<ReadonlyArray<string>, never>;
354354+}
355355+```
356356+357357+### 10.2 Process driver execution (Bun-backed via Effect)
358358+359359+Driver process spawning MUST be implemented with Effect platform command APIs and Bun context layer.
360360+361361+Implementation pattern:
362362+363363+1. Build command (`Command.make(command, ...args)`)
364364+2. Apply env/cwd/stdin
365365+3. Start process via platform `Command` executor
366366+4. Consume stdout/stderr as `Stream`
367367+5. Parse via codec to structured events
368368+6. Await exit code and final decode
369369+370370+Command safety requirements:
371371+372372+- commands must be built as arg vectors (`Command.make(cmd, ...args)`)
373373+- `sh -lc`, `bash -lc`, and interpolated shell command strings are disallowed
374374+- untrusted/user-provided values must flow as args, never shell source text
375375+376376+The implementation layer includes `@effect/platform-bun` runtime context so process operations are backed by Bun spawn internally while preserving typed Effect semantics.
377377+378378+## 11) Executor architecture
379379+380380+### 11.1 Direct executor (default)
381381+382382+- executes the TS program using Bun in local environment
383383+- injects `globalThis.mill`
384384+- enforces scoped lifecycle and cancellation
385385+386386+### 11.2 VM executor (optional)
387387+388388+- same engine contracts
389389+- runs program in sandboxed runtime (docker/firecracker/gvisor)
390390+391391+Executor has no driver knowledge.
392392+393393+## 12) Program API injected into runtime
394394+395395+This is a **user-facing boundary**, so Promise-returning signatures are intentional.
396396+397397+```ts
398398+declare global {
399399+ const mill: {
400400+ spawn(opts: SpawnOptions): Promise<SpawnResult>;
401401+ // extension APIs merged in at runtime
402402+ [key: string]: unknown;
403403+ };
404404+}
405405+```
406406+407407+Runtime validation:
408408+409409+- `systemPrompt` must be non-empty
410410+- `prompt` must be non-empty
411411+- `agent` must be non-empty
412412+413413+Behavior:
414414+415415+- each `spawn` allocates `spawnId`
416416+- engine emits `spawn:start`
417417+- driver handle streams events
418418+- engine maps to tier-1 events and persists
419419+- resolve final `SpawnResult`
420420+421421+## 13) Background worker process
422422+423423+Internal worker command (private API):
424424+425425+```bash
426426+mill _worker --run-id <id> --program <abs-path> --config <resolved-config> [--driver ...] [--executor ...]
427427+```
428428+429429+Worker responsibilities:
430430+431431+1. mark run `running`
432432+2. execute program through engine
433433+3. append tier-1 events
434434+4. write final `result.json`
435435+5. mark terminal status exactly once (idempotent finalize)
436436+437437+CLI `run` command only submits and detaches worker (unless `--sync`).
438438+439439+## 14) Extensions
440440+441441+```ts
442442+interface Extension {
443443+ readonly name: string;
444444+ readonly setup?: (ctx: ExtensionContext) => Effect.Effect<void, ExtensionError, Scope.Scope>;
445445+ readonly onEvent?: (
446446+ event: MillEvent,
447447+ ctx: ExtensionContext,
448448+ ) => Effect.Effect<void, ExtensionError>;
449449+ readonly api?: Record<string, (...args: ReadonlyArray<unknown>) => Promise<unknown>>;
450450+}
451451+```
452452+453453+Rules:
454454+455455+- Extension failure does not crash engine by default; failure becomes `extension:error` event.
456456+- `api` contributions are namespaced into injected `mill` object.
457457+- Extension hooks (`setup`, `onEvent`) stay Effect-native.
458458+- Extension `api` is user-facing, therefore Promise-based by contract.
459459+- Promise adapters for extension API must use `Runtime.runPromise` as the only bridge.
460460+461461+## 15) Observers
462462+463463+Observers consume tier-1 stream (and optionally tier-2 live raw stream):
464464+465465+- `mill watch`
466466+- `mill inspect`
467467+- future TUI/web UI
468468+- automation reading NDJSON
469469+470470+Observers are read-only; they do not mutate engine state.
471471+472472+## 16) `inspect --session`
473473+474474+`mill inspect <runId>.<spawnId> --session` resolves the spawn `sessionRef` via the originating driver and opens or prints a pointer to full native session history.
475475+476476+Engine never normalizes full transcript ownership.
477477+478478+## 17) Cancellation semantics
479479+480480+`mill cancel <runId>`:
481481+482482+1. mark run as cancelling
483483+2. interrupt worker fiber
484484+3. propagate cancel to all live spawn handles (`handle.cancel`)
485485+4. append `run:cancelled` (only if run is not already terminal)
486486+5. mark terminal state `cancelled`
487487+488488+Cancellation must be interruption-safe and idempotent.
489489+If run is already terminal, cancellation is a no-op.
490490+491491+## 18) SDK contract (`@mill/core`)
492492+493493+```ts
494494+interface CreateEngineInput {
495495+ readonly config: MillConfig;
496496+}
497497+498498+declare const createEngine: (
499499+ input: CreateEngineInput,
500500+) => Effect.Effect<MillEngine, ConfigError, Scope.Scope>;
501501+```
502502+503503+CLI is a thin wrapper around SDK service methods.
504504+505505+### 18.1 Package export boundary
506506+507507+`package.json` exports must expose only public entrypoints (`src/public/**` build outputs).
508508+509509+- consumers must not import `src/internal/**` / `src/runtime/**` directly
510510+- internal modules are considered private implementation detail
511511+- CI should fail if an internal path is exported
+6
docs/exec-plans/README.md
···11+# Execution Plans Index
22+33+- Active: `active/`
44+- Completed: `completed/`
55+66+Use active plans for current implementation tracks. Move finished plans to completed with outcomes + follow-ups.