diff --git a/examples/autopilot-quickstart/README.md b/examples/autopilot-quickstart/README.md
new file mode 100644
index 0000000..0a27288
--- /dev/null
+++ b/examples/autopilot-quickstart/README.md
@@ -0,0 +1,36 @@
+# @gemstack/example-autopilot-quickstart
+
+A runnable, end-to-end quickstart for [`@gemstack/ai-autopilot`](../../packages/ai-autopilot) — the four layers of the epic composed into one "build a feature" flow:
+
+```
+personas → Supervisor → runner (sandbox) → surfaces
+```
+
+A lead planner decomposes the task **"Add a paginated Orders page backed by an orders table"** and routes each subtask to a stack-aware **persona** (`universal-orm-modeler`, `vike-page-builder`, `ui-intent-designer`). The **Supervisor** dispatches them; each persona worker acts inside a **runner** sandbox — writing Vike/ORM files through `runnerTools` — and progress is rendered through the **surfaces** (a terminal sink for live output, plus a background handle exposing events + result).
+
+It runs **offline**: `AiFake` scripts the model, so there's no API key and the output is deterministic.
+
+## Run it
+
+```bash
+pnpm install && pnpm build # from the repo root, to build the packages first
+pnpm --filter @gemstack/example-autopilot-quickstart start
+```
+
+You'll see the live plan, the files written into the sandbox, a build + preview URL, and the synthesized result.
+
+## Test it
+
+```bash
+pnpm --filter @gemstack/example-autopilot-quickstart test
+```
+
+## Going real
+
+The only fakes here are `AiFake` (the model) and `FakeRunner` (the sandbox). To run it for real, drop `AiFake` and give the personas real model strings, and swap `FakeRunner` for a real runner adapter (a `FlueRunner`, WebContainer, or Docker sandbox) — the `Runner` interface is the same, so nothing else in the flow changes.
+
+## What each file shows
+
+- **`src/autopilot.ts`** — the composition: `personaWorkers` + `runnerTools`, `agentPlanner` fed the `personaRoster`, a `Supervisor`, and `launchAutopilot` + `terminalSink` for the surfaces.
+- **`src/main.ts`** — the runnable demo.
+- **`src/autopilot.test.ts`** — asserts the four layers actually compose (plan routes by persona, files land in the sandbox, surfaces capture events).
diff --git a/examples/autopilot-quickstart/package.json b/examples/autopilot-quickstart/package.json
new file mode 100644
index 0000000..bff0539
--- /dev/null
+++ b/examples/autopilot-quickstart/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@gemstack/example-autopilot-quickstart",
+ "version": "0.0.0",
+ "private": true,
+ "description": "Runnable end-to-end quickstart for @gemstack/ai-autopilot: personas + Supervisor + runner + surfaces composed into one build-a-feature flow, offline via AiFake.",
+ "type": "module",
+ "scripts": {
+ "typecheck": "tsc --noEmit",
+ "test": "tsc -p tsconfig.test.json && cd dist-test && node --test",
+ "clean": "rm -rf dist-test",
+ "start": "tsx src/main.ts"
+ },
+ "dependencies": {
+ "@gemstack/ai-autopilot": "workspace:^",
+ "@gemstack/ai-sdk": "workspace:^",
+ "zod": "^4.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "tsx": "^4.19.0",
+ "typescript": "^5.4.0"
+ }
+}
diff --git a/examples/autopilot-quickstart/src/autopilot.test.ts b/examples/autopilot-quickstart/src/autopilot.test.ts
new file mode 100644
index 0000000..daf7d82
--- /dev/null
+++ b/examples/autopilot-quickstart/src/autopilot.test.ts
@@ -0,0 +1,41 @@
+import { describe, it } from 'node:test'
+import assert from 'node:assert/strict'
+import { runQuickstart, TASK } from './autopilot.js'
+
+describe('autopilot quickstart: the four layers compose end-to-end', () => {
+ it('plans by persona, dispatches, acts in the sandbox, and surfaces progress', async () => {
+ const lines: string[] = []
+ const result = await runQuickstart(line => lines.push(line))
+
+ // Supervisor: three subtasks, each routed to a stack persona, all succeeded.
+ assert.equal(result.run.plan.length, 3)
+ assert.deepEqual(
+ result.run.plan.map(s => s.worker).sort(),
+ ['ui-intent-designer', 'universal-orm-modeler', 'vike-page-builder'],
+ )
+ assert.ok(result.run.results.every(r => r.ok), 'every subtask succeeded')
+
+ // Runner: each persona worker wrote its file into the sandbox via runnerTools.
+ assert.ok('database/schema.ts' in result.files)
+ assert.ok('pages/orders/+Page.jsx' in result.files)
+ assert.match(result.files['database/schema.ts']!, /orders/)
+ // the seed file is still there
+ assert.ok('package.json' in result.files)
+
+ // Runner: the post-build exec ran and a preview URL was exposed.
+ assert.equal(result.build.exitCode, 0)
+ assert.match(result.build.stdout, /built/)
+ assert.match(result.previewUrl, /:5173$/)
+
+ // Surfaces: the terminal sink printed a plan line and per-subtask lines;
+ // the background handle captured the same events.
+ assert.ok(lines.some(l => l.includes('plan:')), 'terminal printed the plan')
+ assert.ok(lines.some(l => l.includes('✓')), 'terminal printed a completed subtask')
+ assert.equal(result.events[0]!.type, 'plan')
+ assert.equal(result.events.at(-1)!.type, 'synthesize')
+ })
+
+ it('exposes the task constant for the runnable demo', () => {
+ assert.match(TASK, /Orders/)
+ })
+})
diff --git a/examples/autopilot-quickstart/src/autopilot.ts b/examples/autopilot-quickstart/src/autopilot.ts
new file mode 100644
index 0000000..b1a4294
--- /dev/null
+++ b/examples/autopilot-quickstart/src/autopilot.ts
@@ -0,0 +1,150 @@
+import { AiFake, agent, type ToolCall } from '@gemstack/ai-sdk'
+import {
+ Supervisor,
+ agentPlanner,
+ stackPersonas,
+ personaRoster,
+ personaInstructions,
+ personaTools,
+ FakeRunner,
+ runnerTools,
+ terminalSink,
+ launchAutopilot,
+ type RunnerSession,
+ type SupervisorEvent,
+ type SupervisorRun,
+} from '@gemstack/ai-autopilot'
+
+/**
+ * The end-to-end shape of the ai-autopilot epic, in one flow:
+ *
+ * personas → Supervisor → runner (sandbox) → surfaces
+ *
+ * A lead planner decomposes a build task and routes each subtask to a
+ * stack-aware **persona**; the **Supervisor** dispatches them; each persona
+ * worker acts inside a **runner** sandbox (writing Vike/ORM files via
+ * `runnerTools`); progress is rendered through the **surfaces** (a terminal
+ * sink plus a background handle with a live stream).
+ *
+ * It runs offline: `AiFake` scripts the model, so there is no API key and the
+ * output is deterministic. Swapping `FakeRunner` for a real runner and dropping
+ * the fake is the only change needed to run it for real.
+ */
+
+/** The feature we ask autopilot to build. */
+export const TASK = 'Add a paginated Orders page backed by an orders table'
+
+/** Each subtask, the persona that should own it, and the file it writes. */
+const WORK = [
+ {
+ worker: 'universal-orm-modeler',
+ description: 'Define the orders schema and a migration',
+ file: 'database/schema.ts',
+ contents: "export const orders = table('orders', { id: id(), total: integer(), createdAt: timestamp() })\n",
+ },
+ {
+ worker: 'vike-page-builder',
+ description: 'Build the /orders page that lists orders, paginated',
+ file: 'pages/orders/+Page.jsx',
+ contents: "export default function Page({ orders }) { return }\n",
+ },
+ {
+ worker: 'ui-intent-designer',
+ description: 'Express the orders list as intent, not hardcoded markup',
+ file: 'pages/orders/+config.js',
+ contents: "export default { meta: { OrderList: { env: { server: true, client: true } } } }\n",
+ },
+] as const
+
+/** Script the fake: step 0 is the planner's JSON; then each worker writes its file. */
+function scriptModel(fake: AiFake): void {
+ const plannerOutput = JSON.stringify(WORK.map(w => ({ description: w.description, worker: w.worker })))
+ const workerSteps = WORK.flatMap((w, i) => {
+ const toolCalls: ToolCall[] = [
+ { id: `write-${i}`, name: 'write_file', arguments: { path: w.file, contents: w.contents } },
+ ]
+ return [{ toolCalls }, { text: `Wrote ${w.file}` }]
+ })
+ // concurrency: 1 makes the provider-call order deterministic:
+ // 0 = planner, then each worker's (tool-call, final-text) pair in plan order.
+ fake.respondWithSequence([{ text: plannerOutput }, ...workerSteps])
+}
+
+/** Build one worker agent per persona, each with hands inside the sandbox. */
+function personaWorkersWithSandbox(session: RunnerSession): Record> {
+ const sandbox = runnerTools(session)
+ return Object.fromEntries(
+ stackPersonas.map(p => [
+ p.name,
+ agent({ instructions: personaInstructions(p), tools: [...personaTools(p), ...sandbox] }),
+ ]),
+ )
+}
+
+export interface QuickstartResult {
+ run: SupervisorRun
+ /** Every event, in order (from the background handle). */
+ events: SupervisorEvent[]
+ /** Files the workers wrote into the sandbox. */
+ files: Record
+ /** Output of the post-build `exec`. */
+ build: { stdout: string; exitCode: number }
+ /** The preview URL the sandbox exposed. */
+ previewUrl: string
+}
+
+/**
+ * Run the whole flow once and return everything the surfaces exposed.
+ *
+ * @param write where terminal-surface lines go (default: no-op; `main.ts` prints).
+ */
+export async function runQuickstart(write: (line: string) => void = () => {}): Promise {
+ const fake = AiFake.fake()
+ scriptModel(fake)
+ try {
+ // Runner: an in-memory sandbox seeded with a minimal Vike project.
+ const runner = new FakeRunner({
+ onExec: cmd =>
+ cmd.includes('build')
+ ? { stdout: 'orders page built', stderr: '', exitCode: 0 }
+ : { stdout: '', stderr: '', exitCode: 0 },
+ })
+ const session = await runner.boot({ files: { 'package.json': '{ "name": "shop" }\n' } })
+
+ // Personas → Supervisor. The planner is told the roster so it routes by role.
+ const planner = agentPlanner(
+ agent(`You are the lead engineer. Decompose the task and route each subtask to a persona.\n\n${personaRoster(stackPersonas)}`),
+ )
+ const start = (onEvent: (e: SupervisorEvent) => void) =>
+ new Supervisor({
+ plan: planner,
+ workers: personaWorkersWithSandbox(session),
+ concurrency: 1,
+ onEvent,
+ }).run(TASK)
+
+ // Surfaces: one run feeds both the terminal (live) and a background handle.
+ const terminal = terminalSink({ write })
+ const handle = launchAutopilot(onEvent =>
+ start(e => {
+ terminal(e)
+ onEvent(e)
+ }),
+ )
+ const run = await handle.result()
+
+ // Runner again: build the app and grab a preview URL.
+ const build = await session.exec('pnpm build')
+ const preview = session.preview ? await session.preview({ port: 5173 }) : { url: '' }
+
+ return {
+ run,
+ events: handle.events(),
+ files: session.snapshot(),
+ build: { stdout: build.stdout, exitCode: build.exitCode },
+ previewUrl: preview.url,
+ }
+ } finally {
+ fake.restore()
+ }
+}
diff --git a/examples/autopilot-quickstart/src/main.ts b/examples/autopilot-quickstart/src/main.ts
new file mode 100644
index 0000000..7c300bc
--- /dev/null
+++ b/examples/autopilot-quickstart/src/main.ts
@@ -0,0 +1,23 @@
+import { runQuickstart, TASK } from './autopilot.js'
+
+/** Run the quickstart and print what each surface exposed. Offline (AiFake). */
+async function main(): Promise {
+ console.log(`Task: ${TASK}\n`)
+ console.log('--- live progress (terminal surface) ---')
+ const result = await runQuickstart(line => process.stdout.write(line + '\n'))
+
+ console.log('\n--- files written into the sandbox (runner) ---')
+ for (const path of Object.keys(result.files).sort()) console.log(` ${path}`)
+
+ console.log('\n--- build + preview (runner) ---')
+ console.log(` build: exit ${result.build.exitCode} — ${result.build.stdout}`)
+ console.log(` preview: ${result.previewUrl}`)
+
+ console.log('\n--- synthesized result (Supervisor) ---')
+ console.log(result.run.text.replace(/^/gm, ' '))
+}
+
+main().catch(err => {
+ console.error(err)
+ process.exitCode = 1
+})
diff --git a/examples/autopilot-quickstart/tsconfig.json b/examples/autopilot-quickstart/tsconfig.json
new file mode 100644
index 0000000..404aab4
--- /dev/null
+++ b/examples/autopilot-quickstart/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "noEmit": true, "rootDir": "src" },
+ "include": ["src"]
+}
diff --git a/examples/autopilot-quickstart/tsconfig.test.json b/examples/autopilot-quickstart/tsconfig.test.json
new file mode 100644
index 0000000..eebda2f
--- /dev/null
+++ b/examples/autopilot-quickstart/tsconfig.test.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "outDir": "dist-test", "rootDir": "src" },
+ "include": ["src"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 39f1e17..2eb1ca6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -27,6 +27,28 @@ importers:
specifier: ^3.5.29
version: 3.5.39(typescript@5.9.3)
+ examples/autopilot-quickstart:
+ dependencies:
+ '@gemstack/ai-autopilot':
+ specifier: workspace:^
+ version: link:../../packages/ai-autopilot
+ '@gemstack/ai-sdk':
+ specifier: workspace:^
+ version: link:../../packages/ai-sdk
+ zod:
+ specifier: ^4.0.0
+ version: 4.4.3
+ devDependencies:
+ '@types/node':
+ specifier: ^20.0.0
+ version: 20.19.43
+ tsx:
+ specifier: ^4.19.0
+ version: 4.22.4
+ typescript:
+ specifier: ^5.4.0
+ version: 5.9.3
+
examples/connectors-quickstart:
dependencies:
'@gemstack/connectors':