diff --git a/examples/bootstrap-quickstart/README.md b/examples/bootstrap-quickstart/README.md index e92eefe..ea4f92c 100644 --- a/examples/bootstrap-quickstart/README.md +++ b/examples/bootstrap-quickstart/README.md @@ -33,9 +33,26 @@ decision, and the generated `CODE-OVERVIEW.md`. `launchAutopilot` handle. - **Scale mode (#114)** — `CODE-OVERVIEW.md` is generated from the scaffold. -## Live verification (infra-gated) +## Live verification (#124) -The framework pieces verify offline here with fakes. The honest "zero to a running -app" proof needs a live model + `LocalRunner` producing a real app end to end — -that half is infra-gated (#124). Swapping the two fakes for a real model and -`LocalRunner` is the only code change; the flow is identical. +The offline run above verifies the flow structurally. `src/live.ts` runs the same +flow for real — a real model via `@gemstack/ai-sdk` and a real `LocalRunner` +workspace on the host filesystem, so the architect, the build workers, and the +deploy decision are all model-driven and the workers write **real files to disk**: + +```bash +ANTHROPIC_API_KEY=sk-... pnpm --filter @gemstack/example-bootstrap-quickstart start:live +# override the model with any provider ai-sdk knows: +GEMSTACK_MODEL=anthropic/claude-haiku-4-5-20251001 ... start:live +``` + +Swapping the fakes (`AiFake` + `FakeRunner`) for a real model + `LocalRunner` is the +only difference from `main.ts`; the orchestration is identical. A sample live run +scaffolded a 9-file Vike + universal-orm orders app (schema + migration, `pages/orders/` +with `+Page`/`+data`/`+config`, a UI-intent renderer) from the intent, blocked the +checklist once on missing auth, then decided SSR → dockploy. + +Scoped for a first, bounded proof: the production-grade **loop** keeps the scripted +verdict (so the run stays deterministic and cheap), and `deploy` uses `planOnlyTarget` +— it decides + narrates but does not actually ship. Booting/serving the generated app +and real deploy adapters remain (tracked by #109). diff --git a/examples/bootstrap-quickstart/package.json b/examples/bootstrap-quickstart/package.json index 4a165ca..83447ef 100644 --- a/examples/bootstrap-quickstart/package.json +++ b/examples/bootstrap-quickstart/package.json @@ -8,7 +8,8 @@ "typecheck": "tsc --noEmit", "test": "tsc -p tsconfig.test.json && cd dist-test && node --test", "clean": "rm -rf dist-test", - "start": "tsx src/main.ts" + "start": "tsx src/main.ts", + "start:live": "tsx src/main-live.ts" }, "dependencies": { "@gemstack/ai-autopilot": "workspace:^", diff --git a/examples/bootstrap-quickstart/src/bootstrap.ts b/examples/bootstrap-quickstart/src/bootstrap.ts index a7f7111..0caeaa4 100644 --- a/examples/bootstrap-quickstart/src/bootstrap.ts +++ b/examples/bootstrap-quickstart/src/bootstrap.ts @@ -45,9 +45,9 @@ import { * from the scaffold. * * It runs offline: `AiFake` scripts the model and `FakeRunner` is an in-memory - * sandbox, so there is no API key and the output is deterministic. Swapping the - * fakes for a real model + `LocalRunner` is the only change to run it for real — - * that live proof is the infra-gated half of this example (#124). + * sandbox, so there is no API key and the output is deterministic. `live.ts` runs + * the same flow for real (a real model + `LocalRunner` writing files to disk) — the + * live proof for #124. */ /** What the user wants built (the one thing scope asks about). */ diff --git a/examples/bootstrap-quickstart/src/live.ts b/examples/bootstrap-quickstart/src/live.ts new file mode 100644 index 0000000..982d936 --- /dev/null +++ b/examples/bootstrap-quickstart/src/live.ts @@ -0,0 +1,161 @@ +import { AiRegistry, AnthropicProvider, agent } from '@gemstack/ai-sdk' +import { + Bootstrap, + agentArchitect, + supervisorBuild, + loopChecklist, + loopImprove, + agentDeploy, + Loop, + definePrompt, + defineRule, + personaInstructions, + personaTools, + builtinPresetRegistry, + presetPersonas, + CodeOverviewMaintainer, + DecisionLedger, + LocalRunner, + runnerTools, + launchAutopilot, + type BootstrapEvent, + type BootstrapResult, + type Planner, + type RunnerSession, +} from '@gemstack/ai-autopilot' +import { INTENT, formatBootstrapEvent, type CapstoneResult } from './bootstrap.js' + +/** + * The LIVE half of the capstone (#124): the same flow as `bootstrap.ts`, but with + * the fakes swapped for real infra — a real model via `@gemstack/ai-sdk` and a real + * `LocalRunner` sandbox on the host filesystem. The architect, the build workers, + * and the deploy decision are all model-driven; the workers write REAL files into a + * real temp workspace. This is the honest "zero to a scaffolded app" proof that the + * offline run (AiFake + FakeRunner) can only assert structurally. + * + * Scoped for a first, bounded, cheap proof: the production-grade loop keeps the same + * scripted verdict as the offline example (blocks once on "no auth", then clears), + * so the run is deterministic and does not spend model budget on the checklist. + * Making the checklist a real reviewer agent is the natural follow-up. + * + * Run it: `ANTHROPIC_API_KEY=… pnpm start:live` (any provider ai-sdk knows works via + * GEMSTACK_MODEL, e.g. `GEMSTACK_MODEL=anthropic/claude-haiku-4-5-20251001`). + */ + +/** The model to drive the live run. Cheap + fast by default; override via env. */ +const MODEL = process.env['GEMSTACK_MODEL'] ?? 'anthropic/claude-haiku-4-5-20251001' + +/** The project we detect a framework from — Vike here, so the Vike preset wins. */ +const PROJECT_DEPS = { 'vike-react': '1.0.0', react: '18.0.0', '@universal-orm/core': '1.0.0' } + +/** The build plan: three subtasks, each owned by a preset persona (by name). The real + * worker decides the file contents; only the decomposition is fixed. */ +const WORK = [ + { worker: 'universal-orm-modeler', description: 'Define the orders schema and a migration in database/schema.ts' }, + { worker: 'vike-page-builder', description: 'Build pages/orders/+Page.jsx: a server-rendered, paginated list of orders' }, + { worker: 'ui-intent-designer', description: 'Express the orders list as intent (a +config.js meta), not hardcoded markup' }, +] as const +const livePlanner: Planner = () => WORK.map(w => ({ description: w.description, worker: w.worker })) + +/** Register the Anthropic provider from the environment key. Throws with a clear + * message if the key is missing, so a bad env fails loudly rather than at request time. */ +export function registerModel(): void { + const apiKey = process.env['ANTHROPIC_API_KEY'] + if (!apiKey) throw new Error('[live capstone] set ANTHROPIC_API_KEY (a model key) in the environment') + AiRegistry.register(new AnthropicProvider({ apiKey })) +} + +/** One real worker agent per preset persona, each with hands (runner tools) in the sandbox. */ +function presetWorkers(session: RunnerSession, personas: ReturnType) { + const sandbox = runnerTools(session) + return Object.fromEntries( + personas.map(p => [ + p.name, + agent({ model: MODEL, instructions: personaInstructions(p), tools: [...personaTools(p), ...sandbox] }), + ]), + ) +} + +/** The full-fledged loop: the checklist blocks once, then clears after the fix (scripted). */ +function buildLoop(): Loop { + const verdicts = [ + '```json\n{ "blockers": ["No authentication on the orders page yet"] }\n```', + '```json\n{ "blockers": [] }\n```', + ] + let pass = 0 + return new Loop({ + rules: [ + defineRule({ on: 'production-check', run: ['production-grade'] }), + defineRule({ on: 'major-change', run: ['address-blockers'] }), + ], + prompts: [ + definePrompt({ id: 'production-grade', run: () => verdicts[Math.min(pass++, verdicts.length - 1)]! }), + definePrompt({ id: 'address-blockers', run: () => 'Added a +guard to the orders page (vike-auth).' }), + ], + }) +} + +/** Snapshot the real workspace into a { path: contents } map. */ +async function snapshot(session: RunnerSession): Promise> { + const files: Record = {} + for (const path of await session.fs.list()) { + files[path] = await session.fs.read(path) + } + return files +} + +/** + * Run the live capstone once against a real model + LocalRunner and return everything + * the surfaces exposed. Disposes the temp workspace when done. + */ +export async function runLiveCapstone(write: (line: string) => void = () => {}): Promise { + registerModel() + + // 0. Preset: detect the framework from the project's deps, pick its personas. + const { preset, detection } = builtinPresetRegistry().select({ dependencies: PROJECT_DEPS }) + const personas = presetPersonas(preset) + + // Runner: a REAL isolated workspace on the host filesystem, seeded with a minimal project. + const runner = new LocalRunner() + const session = await runner.boot({ files: { 'package.json': JSON.stringify({ name: 'orders-app' }) + '\n' } }) + + try { + const loop = buildLoop() + const ledger = new DecisionLedger() + + const handle = launchAutopilot(onEvent => + new Bootstrap({ + ledger, + onEvent: e => { + write(formatBootstrapEvent(e)) + onEvent(e) + }, + steps: { + scope: () => ({ scope: 'full', intent: INTENT }), + architect: agentArchitect(agent({ model: MODEL, instructions: 'architect' })), + build: supervisorBuild({ plan: livePlanner, workers: presetWorkers(session, personas), concurrency: 1 }), + checklist: loopChecklist({ loop }), + improve: loopImprove({ loop }), + // planOnlyTarget default: the model DECIDES render + target and narrates; actually + // shipping to Dockploy/Cloudflare stays behind the real DeployTarget adapters (#109). + deploy: agentDeploy(agent({ model: MODEL, instructions: 'deployer' })), + }, + }).run(), + ) + const result = await handle.result() + const files = await snapshot(session) + + // Scale mode: generate CODE-OVERVIEW.md from the real scaffold (a material change). + const maintainer = new CodeOverviewMaintainer({ + regenerate: () => ({ + summary: 'A server-rendered orders app on Vike + universal-orm.', + sections: [{ title: 'Structure', body: Object.keys(files).map(f => `- \`${f}\``).join('\n') }], + }), + }) + await maintainer.handle({ kind: 'major-change', summary: 'scaffolded the app', paths: Object.keys(files) }) + + return { detection, result, events: handle.events(), files, overview: maintainer.get() } + } finally { + await session.dispose() + } +} diff --git a/examples/bootstrap-quickstart/src/main-live.ts b/examples/bootstrap-quickstart/src/main-live.ts new file mode 100644 index 0000000..ad91620 --- /dev/null +++ b/examples/bootstrap-quickstart/src/main-live.ts @@ -0,0 +1,30 @@ +import { runLiveCapstone } from './live.js' +import { INTENT } from './bootstrap.js' + +/** Run the LIVE capstone (real model + LocalRunner) and print what each phase produced. + * Needs ANTHROPIC_API_KEY in the environment (see live.ts). */ +async function main(): Promise { + console.log(`Bootstrap (LIVE): "${INTENT}"\n`) + console.log('--- live narration (surface stream) ---') + const { detection, result, files, overview } = await runLiveCapstone(line => process.stdout.write(line + '\n')) + + console.log('\n--- preset ---') + console.log(` detected: ${detection.framework} (confidence ${detection.confidence})`) + + console.log('\n--- app scaffolded (REAL LocalRunner workspace) ---') + for (const path of Object.keys(files).sort()) console.log(` ${path} (${files[path]?.length ?? 0} bytes)`) + + console.log('\n--- outcome ---') + console.log(` scope: ${result.scope}`) + console.log(` production-grade: ${result.productionGrade} (after ${result.passes} checklist pass(es))`) + console.log(` deploy: ${result.deploy?.plan.render.toUpperCase()} → ${result.deploy?.plan.target}`) + console.log(` decisions recorded: ${result.plan.decisions.length}`) + + console.log('\n--- scale mode: CODE-OVERVIEW.md ---') + console.log(` ${overview?.summary}`) +} + +main().catch(err => { + console.error(err) + process.exitCode = 1 +})