Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions examples/bootstrap-quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,26 @@ decision, and the generated `CODE-OVERVIEW.md`.
`launchAutopilot<BootstrapEvent, BootstrapResult>` 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).
3 changes: 2 additions & 1 deletion examples/bootstrap-quickstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
6 changes: 3 additions & 3 deletions examples/bootstrap-quickstart/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down
161 changes: 161 additions & 0 deletions examples/bootstrap-quickstart/src/live.ts
Original file line number Diff line number Diff line change
@@ -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<typeof presetPersonas>) {
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<Record<string, string>> {
const files: Record<string, string> = {}
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<CapstoneResult> {
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<BootstrapEvent, BootstrapResult>(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()
}
}
30 changes: 30 additions & 0 deletions examples/bootstrap-quickstart/src/main-live.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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
})
Loading