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
35 changes: 28 additions & 7 deletions examples/bootstrap-quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ pnpm --filter @gemstack/example-bootstrap-quickstart start

No API key: `AiFake` scripts the model and `FakeRunner` is an in-memory sandbox,
so the run is deterministic. You'll see the narration stream live, the files the
persona workers wrote, the checklist blocking once and then clearing, the deploy
decision, and the generated `CODE-OVERVIEW.md`.
persona workers wrote, the checklist blocking once and then clearing, the app
shipped to a Cloudflare URL, and the generated `CODE-OVERVIEW.md`. The deploy runs
the real `cloudflareTarget` adapter over a simulated `wrangler`, so the whole flow
ends at a live-looking URL with no credentials.

## What it shows

Expand All @@ -28,7 +30,8 @@ decision, and the generated `CODE-OVERVIEW.md`.
and records its choices to the **decisions ledger**, a **build** scaffolds the app
with the persona workers inside a **runner**, the **full-fledged loop** repeats the
production-grade checklist until its `{ blockers }` verdict is empty, and a
**deploy** is decided behind the `DeployTarget` seam.
**deploy** is decided and shipped through the `DeployTarget` seam — here the real
`cloudflareTarget` (SSR → Workers), run over a simulated `wrangler` offline.
- **Surfaces (#100/#120)** — every phase streams as narration over the generic
`launchAutopilot<BootstrapEvent, BootstrapResult>` handle.
- **Scale mode (#114)** — `CODE-OVERVIEW.md` is generated from the scaffold.
Expand All @@ -50,11 +53,29 @@ Swapping the fakes (`AiFake` + `FakeRunner`) for a real model + `LocalRunner` is
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.
checklist once on missing auth, then decided SSR → Cloudflare.

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. Real deploy adapters remain (#109).
### Real deploy to Cloudflare

Add a Cloudflare token and the live run ships for real — `cloudflareTarget`
installs, builds, and deploys the scaffold (Workers for SSR, Pages for SSG/SPA)
and reports the live URL:

```bash
ANTHROPIC_API_KEY=sk-... \
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_PROJECT=my-app \
pnpm --filter @gemstack/example-bootstrap-quickstart start:live
```

Without `CLOUDFLARE_API_TOKEN` the deploy falls back to `planOnlyTarget` (decide +
narrate, no ship), so the live run works with only a model key. Note the deploy
runs `npm install && npm run build` against the scaffold, so a real ship needs the
generated app to actually build — that is what the full-fledged loop (and an
opt-in `serveCheck`, below) are for.

Scoped for a bounded proof, the production-grade **loop** keeps the scripted verdict
so the run stays deterministic and cheap; making the checklist a real reviewer agent
is the natural follow-up.

### Giving the loop teeth: `serveCheck`

Expand Down
8 changes: 5 additions & 3 deletions examples/bootstrap-quickstart/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ describe('bootstrap capstone: the whole epic composes end-to-end (offline)', ()
assert.deepEqual(result.blockers, [])
assert.equal(result.productionGrade, true)

// Deploy: decided SSR + a target and reported the (fake) shipped URL.
// Deploy: decided SSR → Cloudflare and shipped via the real cloudflareTarget
// adapter (over a simulated wrangler), reporting the live URL it printed.
assert.equal(result.deploy?.plan.render, 'ssr')
assert.equal(result.deploy?.plan.target, 'dockploy')
assert.equal(result.deploy?.result.url, 'https://orders.example.app')
assert.equal(result.deploy?.plan.target, 'cloudflare')
assert.equal(result.deploy?.result.deployed, true)
assert.equal(result.deploy?.result.url, 'https://orders-app.gemstack.workers.dev')

// Surface: the stream ran scope-first, done-last, and the terminal printed it.
assert.equal(events[0]?.type, 'scope')
Expand Down
26 changes: 20 additions & 6 deletions examples/bootstrap-quickstart/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
loopChecklist,
loopImprove,
agentDeploy,
FakeDeployTarget,
cloudflareTarget,
Loop,
definePrompt,
defineRule,
Expand Down Expand Up @@ -88,8 +88,11 @@ const ARCHITECT_PLAN = {
],
}

/** The deploy decision (what `agentDeploy` parses). */
const DEPLOY_DECISION = { render: 'ssr', target: 'dockploy', reason: 'per-request orders data + server-side auth' }
/** The deploy decision (what `agentDeploy` parses). SSR → Cloudflare Workers. */
const DEPLOY_DECISION = { render: 'ssr', target: 'cloudflare', reason: 'per-request orders data + server-side auth' }

/** The URL the simulated `wrangler` prints, so the offline demo ends at a live-looking URL. */
const DEPLOY_URL = 'https://orders-app.gemstack.workers.dev'

/**
* Script the fake provider. Order (concurrency 1) is: the architect's plan, then
Expand Down Expand Up @@ -189,15 +192,26 @@ export async function runCapstone(write: (line: string) => void = () => {}): Pro
const { preset, detection } = builtinPresetRegistry().select({ dependencies: PROJECT_DEPS })
const personas = presetPersonas(preset)

// Runner: an in-memory sandbox seeded with a minimal project.
// Runner: an in-memory sandbox seeded with a minimal project. `wrangler` is
// simulated (prints a live-looking URL) so the real cloudflareTarget adapter
// runs its full path — install → build → deploy → parse URL — offline.
const runner = new FakeRunner({
onExec: cmd => (cmd.includes('build') ? { stdout: 'built', stderr: '', exitCode: 0 } : { stdout: '', stderr: '', exitCode: 0 }),
onExec: cmd => {
if (cmd.includes('wrangler')) return { stdout: `Published orders-app\n${DEPLOY_URL}`, stderr: '', exitCode: 0 }
return { stdout: cmd.includes('build') ? 'built' : '', stderr: '', exitCode: 0 }
},
})
const session = await runner.boot({ files: { 'package.json': JSON.stringify({ name: 'orders-app' }) + '\n' } })

const loop = buildLoop()
const ledger = new DecisionLedger()
const deployTarget = new FakeDeployTarget({ result: { deployed: true, url: 'https://orders.example.app' } })
// The real Cloudflare adapter, run over the simulated wrangler above. A fake
// token lets it proceed offline; the live capstone passes a real one.
const deployTarget = cloudflareTarget({
session,
apiToken: process.env['CLOUDFLARE_API_TOKEN'] ?? 'demo-token',
projectName: 'orders-app',
})

// Surfaces: run bootstrap detached; the terminal prints as events stream.
const handle = launchAutopilot<BootstrapEvent, BootstrapResult>(onEvent =>
Expand Down
23 changes: 20 additions & 3 deletions examples/bootstrap-quickstart/src/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
loopChecklist,
loopImprove,
agentDeploy,
cloudflareTarget,
Loop,
definePrompt,
defineRule,
Expand Down Expand Up @@ -95,6 +96,23 @@ function buildLoop(): Loop {
})
}

/**
* The deploy step. With `CLOUDFLARE_API_TOKEN` set, it ships for real: the model
* decides SSR/SSG/SPA and `cloudflareTarget` installs → builds → deploys the app
* to Cloudflare (Workers for SSR, Pages for SSG/SPA) and reports the live URL.
* Without a token it falls back to `planOnlyTarget` (decide + narrate, no ship),
* so the run works with only a model key.
*/
function deployStep(session: RunnerSession, deployer: ReturnType<typeof agent>) {
if (process.env['CLOUDFLARE_API_TOKEN']) {
return agentDeploy(deployer, {
targets: ['cloudflare'],
target: cloudflareTarget({ session, projectName: process.env['CLOUDFLARE_PROJECT'] ?? 'gemstack-orders-demo' }),
})
}
return agentDeploy(deployer)
}

/** Snapshot the real workspace into a { path: contents } map. */
async function snapshot(session: RunnerSession): Promise<Record<string, string>> {
const files: Record<string, string> = {}
Expand Down Expand Up @@ -136,9 +154,8 @@ export async function runLiveCapstone(write: (line: string) => void = () => {}):
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' })),
// Real Cloudflare deploy when CLOUDFLARE_API_TOKEN is set, else plan-only.
deploy: deployStep(session, agent({ model: MODEL, instructions: 'deployer' })),
},
}).run(),
)
Expand Down
Loading