From f3046ab6c2cf0ff01249f68a2dae3f156ca2921e Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Thu, 2 Jul 2026 20:34:11 +0300 Subject: [PATCH] feat(ai-autopilot): bootstrap deploy step + DeployTarget adapter seam (#123) --- .changeset/ai-autopilot-bootstrap-deploy.md | 5 + .../src/bootstrap/bootstrap.test.ts | 59 +++++++++ .../ai-autopilot/src/bootstrap/bootstrap.ts | 22 ++- .../ai-autopilot/src/bootstrap/deploy.test.ts | 89 +++++++++++++ packages/ai-autopilot/src/bootstrap/deploy.ts | 125 ++++++++++++++++++ packages/ai-autopilot/src/bootstrap/index.ts | 15 +++ .../ai-autopilot/src/bootstrap/steps.test.ts | 11 +- packages/ai-autopilot/src/bootstrap/types.ts | 66 ++++++++- packages/ai-autopilot/src/index.ts | 18 ++- 9 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 .changeset/ai-autopilot-bootstrap-deploy.md create mode 100644 packages/ai-autopilot/src/bootstrap/deploy.test.ts create mode 100644 packages/ai-autopilot/src/bootstrap/deploy.ts diff --git a/.changeset/ai-autopilot-bootstrap-deploy.md b/.changeset/ai-autopilot-bootstrap-deploy.md new file mode 100644 index 0000000..ee098c0 --- /dev/null +++ b/.changeset/ai-autopilot-bootstrap-deploy.md @@ -0,0 +1,5 @@ +--- +"@gemstack/ai-autopilot": minor +--- + +Add bootstrap's deploy phase and the `DeployTarget` adapter seam. The final phase decides the rendering mode (SSR/SSG/SPA) and the deploy target (Dockploy vs Cloudflare), narrates the plan, and hands it to a `DeployTarget` — the same pattern as the runner seam. `agentDeploy` is the default step (an `ai-sdk` agent decides `{ render, target, reason }`, normalized against the allowed sets); `planOnlyTarget` is the v1 default that decides and narrates without shipping, and `FakeDeployTarget` backs tests. v1 decides + narrates only; real Dockploy / Cloudflare adapters implement `DeployTarget` and are infra-gated follow-ups, so bootstrap never does a blind deploy. The deploy step is optional and its outcome rides on `BootstrapResult.deploy`. Closes #123. diff --git a/packages/ai-autopilot/src/bootstrap/bootstrap.test.ts b/packages/ai-autopilot/src/bootstrap/bootstrap.test.ts index 5b6a66d..3630ded 100644 --- a/packages/ai-autopilot/src/bootstrap/bootstrap.test.ts +++ b/packages/ai-autopilot/src/bootstrap/bootstrap.test.ts @@ -124,6 +124,65 @@ describe('Bootstrap — prototype scope', () => { }) }) +describe('Bootstrap — deploy phase', () => { + const deployStub = (): NonNullable => () => ({ + plan: { render: 'ssr', target: 'dockploy', reason: 'per-request data' }, + result: { deployed: false, detail: 'plan only' }, + }) + + it('runs the deploy step last and carries its outcome on the result', async () => { + const events: BootstrapEvent[] = [] + const boot = new Bootstrap({ steps: stubSteps({ deploy: deployStub() }), onEvent: e => events.push(e) }) + const result = await boot.run() + + assert.equal(result.deploy?.plan.render, 'ssr') + assert.equal(result.deploy?.plan.target, 'dockploy') + assert.equal(result.deploy?.result.deployed, false) + // deploy narration + event land after the checklist, before done + const types = events.map(e => e.type) + assert.deepEqual(types.slice(-3), ['narrate', 'deploy', 'done']) + const deployEvent = events.find(e => e.type === 'deploy') + assert.equal(deployEvent?.type === 'deploy' && deployEvent.plan.target, 'dockploy') + }) + + it('is optional — no deploy step means no deploy on the result', async () => { + const boot = new Bootstrap({ steps: stubSteps() }) + const result = await boot.run() + assert.equal(result.deploy, undefined) + }) + + it('passes productionGrade into the deploy context', async () => { + let seenProductionGrade: boolean | undefined + const boot = new Bootstrap({ + steps: stubSteps({ + deploy: ctx => { + seenProductionGrade = ctx.productionGrade + return { plan: { render: 'ssg', target: 'cloudflare', reason: 'static' }, result: { deployed: false } } + }, + }), + }) + await boot.run() + assert.equal(seenProductionGrade, true) // full scope, passed the checklist + }) + + it('runs deploy for a prototype too (not loop-gated)', async () => { + let deployRan = false + const boot = new Bootstrap({ + steps: stubSteps({ + scope: () => ({ scope: 'prototype', intent: 'quick demo' }), + deploy: ctx => { + deployRan = true + assert.equal(ctx.productionGrade, false) // prototype never runs the loop + return { plan: { render: 'spa', target: 'cloudflare', reason: 'client demo' }, result: { deployed: false } } + }, + }), + }) + const result = await boot.run() + assert.equal(deployRan, true) + assert.equal(result.deploy?.plan.render, 'spa') + }) +}) + describe('Bootstrap — interrupt + isolation', () => { it('aborts between phases when the signal fires', async () => { const controller = new AbortController() diff --git a/packages/ai-autopilot/src/bootstrap/bootstrap.ts b/packages/ai-autopilot/src/bootstrap/bootstrap.ts index 471597d..3704617 100644 --- a/packages/ai-autopilot/src/bootstrap/bootstrap.ts +++ b/packages/ai-autopilot/src/bootstrap/bootstrap.ts @@ -5,6 +5,7 @@ import type { BootstrapOptions, BootstrapResult, BootstrapSteps, + DeployOutcome, } from './types.js' /** Thrown when a run is aborted via the `AbortSignal`. */ @@ -132,6 +133,24 @@ export class Bootstrap { } } + const productionGrade = passes > 0 && blockers.length === 0 + + // 5. Deploy — the final phase: decide SSR/SSG/SPA + target, narrate, and hand + // the plan to a DeployTarget. v1 targets are plan-only (they do not ship). + let deploy: DeployOutcome | undefined + if (this.steps.deploy) { + this.throwIfAborted() + this.emit({ type: 'narrate', phase: 'deploy', message: 'Deciding how and where to deploy' }) + deploy = await this.steps.deploy({ + plan, + scope, + intent, + productionGrade, + ...(this.signal ? { signal: this.signal } : {}), + }) + this.emit({ type: 'deploy', plan: deploy.plan, result: deploy.result }) + } + const result: BootstrapResult = { scope, intent, @@ -139,8 +158,9 @@ export class Bootstrap { run, passes, blockers, - productionGrade: passes > 0 && blockers.length === 0, + productionGrade, stoppedEarly, + ...(deploy ? { deploy } : {}), } this.emit({ type: 'done', result }) return result diff --git a/packages/ai-autopilot/src/bootstrap/deploy.test.ts b/packages/ai-autopilot/src/bootstrap/deploy.test.ts new file mode 100644 index 0000000..219627a --- /dev/null +++ b/packages/ai-autopilot/src/bootstrap/deploy.test.ts @@ -0,0 +1,89 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { AiFake, agent } from '@gemstack/ai-sdk' +import { agentDeploy, planOnlyTarget, FakeDeployTarget, DEFAULT_DEPLOY_TARGETS } from './deploy.js' +import type { DeployContext } from './types.js' + +const ctx = (over: Partial = {}): DeployContext => ({ + plan: { stack: 'Vike + universal-orm', narration: '', decisions: [] }, + scope: 'full', + intent: 'a shop', + productionGrade: true, + ...over, +}) + +describe('planOnlyTarget (v1 default seam)', () => { + it('decides + narrates only — never reports a deploy', async () => { + const target = planOnlyTarget() + const result = await target.deploy({ plan: { render: 'ssr', target: 'dockploy', reason: 'x' }, intent: 'a shop' }) + assert.equal(result.deployed, false) + assert.match(result.detail ?? '', /infra-gated/) + }) +}) + +describe('FakeDeployTarget', () => { + it('records the plans it received and returns a canned result', () => { + const target = new FakeDeployTarget({ result: { deployed: true, url: 'https://x.example' } }) + const plan = { render: 'ssg' as const, target: 'cloudflare', reason: 'static' } + const result = target.deploy({ plan, intent: 'a shop' }) + assert.equal(result.url, 'https://x.example') + assert.deepEqual(target.deployed, [plan]) + }) +}) + +describe('agentDeploy (default step over an ai-sdk agent)', () => { + it('decides { render, target, reason } and hands the plan to the target', async () => { + const fake = AiFake.fake() + try { + fake.respondWithSequence([ + { text: JSON.stringify({ render: 'ssr', target: 'dockploy', reason: 'auth + per-request data' }) }, + ]) + const target = new FakeDeployTarget() + const step = agentDeploy(agent({ instructions: 'deployer' }), { target }) + const outcome = await step(ctx()) + + assert.deepEqual(outcome.plan, { render: 'ssr', target: 'dockploy', reason: 'auth + per-request data' }) + assert.equal(outcome.result.deployed, true) + assert.equal(target.deployed.length, 1) // the plan reached the target + } finally { + fake.restore() + } + }) + + it('defaults to a plan-only target when none is wired (v1 decides, does not ship)', async () => { + const fake = AiFake.fake() + try { + fake.respondWithSequence([{ text: JSON.stringify({ render: 'ssg', target: 'cloudflare', reason: 'static' }) }]) + const outcome = await agentDeploy(agent({ instructions: 'deployer' }))(ctx()) + assert.equal(outcome.plan.target, 'cloudflare') + assert.equal(outcome.result.deployed, false) + } finally { + fake.restore() + } + }) + + it('normalizes an out-of-set target to the first allowed one', async () => { + const fake = AiFake.fake() + try { + fake.respondWithSequence([{ text: JSON.stringify({ render: 'ssr', target: 'heroku', reason: 'habit' }) }]) + const outcome = await agentDeploy(agent({ instructions: 'deployer' }))(ctx()) + assert.equal(outcome.plan.target, DEFAULT_DEPLOY_TARGETS[0]) // 'heroku' is not allowed → 'dockploy' + } finally { + fake.restore() + } + }) + + it('steers the decision with a custom target list', async () => { + const fake = AiFake.fake() + try { + fake.respondWithSequence([{ text: JSON.stringify({ render: 'spa', target: 'fly', reason: 'edge' }) }]) + const outcome = await agentDeploy(agent({ instructions: 'deployer' }), { targets: ['fly', 'render'] })(ctx()) + assert.equal(outcome.plan.target, 'fly') + // the allowed targets reached the model + const sent = JSON.stringify(fake.getCalls()[0]) + assert.match(sent, /fly/) + } finally { + fake.restore() + } + }) +}) diff --git a/packages/ai-autopilot/src/bootstrap/deploy.ts b/packages/ai-autopilot/src/bootstrap/deploy.ts new file mode 100644 index 0000000..a000c47 --- /dev/null +++ b/packages/ai-autopilot/src/bootstrap/deploy.ts @@ -0,0 +1,125 @@ +import { Output } from '@gemstack/ai-sdk' +import type { Agent } from '@gemstack/ai-sdk' +import { z } from 'zod' +import type { + BootstrapSteps, + DeployPlan, + DeployResult, + DeployTarget, + DeployTargetContext, + RenderMode, +} from './types.js' + +/** + * The deploy phase — decide SSR/SSG/SPA + a target, narrate the plan, and hand it + * to a {@link DeployTarget}. Deciding is this module's job; *executing* the plan + * is the target's, behind the adapter seam (the same shape as the runner seam). + * + * v1 ships only {@link planOnlyTarget} (decides + narrates, does not ship) and + * {@link FakeDeployTarget} (tests). Real Dockploy / Cloudflare adapters implement + * {@link DeployTarget} and are infra-gated follow-ups — so bootstrap never does a + * blind deploy now. + */ + +/** The target names the deploy step steers toward by default. */ +export const DEFAULT_DEPLOY_TARGETS = ['dockploy', 'cloudflare'] as const + +const RENDER_MODES: readonly RenderMode[] = ['ssr', 'ssg', 'spa'] + +/** + * A plan-only {@link DeployTarget}: it reports that nothing was shipped, so the + * flow decides + narrates without a blind deploy. This is the v1 default and the + * reference implementation of the seam. + */ +export function planOnlyTarget(name = 'plan-only'): DeployTarget { + return { + name, + deploy: () => ({ + deployed: false, + detail: 'Decided and narrated only — no deploy adapter is wired (infra-gated).', + }), + } +} + +/** Options for {@link FakeDeployTarget}. */ +export interface FakeDeployTargetOptions { + name?: string + /** The result to return; defaults to a shipped result with a fake URL. */ + result?: DeployResult +} + +/** An in-memory {@link DeployTarget} for tests: records the plan and returns a canned result. */ +export class FakeDeployTarget implements DeployTarget { + readonly name: string + private readonly result: DeployResult + /** Every plan handed to {@link deploy}, in order. */ + readonly deployed: DeployPlan[] = [] + + constructor(opts: FakeDeployTargetOptions = {}) { + this.name = opts.name ?? 'fake' + this.result = opts.result ?? { deployed: true, url: 'https://fake.deploy.example', detail: 'fake deploy' } + } + + deploy(ctx: DeployTargetContext): DeployResult { + this.deployed.push(ctx.plan) + return this.result + } +} + +/** Options for {@link agentDeploy}. */ +export interface AgentDeployOptions { + /** The target that executes the plan. Defaults to {@link planOnlyTarget} (v1). */ + target?: DeployTarget + /** Allowed target names to steer the decision. Default {@link DEFAULT_DEPLOY_TARGETS}. */ + targets?: readonly string[] + /** Override the deploy instruction prepended to the app context. */ + instructions?: string +} + +const DEFAULT_DEPLOY_INSTRUCTIONS = `You are deciding how to ship this app. Choose the rendering mode and the deploy +target that fit what was built, and explain the choice in one line — act like an +engineer who decides, not one who asks. Pick SSR when pages need per-request data +or auth, SSG when content is mostly static and can be prebuilt, and SPA only when +the app is a client-side dashboard behind a login.` + +/** + * A deploy step backed by an `ai-sdk` agent: it asks for a structured + * `{ render, target, reason }` decision (validated against the allowed render + * modes + target names), then hands the plan to the {@link DeployTarget}. With no + * target wired it uses {@link planOnlyTarget}, so v1 decides and narrates without + * shipping. + */ +export function agentDeploy(deployer: Agent, opts: AgentDeployOptions = {}): NonNullable { + const targets = opts.targets && opts.targets.length ? opts.targets : DEFAULT_DEPLOY_TARGETS + const target = opts.target ?? planOnlyTarget() + const instructions = opts.instructions ?? DEFAULT_DEPLOY_INSTRUCTIONS + const schema = z.object({ + render: z.enum(['ssr', 'ssg', 'spa']).describe('How the app is rendered/served'), + target: z.string().describe(`One of: ${targets.join(', ')}`), + reason: z.string().describe('One-line rationale'), + }) + const output = Output.object({ schema }) + + return async ({ plan: architecture, scope, intent, productionGrade, signal }) => { + const context = [ + `# What the user wanted (${scope})`, + intent, + `# Stack`, + architecture.stack, + `# Production-grade`, + productionGrade ? 'the app passed the production-grade checklist' : 'not yet fully production-grade', + `# Allowed targets`, + targets.join(', '), + ].join('\n') + const response = await deployer.prompt(`${instructions}\n\n${context}\n\n${output.toSystemPrompt()}`) + const decided = output.parse(response.text ?? '') + + // Normalize the decision against the allowed sets so a stray value cannot slip through. + const render: RenderMode = RENDER_MODES.includes(decided.render) ? decided.render : 'ssr' + const targetName = targets.includes(decided.target) ? decided.target : targets[0]! + const deployPlan: DeployPlan = { render, target: targetName, reason: decided.reason } + + const result = await target.deploy({ plan: deployPlan, intent, ...(signal ? { signal } : {}) }) + return { plan: deployPlan, result } + } +} diff --git a/packages/ai-autopilot/src/bootstrap/index.ts b/packages/ai-autopilot/src/bootstrap/index.ts index e42f651..aabb360 100644 --- a/packages/ai-autopilot/src/bootstrap/index.ts +++ b/packages/ai-autopilot/src/bootstrap/index.ts @@ -24,17 +24,32 @@ export { type LoopChecklistOptions, type LoopImproveOptions, } from './steps.js' +export { + agentDeploy, + planOnlyTarget, + FakeDeployTarget, + DEFAULT_DEPLOY_TARGETS, + type AgentDeployOptions, + type FakeDeployTargetOptions, +} from './deploy.js' export type { BootstrapScope, BootstrapPhase, ScopeAnswer, ArchitectDecision, ArchitectPlan, + RenderMode, + DeployPlan, + DeployResult, + DeployOutcome, + DeployTarget, + DeployTargetContext, BootstrapEvent, BootstrapResult, BootstrapSteps, BootstrapOptions, BuildContext, ArchitectContext, + DeployContext, LoopPassContext, } from './types.js' diff --git a/packages/ai-autopilot/src/bootstrap/steps.test.ts b/packages/ai-autopilot/src/bootstrap/steps.test.ts index 3702c99..bb45b76 100644 --- a/packages/ai-autopilot/src/bootstrap/steps.test.ts +++ b/packages/ai-autopilot/src/bootstrap/steps.test.ts @@ -2,6 +2,7 @@ import { describe, it } from 'node:test' import assert from 'node:assert/strict' import { AiFake, agent } from '@gemstack/ai-sdk' import { agentArchitect, supervisorBuild, loopChecklist, loopImprove } from './steps.js' +import { agentDeploy, FakeDeployTarget } from './deploy.js' import { Bootstrap } from './bootstrap.js' import { DecisionLedger } from '../decisions/ledger.js' import { Loop } from '../loop/loop.js' @@ -128,7 +129,7 @@ describe('Bootstrap end-to-end with the default steps (offline)', () => { it('runs scope → architect → build → full-fledged loop against real primitives', async () => { const fake = AiFake.fake() try { - // Two model calls, in order: the architect plan, then the one build worker. + // Three model calls, in order: the architect plan, the build worker, the deploy decision. fake.respondWithSequence([ { text: JSON.stringify({ @@ -138,6 +139,7 @@ describe('Bootstrap end-to-end with the default steps (offline)', () => { }), }, { text: 'scaffolded the catalog page and orders schema' }, + { text: JSON.stringify({ render: 'ssr', target: 'dockploy', reason: 'per-request catalog + auth' }) }, ]) // The full-fledged loop: first checklist has a blocker, second is clean. @@ -156,6 +158,7 @@ describe('Bootstrap end-to-end with the default steps (offline)', () => { }) const ledger = new DecisionLedger() + const deployTarget = new FakeDeployTarget({ result: { deployed: true, url: 'https://bookstore.example' } }) const events: BootstrapEvent[] = [] const boot = new Bootstrap({ ledger, @@ -170,6 +173,7 @@ describe('Bootstrap end-to-end with the default steps (offline)', () => { }), checklist: loopChecklist({ loop }), improve: loopImprove({ loop }), + deploy: agentDeploy(agent({ instructions: 'deployer' }), { target: deployTarget }), }, }) @@ -182,9 +186,14 @@ describe('Bootstrap end-to-end with the default steps (offline)', () => { assert.equal(result.productionGrade, true) assert.equal(improved, 1) // improved once, between the two checks assert.equal(ledger.size, 1) // architect choice recorded + // deploy ran last: decided SSR/dockploy and reached the (fake) target + assert.deepEqual(result.deploy?.plan, { render: 'ssr', target: 'dockploy', reason: 'per-request catalog + auth' }) + assert.equal(result.deploy?.result.url, 'https://bookstore.example') + assert.equal(deployTarget.deployed.length, 1) // build events were forwarded into the narration assert.ok(events.some(e => e.type === 'build' && e.event.type === 'plan')) assert.ok(events.some(e => e.type === 'checklist' && e.passing)) + assert.ok(events.some(e => e.type === 'deploy')) } finally { fake.restore() } diff --git a/packages/ai-autopilot/src/bootstrap/types.ts b/packages/ai-autopilot/src/bootstrap/types.ts index 6afaf8c..846fb63 100644 --- a/packages/ai-autopilot/src/bootstrap/types.ts +++ b/packages/ai-autopilot/src/bootstrap/types.ts @@ -28,7 +28,7 @@ import type { Verdict } from '../loop/verdict.js' export type BootstrapScope = 'prototype' | 'full' /** The phase a narration line belongs to. */ -export type BootstrapPhase = 'scope' | 'architect' | 'build' | 'loop' +export type BootstrapPhase = 'scope' | 'architect' | 'build' | 'loop' | 'deploy' /** The answer to the one upfront question. */ export interface ScopeAnswer { @@ -43,6 +43,55 @@ export interface ArchitectDecision { why: string } +/** How the app is rendered/served, which drives the deploy shape. */ +export type RenderMode = 'ssr' | 'ssg' | 'spa' + +/** The deploy decision: how to render, where to ship, and why. */ +export interface DeployPlan { + render: RenderMode + /** The deploy target's name (e.g. "dockploy", "cloudflare"). */ + target: string + /** One-line rationale, to narrate. */ + reason: string +} + +/** What a {@link DeployTarget} reports back. */ +export interface DeployResult { + /** True when a real deploy ran; false for a plan-only (v1 default) target. */ + deployed: boolean + /** The live URL, when a real adapter produced one. */ + url?: string + /** Human-readable detail (what happened, or why nothing did). */ + detail?: string +} + +/** The result of the deploy phase: the decided plan and, if a target ran, its result. */ +export interface DeployOutcome { + plan: DeployPlan + result: DeployResult +} + +/** Context handed to a {@link DeployTarget}. */ +export interface DeployTargetContext { + plan: DeployPlan + intent: string + signal?: AbortSignal +} + +/** + * The deploy adapter seam — the same pattern as the runner seam (#109). v1 ships + * only plan-only targets ({@link planOnlyTarget}) and a fake for tests; real + * Dockploy / Cloudflare adapters implement this behind the same interface and + * are infra-gated follow-ups. A target *executes* a {@link DeployPlan}; deciding + * the plan is the deploy step's job. + */ +export interface DeployTarget { + /** Stable name, matched against {@link DeployPlan.target}. */ + readonly name: string + /** Execute the plan (or, for a plan-only target, report that it did not). */ + deploy(ctx: DeployTargetContext): DeployResult | Promise +} + /** 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"). */ @@ -64,6 +113,7 @@ export type BootstrapEvent = | { type: 'build'; event: SupervisorEvent } | { type: 'checklist'; pass: number; blockers: readonly string[]; passing: boolean } | { type: 'improve'; pass: number; blockers: readonly string[] } + | { type: 'deploy'; plan: DeployPlan; result: DeployResult } | { type: 'done'; result: BootstrapResult } /** The outcome of a bootstrap run. */ @@ -82,6 +132,8 @@ export interface BootstrapResult { productionGrade: boolean /** True when the loop hit `maxPasses` with blockers still open. */ stoppedEarly: boolean + /** The deploy phase's outcome, when a `deploy` step ran. */ + deploy?: DeployOutcome } /** Context handed to the build step. */ @@ -103,6 +155,16 @@ export interface ArchitectContext { signal?: AbortSignal } +/** Context handed to the deploy step (the final phase). */ +export interface DeployContext { + plan: ArchitectPlan + scope: BootstrapScope + intent: string + /** Whether the full-fledged loop ended clean, so the step can factor readiness in. */ + productionGrade: boolean + signal?: AbortSignal +} + /** Context handed to the improve / checklist steps in the full-fledged loop. */ export interface LoopPassContext { /** 1-based pass number; each pass is meant to run with fresh context. */ @@ -130,6 +192,8 @@ export interface BootstrapSteps { checklist?: (ctx: LoopPassContext) => Verdict | Promise /** Address the current blockers with fresh context, before the next checklist. */ improve?: (ctx: LoopPassContext) => unknown | Promise + /** Decide SSR/SSG/SPA + target, narrate, and (via a target) deploy. The final phase. */ + deploy?: (ctx: DeployContext) => DeployOutcome | Promise } /** Options for {@link Bootstrap}. */ diff --git a/packages/ai-autopilot/src/index.ts b/packages/ai-autopilot/src/index.ts index 042c74d..74eb3b5 100644 --- a/packages/ai-autopilot/src/index.ts +++ b/packages/ai-autopilot/src/index.ts @@ -67,9 +67,12 @@ * running, production-grade app. It narrates each phase and repeats the * production-grade checklist until its `{ blockers }` verdict is empty. * - * - {@link Bootstrap} — the orchestrator over four injectable steps + * - {@link Bootstrap} — the orchestrator over the injectable steps * - {@link agentArchitect} / {@link supervisorBuild} — the default step wirings * - {@link loopChecklist} / {@link loopImprove} — the full-fledged loop steps + * - {@link agentDeploy} + the {@link DeployTarget} seam ({@link planOnlyTarget}, + * {@link FakeDeployTarget}) — the final phase: decide SSR/SSG/SPA + target and + * narrate; real adapters are infra-gated */ export { Supervisor } from './supervisor.js' export { agentPlanner, type AgentPlannerOptions } from './planner.js' @@ -192,22 +195,35 @@ export { supervisorBuild, loopChecklist, loopImprove, + agentDeploy, + planOnlyTarget, + FakeDeployTarget, + DEFAULT_DEPLOY_TARGETS, type ArchitectAgentOptions, type SupervisorBuildOptions, type LoopStepOptions, type LoopChecklistOptions, type LoopImproveOptions, + type AgentDeployOptions, + type FakeDeployTargetOptions, type BootstrapScope, type BootstrapPhase, type ScopeAnswer, type ArchitectDecision, type ArchitectPlan, + type RenderMode, + type DeployPlan, + type DeployResult, + type DeployOutcome, + type DeployTarget, + type DeployTargetContext, type BootstrapEvent, type BootstrapResult, type BootstrapSteps, type BootstrapOptions, type BuildContext, type ArchitectContext, + type DeployContext, type LoopPassContext, } from './bootstrap/index.js' export type {