diff --git a/examples/framework-demo/README.md b/examples/framework-demo/README.md
new file mode 100644
index 0000000..994b3f7
--- /dev/null
+++ b/examples/framework-demo/README.md
@@ -0,0 +1,72 @@
+# The Framework — end-to-end demo
+
+One prompt, taken all the way to a **running, deployed app** — offline and
+deterministic, in a couple of seconds.
+
+This runs the real product (`@gemstack/framework`'s `runFramework`) with the
+built-in **fake driver**: no Claude Code, no model, no API keys. Same code a live
+run executes — preset detection, architect decisions, the full-fledged
+production-grade loop, deploy — just with scripted agent turns so it is instant
+and repeatable. Two things are genuinely real, not narrated:
+
+- **the app boots and serves** — the serve gate starts a real HTTP server and the
+ run leaves it running, so the demo `fetch`es it and prints what it served;
+- **deploy runs the real `cloudflareTarget` adapter** (over a simulated wrangler),
+ so it ends at a real-looking `workers.dev` URL.
+
+## Run it
+
+```bash
+pnpm --filter @gemstack/example-framework-demo start
+```
+
+## What you see
+
+```
+Prompt: "A paginated orders page backed by an orders table, with sign-in."
+
+--- live narration ---
+◆ fake in /tmp/framework-demo-xxxx
+ Detected Vike (confidence 2); framing with 3 persona(s)
+▶ scope: full — "A paginated orders page backed by an orders table, with sign-in."
+▶ architect: Vike + universal-orm on Postgres, with vike-auth
+ · universal-orm on Postgres — the orders catalog is relational and needs typed queries
+ · SSR over SPA — orders need per-request data and auth on the server
+ Building on Vike + universal-orm on Postgres, with vike-auth
+ Checking the app is production-grade
+ serve: start: node server.js
+ serve: fetch http://localhost:50106/ -> 200
+ ✗ checklist pass 1: No authentication on the orders page yet
+ → improving: No authentication on the orders page yet
+ serve: start: node server.js
+ serve: fetch http://localhost:50106/ -> 200
+ ✓ checklist pass 2: production-grade
+▶ deploy: SSR → cloudflare (per-request orders data + server-side auth)
+✓ production-grade in 2 pass(es)
+▶ your app is running at http://localhost:50106
+✓ done
+
+--- outcome ---
+ preset detected: Vike
+ production-grade: true (in 2 pass(es))
+ deployed to: cloudflare → https://orders-app.gemstack.workers.dev
+ running locally: http://localhost:50106
+ it served:
Orders Orders
…
+```
+
+The loop does not take the agent's word for "production-grade": it **boots the app
+and fetches it** every pass (the `serve:` lines), and blocks until both the review
+and the real server pass. When it finishes, the app is left running so you can open
+it — that is the localhost link, live until the run stops.
+
+## The real thing
+
+Same flow, driven against Claude Code instead of the fake driver:
+
+```bash
+npx @gemstack/framework "a paginated orders page with sign-in"
+```
+
+That opens the localhost dashboard, wraps Claude Code as the coding agent, and runs
+the identical scope → architect → build → production-grade loop → deploy — writing
+real files and, with `--serve`, leaving the real app running behind a preview link.
diff --git a/examples/framework-demo/package.json b/examples/framework-demo/package.json
new file mode 100644
index 0000000..03eebc8
--- /dev/null
+++ b/examples/framework-demo/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@gemstack/example-framework-demo",
+ "version": "0.0.0",
+ "private": true,
+ "description": "Showable end-to-end demo of @gemstack/framework: one prompt → scope → architect → build → full-fledged loop → deploy → a real running app, offline and deterministic via the fake driver.",
+ "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/framework": "workspace:^",
+ "@gemstack/ai-autopilot": "workspace:^"
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "tsx": "^4.19.0",
+ "typescript": "^5.4.0"
+ }
+}
diff --git a/examples/framework-demo/src/demo.test.ts b/examples/framework-demo/src/demo.test.ts
new file mode 100644
index 0000000..13f3a7b
--- /dev/null
+++ b/examples/framework-demo/src/demo.test.ts
@@ -0,0 +1,31 @@
+import { describe, it } from 'node:test'
+import assert from 'node:assert/strict'
+import { DEMO_INTENT, runDemo } from './demo.js'
+
+describe('framework demo: one prompt → a running, deployed app (offline)', () => {
+ it('runs the whole product flow and ends at a real serving app', async () => {
+ const lines: string[] = []
+ const out = await runDemo(line => lines.push(line))
+
+ // Preset detection picked Vike from the demo's deps signals.
+ assert.equal(out.framework, 'Vike')
+
+ // The full-fledged loop blocked once (no auth) then cleared → production-grade.
+ assert.equal(out.productionGrade, true)
+ assert.equal(out.passes, 2)
+
+ // Deploy ran the real cloudflareTarget adapter → a live workers.dev URL.
+ assert.equal(out.deployTarget, 'cloudflare')
+ assert.equal(out.deployUrl, 'https://orders-app.gemstack.workers.dev')
+
+ // The app was actually booted and served (not just narrated).
+ assert.match(out.previewUrl ?? '', /^http:\/\/localhost:\d+$/)
+ assert.match(out.served, /Orders/)
+ assert.match(out.served, /Ada Lovelace/)
+
+ // The narration told the whole story: the prompt, then a production-grade end.
+ assert.ok(lines.some(l => l.includes(DEMO_INTENT)))
+ assert.ok(lines.some(l => l.includes('production-grade')))
+ assert.ok(lines.some(l => l.includes('your app is running at')))
+ })
+})
diff --git a/examples/framework-demo/src/demo.ts b/examples/framework-demo/src/demo.ts
new file mode 100644
index 0000000..4fe5ea6
--- /dev/null
+++ b/examples/framework-demo/src/demo.ts
@@ -0,0 +1,139 @@
+import { mkdtemp, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { createServer } from 'node:net'
+import { cloudflareTarget } from '@gemstack/ai-autopilot'
+import {
+ FAKE_DEPLOY,
+ FAKE_INTENT,
+ FAKE_SIGNALS,
+ fakeDriver,
+ formatFrameworkEvent,
+ runFramework,
+ type AppPreview,
+} from '@gemstack/framework'
+
+/**
+ * The showable end-to-end demo for `@gemstack/framework`: one prompt taken all
+ * the way to a *running, deployed* app, offline and deterministic.
+ *
+ * It drives the real product (`runFramework`) with the built-in **fake driver**
+ * (no Claude Code, no model, no keys) so the whole flow — preset detection,
+ * architect decisions, build, the full-fledged production-grade loop, and deploy
+ * — runs the same code a live run does, just with scripted agent turns. Two
+ * things are genuinely real, not narrated:
+ *
+ * - the app **boots and serves**: the serve gate starts a real HTTP server and
+ * the run leaves it running, so the demo can `fetch` it and prove it works;
+ * - the **deploy** runs the real `cloudflareTarget` adapter over a simulated
+ * wrangler, so it ends at a real-looking `workers.dev` URL deterministically.
+ *
+ * This is the artifact to point people at (README / the-framework.ai / Discord):
+ * a single command that goes from an idea to an app you can open.
+ */
+
+/** The one prompt the demo builds from. */
+export const DEMO_INTENT = FAKE_INTENT
+
+/** What {@link runDemo} reports back after the flow completes. */
+export interface DemoOutcome {
+ /** The detected framework preset (Vike, from the demo's deps signals). */
+ framework: string | undefined
+ /** Whether the loop reached production-grade, and in how many passes. */
+ productionGrade: boolean
+ passes: number
+ /** The deploy result: the target and the (simulated) live URL. */
+ deployTarget: string | undefined
+ deployUrl: string | undefined
+ /** The localhost URL the app was left running at. */
+ previewUrl: string | undefined
+ /** The first bytes the running app actually served (proof it works). */
+ served: string
+}
+
+/** An ephemeral free port so the demo never collides with something already bound. */
+function freePort(): Promise {
+ return new Promise((resolvePromise, rejectPromise) => {
+ const srv = createServer()
+ srv.on('error', rejectPromise)
+ srv.listen(0, '127.0.0.1', () => {
+ const addr = srv.address()
+ const port = typeof addr === 'object' && addr ? addr.port : 0
+ srv.close(() => resolvePromise(port))
+ })
+ })
+}
+
+/** The tiny but real "orders app" the serve gate boots — a stand-in for what the agent builds. */
+function ordersAppSource(port: number): string {
+ return [
+ `const http = require('http')`,
+ `const page = \`Orders`,
+ `Orders
| # | Customer | Total |
`,
+ `| 1001 | Ada Lovelace | $42.00 |
`,
+ `| 1002 | Alan Turing | $17.50 |
\``,
+ `http.createServer((_, res) => res.end(page)).listen(${port})`,
+ ].join('\n')
+}
+
+/** The simulated Cloudflare: every command succeeds; the deploy prints a workers.dev URL. */
+const simulatedCloudflare = {
+ exec: async (command: string) => ({
+ stdout: /wrangler|deploy/.test(command)
+ ? 'Published framework-demo\nhttps://orders-app.gemstack.workers.dev'
+ : '',
+ stderr: '',
+ exitCode: 0,
+ }),
+}
+
+/**
+ * Run the whole demo and stream one narration line per phase to `onLine`.
+ * Deterministic and offline; leaves no processes or temp files behind.
+ */
+export async function runDemo(onLine: (line: string) => void): Promise {
+ const dir = await mkdtemp(join(tmpdir(), 'framework-demo-'))
+ const port = await freePort()
+ await writeFile(join(dir, 'server.js'), ordersAppSource(port) + '\n')
+ await writeFile(join(dir, 'package.json'), JSON.stringify({ name: 'orders-app', private: true }) + '\n')
+
+ let preview: AppPreview | undefined
+ try {
+ const run = await runFramework({
+ intent: DEMO_INTENT,
+ driver: fakeDriver(),
+ cwd: dir,
+ signals: FAKE_SIGNALS,
+ serve: { command: 'node server.js', port, waitMs: 8000, keepAlive: true },
+ deploy: FAKE_DEPLOY,
+ deployTarget: cloudflareTarget({
+ session: simulatedCloudflare,
+ apiToken: 'demo-token',
+ accountId: 'demo-account',
+ projectName: 'framework-demo',
+ }),
+ onEvent: event => onLine(formatFrameworkEvent(event)),
+ })
+ preview = run.preview
+
+ // Prove the app the loop signed off on is actually serving right now.
+ let served = ''
+ if (preview) {
+ const res = await fetch(preview.url)
+ served = (await res.text()).replace(/\s+/g, ' ').trim()
+ }
+
+ return {
+ framework: run.detection.framework,
+ productionGrade: run.result.productionGrade,
+ passes: run.result.passes,
+ deployTarget: run.result.deploy?.plan.target,
+ deployUrl: run.result.deploy?.result.url,
+ previewUrl: preview?.url,
+ served,
+ }
+ } finally {
+ if (preview) await preview.stop()
+ await rm(dir, { recursive: true, force: true })
+ }
+}
diff --git a/examples/framework-demo/src/main.ts b/examples/framework-demo/src/main.ts
new file mode 100644
index 0000000..80d15af
--- /dev/null
+++ b/examples/framework-demo/src/main.ts
@@ -0,0 +1,24 @@
+import { DEMO_INTENT, runDemo } from './demo.js'
+
+/** Run the demo and print the whole story: the live narration, then the outcome. */
+async function main(): Promise {
+ console.log('The Framework — end-to-end demo (offline, deterministic)\n')
+ console.log(`Prompt: "${DEMO_INTENT}"\n`)
+ console.log('--- live narration ---')
+ const out = await runDemo(line => console.log(line))
+
+ console.log('\n--- outcome ---')
+ console.log(` preset detected: ${out.framework}`)
+ console.log(` production-grade: ${out.productionGrade} (in ${out.passes} pass(es))`)
+ console.log(` deployed to: ${out.deployTarget} → ${out.deployUrl}`)
+ console.log(` running locally: ${out.previewUrl}`)
+ console.log(` it served: ${out.served.slice(0, 72)}${out.served.length > 72 ? '…' : ''}`)
+
+ console.log('\nThat is the fake driver. To do it for real against Claude Code:')
+ console.log(' npx @gemstack/framework "a paginated orders page with sign-in"')
+}
+
+main().catch(err => {
+ console.error(err)
+ process.exitCode = 1
+})
diff --git a/examples/framework-demo/tsconfig.json b/examples/framework-demo/tsconfig.json
new file mode 100644
index 0000000..404aab4
--- /dev/null
+++ b/examples/framework-demo/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "noEmit": true, "rootDir": "src" },
+ "include": ["src"]
+}
diff --git a/examples/framework-demo/tsconfig.test.json b/examples/framework-demo/tsconfig.test.json
new file mode 100644
index 0000000..eebda2f
--- /dev/null
+++ b/examples/framework-demo/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 f28338e..367f377 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -96,6 +96,25 @@ importers:
specifier: ^5.4.0
version: 5.9.3
+ examples/framework-demo:
+ dependencies:
+ '@gemstack/ai-autopilot':
+ specifier: workspace:^
+ version: link:../../packages/ai-autopilot
+ '@gemstack/framework':
+ specifier: workspace:^
+ version: link:../../packages/framework
+ 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/mcp-quickstart:
dependencies:
'@gemstack/mcp':