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
7 changes: 7 additions & 0 deletions .changeset/data-persona-prisma.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@gemstack/ai-autopilot': minor
---

Point the flagship data persona at Prisma (installable) instead of the unpublished universal-orm

The bootstrap data persona told the agent to build the data layer on `universal-orm`, which isn't installable (`@universal-orm/core` 404s on npm), so from-scratch live builds stalled sanity-checking the stack and produced nothing. It now defaults to Prisma with concrete install/init steps (schema-first, migrations derived from the schema, a fully typed client), and the architect default no longer names an unpublished package. The persona export is renamed `universalOrmModeler` -> `dataModeler` (persona name `data-modeler`). Closes #181.
7 changes: 7 additions & 0 deletions .changeset/scaffold-empty-workspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@gemstack/framework': minor
---

Recover from-scratch builds: scaffold an empty workspace instead of only polishing

The full-fledged loop assumed an app already existed, so a from-scratch run could end at the framework's default "Welcome" page. The build step now verifies it produced files and hard re-prompts to scaffold from scratch if the workspace is still empty; the improve step switches to a "create the whole app from scratch" directive when the workspace is empty (instead of "smallest change / no unrelated features"); and the default `--max-passes` is raised from 3 to 5 so a from-scratch build has room to recover. Also clarifies the dashboard/terminal end status ("finished", not "done", so it reads as separate from the production-grade badge). Closes #182.
2 changes: 1 addition & 1 deletion examples/autopilot-quickstart/src/autopilot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('autopilot quickstart: the four layers compose end-to-end', () => {
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'],
['data-modeler', 'ui-intent-designer', 'vike-page-builder'],
)
assert.ok(result.run.results.every(r => r.ok), 'every subtask succeeded')

Expand Down
2 changes: 1 addition & 1 deletion examples/autopilot-quickstart/src/autopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ 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',
worker: 'data-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",
Expand Down
2 changes: 1 addition & 1 deletion examples/bootstrap-quickstart/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('bootstrap capstone: the whole epic composes end-to-end (offline)', ()
assert.equal(detection.framework, 'Vike')

// Architect: chose the stack and recorded its choices to the ledger.
assert.match(result.plan.stack, /Vike \+ universal-orm/)
assert.match(result.plan.stack, /Vike \+ Prisma/)
assert.equal(result.plan.decisions.length, 2)

// Build: each preset persona wrote its file into the sandbox.
Expand Down
14 changes: 7 additions & 7 deletions examples/bootstrap-quickstart/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ import {
export const INTENT = 'A paginated Orders page backed by an orders table, with sign-in.'

/** 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' }
const PROJECT_DEPS = { 'vike-react': '1.0.0', react: '18.0.0', '@prisma/client': '1.0.0' }

/** Each build subtask, the persona that owns it, and the file it writes. */
const WORK = [
{
worker: 'universal-orm-modeler',
worker: 'data-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",
Expand All @@ -80,10 +80,10 @@ const WORK = [

/** The architect's structured decision (what `agentArchitect` parses). */
const ARCHITECT_PLAN = {
stack: 'Vike + universal-orm on Postgres, with vike-auth',
narration: 'Server-rendered orders app: Vike pages, a universal-orm data layer, sessions via vike-auth.',
stack: 'Vike + Prisma on Postgres, with vike-auth',
narration: 'Server-rendered orders app: Vike pages, a Prisma data layer, sessions via vike-auth.',
decisions: [
{ choice: 'universal-orm on Postgres', why: 'the orders catalog is relational and needs typed queries' },
{ choice: 'Prisma on Postgres', why: 'the orders catalog is relational and needs typed queries' },
{ choice: 'SSR over SPA', why: 'orders need per-request data and auth on the server' },
],
}
Expand Down Expand Up @@ -237,10 +237,10 @@ export async function runCapstone(write: (line: string) => void = () => {}): Pro
// Scale mode: generate CODE-OVERVIEW.md from the scaffold (a material change).
const maintainer = new CodeOverviewMaintainer({
regenerate: () => ({
summary: 'A server-rendered orders app on Vike + universal-orm.',
summary: 'A server-rendered orders app on Vike + Prisma.',
sections: [
{ title: 'Structure', body: '- `pages/orders/` — the paginated orders page\n- `database/` — the orders schema + migrations' },
{ title: 'Conventions', body: 'Data goes through the universal-orm model builder; pages stay thin.' },
{ title: 'Conventions', body: 'Data goes through the Prisma client; pages stay thin.' },
],
}),
})
Expand Down
6 changes: 3 additions & 3 deletions examples/bootstrap-quickstart/src/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ import { INTENT, formatBootstrapEvent, type CapstoneResult } from './bootstrap.j
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' }
const PROJECT_DEPS = { 'vike-react': '1.0.0', react: '18.0.0', '@prisma/client': '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: 'data-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
Expand Down Expand Up @@ -165,7 +165,7 @@ export async function runLiveCapstone(write: (line: string) => void = () => {}):
// 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.',
summary: 'A server-rendered orders app on Vike + Prisma.',
sections: [{ title: 'Structure', body: Object.keys(files).map(f => `- \`${f}\``).join('\n') }],
}),
})
Expand Down
5 changes: 3 additions & 2 deletions packages/ai-autopilot/src/bootstrap/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export interface ArchitectAgentOptions {

const DEFAULT_ARCHITECT_INSTRUCTIONS = `You are the lead architect. Choose the stack and structure for the app the user
describes and commit to it — act like a senior engineer who decides and explains,
not one who asks permission. Default to the GemStack stack (Vike + universal-orm)
unless the intent clearly calls for something else. Narrate what you are building
not one who asks permission. Default to the GemStack stack (Vike + Prisma)
unless the intent clearly calls for something else. Only choose packages that are
published and installable on npm. Narrate what you are building
and why in a sentence or two, and list the key choices so they are recorded and
not re-litigated later.`

Expand Down
4 changes: 2 additions & 2 deletions packages/ai-autopilot/src/bootstrap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Verdict } from '../loop/verdict.js'
* scope → architect → build → full-fledged loop
*
* - **Scope** is the one and only interrogation: prototype vs full, plus intent.
* - **Architect** picks the stack (Vike + universal-orm), narrates it, and
* - **Architect** picks the stack (Vike + Prisma), narrates it, and
* records key choices to the decisions ledger — no permission asked.
* - **Build** runs the Supervisor over the stack personas inside a runner,
* streaming narration; the caller can interrupt via an `AbortSignal`.
Expand Down Expand Up @@ -94,7 +94,7 @@ export interface DeployTarget {

/** The architect's output: the stack it chose, a narration, and the key choices. */
export interface ArchitectPlan {
/** The chosen stack, one line (e.g. "Vike + universal-orm, Postgres, vike-auth"). */
/** The chosen stack, one line (e.g. "Vike + Prisma, Postgres, vike-auth"). */
stack: string
/** What it is building and why, to narrate to the user. */
narration: string
Expand Down
6 changes: 3 additions & 3 deletions packages/ai-autopilot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
* - {@link agentSynthesizer} / {@link defaultSynthesize} — combine results
*
* Personas add the stack-aware knowledge layer: reusable roles that know the
* GemStack stack (Vike/Next + universal-orm), materialized into worker agents.
* GemStack stack (Vike/Next + Prisma), materialized into worker agents.
*
* - {@link definePersona} — define a stack-aware role
* - {@link personaAgent} / {@link personaWorkers} — materialize personas for a run
* - {@link personaRoster} — describe personas to a planner
* - {@link stackPersonas} — the built-in Vike + universal-orm personas
* - {@link stackPersonas} — the built-in Vike + Prisma personas
* - {@link sharedPersonas} — the framework-neutral core (data layer + intent UI)
*
* The runner is the pluggable execution seam: a workspace (filesystem + shell +
Expand Down Expand Up @@ -106,7 +106,7 @@ export {
personaRoster,
vikePageBuilder,
nextPageBuilder,
universalOrmModeler,
dataModeler,
uiIntentDesigner,
sharedPersonas,
stackPersonas,
Expand Down
4 changes: 2 additions & 2 deletions packages/ai-autopilot/src/personas/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('personaWorkers', () => {
const workers = personaWorkers(stackPersonas)
assert.deepEqual(
Object.keys(workers).sort(),
['ui-intent-designer', 'universal-orm-modeler', 'vike-page-builder'],
['data-modeler', 'ui-intent-designer', 'vike-page-builder'],
)
})

Expand All @@ -105,7 +105,7 @@ describe('personaRoster', () => {
it('lists each persona name + role for a planner to route on', () => {
const roster = personaRoster(stackPersonas)
assert.match(roster, /`vike-page-builder`/)
assert.match(roster, /`universal-orm-modeler`/)
assert.match(roster, /`data-modeler`/)
assert.match(roster, /`ui-intent-designer`/)
assert.match(roster, /worker/)
})
Expand Down
2 changes: 1 addition & 1 deletion packages/ai-autopilot/src/personas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export {
export {
vikePageBuilder,
nextPageBuilder,
universalOrmModeler,
dataModeler,
uiIntentDesigner,
sharedPersonas,
stackPersonas,
Expand Down
40 changes: 25 additions & 15 deletions packages/ai-autopilot/src/personas/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Persona } from './types.js'

/**
* The built-in, stack-aware personas — the opinionated knowledge that makes
* autopilot know the GemStack stack (Vike + universal-orm) instead of guessing.
* autopilot know the GemStack stack (Vike + Prisma) instead of guessing.
* Each carries conventions-level guidance; detailed how-to arrives via skills
* attached to a persona at use time.
*/
Expand Down Expand Up @@ -56,21 +56,31 @@ Do not reach for \`getServerSideProps\`/\`getStaticProps\` (that is the older Pa
Router); use the App Router data model.`,
})

/** Defines schema, derives migrations, and writes queries on universal-orm. */
export const universalOrmModeler: Persona = definePersona({
name: 'universal-orm-modeler',
role: 'Models data with universal-orm: schema first, migrations derived, typed queries',
appliesTo: ['@universal-orm/*', 'universal-orm'],
systemPrompt: `You own the data layer with universal-orm, which is schema-first and
adapter-agnostic (a native engine and Prisma/Drizzle adapters share one API).
/** Defines schema, derives migrations, and writes typed queries with Prisma (the default ORM). */
export const dataModeler: Persona = definePersona({
name: 'data-modeler',
role: 'Models data schema-first: derive migrations, generate a typed client, write typed queries',
appliesTo: ['prisma', '@prisma/client', 'drizzle-orm', 'drizzle-kit'],
systemPrompt: `You own the data layer. Default to Prisma — schema-first, migrations derived
from the schema, and a fully typed client — unless the architect explicitly chose
another published SQL ORM (e.g. Drizzle). Never assume an unpublished/private ORM
is installable: use only packages that resolve on npm.

Set up Prisma concretely, do not investigate whether it exists:
- Install: \`npm install -D prisma\` and \`npm install @prisma/client\`.
- Init once: \`npx prisma init --datasource-provider postgresql\` (or sqlite for a
quick start). This creates \`prisma/schema.prisma\` and a \`DATABASE_URL\` in \`.env\`.
- Define your models in \`prisma/schema.prisma\`, then \`npx prisma migrate dev --name
<change>\` to derive and apply a migration, and \`npx prisma generate\` for the client.
- Import the typed client from \`@prisma/client\` and query through it.

Conventions to follow:
- The schema is the source of truth. Define models/tables in the schema; derive
migrations from it rather than hand-writing DDL. Regenerate the typed registry
after a schema change so queries stay typed.
- Reads and writes go through the model query builder, not raw SQL. Reach for
raw SQL only when the builder genuinely cannot express the query, and prefer
bulk/cursor helpers (chunked iteration, RETURNING on writes) over N+1 loops.
- The schema is the source of truth. Define models in the schema; derive
migrations from it rather than hand-writing DDL. Regenerate the client after a
schema change so queries stay typed.
- Reads and writes go through the typed client, not raw SQL. Reach for raw SQL
only when the client genuinely cannot express the query, and prefer batch
helpers over N+1 loops.
- Keep the data layer separate from the framework: it does not import Vike or
know about pages. A page's \`+data\` hook calls into it; it never calls back.
- Migrations are forward-only and reviewed. Never edit an applied migration —
Expand Down Expand Up @@ -117,7 +127,7 @@ to the decoupled implementation.`,
* A preset adds its framework-specific page builder on top (see the presets seam).
*/
export const sharedPersonas: readonly Persona[] = Object.freeze([
universalOrmModeler,
dataModeler,
uiIntentDesigner,
])

Expand Down
2 changes: 1 addition & 1 deletion packages/ai-autopilot/src/personas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { LoadedSkill } from '@gemstack/ai-skills'
* A reusable, stack-aware role an agent can take on. A persona is *data*: a
* name, a one-line role, a system-prompt fragment, and the skills/tools it
* brings. It carries opinionated knowledge of the GemStack stack (Vike +
* universal-orm) so an autopilot run is not generic — it knows where pages
* Prisma) so an autopilot run is not generic — it knows where pages
* live, how the schema drives migrations, and to express UI as intent.
*
* A persona is materialized into an `Agent` on demand (see `personaAgent`),
Expand Down
2 changes: 1 addition & 1 deletion packages/ai-autopilot/src/presets/library.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('built-in presets', () => {
describe('presetPersonas', () => {
it('is the preset page builder followed by the shared neutral personas', () => {
const names = presetPersonas(vikePreset).map(p => p.name)
assert.deepEqual(names, ['vike-page-builder', 'universal-orm-modeler', 'ui-intent-designer'])
assert.deepEqual(names, ['vike-page-builder', 'data-modeler', 'ui-intent-designer'])
})

it('swaps only the page builder between frameworks — the rest of the stack is shared', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/framework/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Options:
--cwd <dir> Workspace the agent builds in (default: current directory).
--model <id> Model to pass through to the wrapped agent.
--scope <prototype|full> How much app to build (default: full).
--max-passes <n> Full-fledged loop pass budget (default: 3).
--max-passes <n> Full-fledged loop pass budget (default: 5).
--permission-mode <mode> Claude Code permission mode: default | acceptEdits |
bypassPermissions | plan (default: acceptEdits).
--dangerously-skip-permissions Bypass all agent permission checks (sandboxes only).
Expand Down
2 changes: 1 addition & 1 deletion packages/framework/src/dashboard/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ function onEvent(fe) {
else if (fe.kind === 'bootstrap') bootstrap(fe.event);
else if (fe.kind === 'driver') driver(fe.event);
else if (fe.kind === 'log') log(fe.message);
else if (fe.kind === 'end') { $('status').textContent = fe.ok ? '\\u25cf done' : '\\u25cf failed'; }
else if (fe.kind === 'end') { $('status').textContent = fe.ok ? '\\u25cf finished' : '\\u25cf failed'; }
}
function esc(s) { const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
const src = new EventSource('/events');
Expand Down
2 changes: 1 addition & 1 deletion packages/framework/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function formatFrameworkEvent(event: FrameworkEvent): string {
case 'bootstrap':
return formatBootstrapEvent(event.event)
case 'end':
return event.ok ? '✓ done' : `✗ failed: ${event.detail ?? 'unknown error'}`
return event.ok ? '✓ finished' : `✗ failed: ${event.detail ?? 'unknown error'}`
}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/framework/src/fake-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FakeDriver, type FakeTurn } from './driver/index.js'
import type { DeployDecision } from './run.js'

/**
* The deterministic `--fake` scenario: a small Vike + universal-orm orders app.
* The deterministic `--fake` scenario: a small Vike + Prisma orders app.
* It wires a {@link FakeDriver} whose scripted turns walk the exact prompt order
* the flow issues (architect JSON, build summary, checklist-with-blocker,
* improve, clean checklist), so the whole scope -> deploy flow runs offline with
Expand All @@ -16,7 +16,7 @@ export const FAKE_INTENT = 'A paginated orders page backed by an orders table, w

/** Deps that make the Vike preset win detection in the demo. */
export const FAKE_SIGNALS: FrameworkSignals = {
dependencies: { 'vike-react': '1.0.0', react: '18.0.0', '@universal-orm/core': '1.0.0' },
dependencies: { 'vike-react': '1.0.0', react: '18.0.0', '@prisma/client': '5.0.0' },
}

/** The deploy decision narrated at the end of the demo. */
Expand All @@ -27,10 +27,10 @@ export const FAKE_DEPLOY: DeployDecision = {
}

const ARCHITECT = {
stack: 'Vike + universal-orm on Postgres, with vike-auth',
narration: 'Server-rendered orders app: Vike pages, a universal-orm data layer, sessions via vike-auth.',
stack: 'Vike + Prisma on Postgres, with vike-auth',
narration: 'Server-rendered orders app: Vike pages, a Prisma data layer, sessions via vike-auth.',
decisions: [
{ choice: 'universal-orm on Postgres', why: 'the orders catalog is relational and needs typed queries' },
{ choice: 'Prisma on Postgres', why: 'the orders catalog is relational and needs typed queries' },
{ choice: 'SSR over SPA', why: 'orders need per-request data and auth on the server' },
],
}
Expand Down
7 changes: 6 additions & 1 deletion packages/framework/src/run.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { strict as assert } from 'node:assert'
import { test } from 'node:test'
import { runFramework } from './run.js'
import { DEFAULT_MAX_PASSES, runFramework } from './run.js'
import { FAKE_DEPLOY, FAKE_INTENT, FAKE_SIGNALS, fakeDriver } from './fake-script.js'
import type { FrameworkEvent } from './events.js'

Expand Down Expand Up @@ -97,6 +97,11 @@ test('runFramework shows a literal session link immediately (no template)', asyn
assert.equal(session.sessionLink, 'https://code.example.com/live')
})

test('the default pass budget is raised for from-scratch builds (#182)', () => {
// 3 was too low: the first passes go to bootstrapping an empty workspace.
assert.equal(DEFAULT_MAX_PASSES, 5)
})

test('runFramework prototype scope skips the full-fledged loop', async () => {
const { result } = await runFramework({
intent: 'a quick landing page',
Expand Down
Loading
Loading