framework
Version:
The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.
261 lines • 13.2 kB
TypeScript
import { LoopEngine, type BootstrapResult, type BootstrapScope, type DeployTarget, type DomainPreset, type FrameworkDetection, type FrameworkSignals, type RunnerSession } from '@gemstack/ai-autopilot';
import type { Driver } from './driver/index.js';
import { type EcoOptions } from './system-prompt.js';
import { type BindProjectDeps, type RecordMessage } from './await-gate.js';
import { type TodoLoopResult } from './todo-loop.js';
import { type ChoicePick, type ChoiceRequest, type FrameworkEvent } from './events.js';
import type { RunMessages } from './run-messages.js';
/**
* The framework's default full-fledged pass budget. Higher than ai-autopilot's
* base of 3 because a from-scratch build spends its first pass or two just
* bootstrapping an empty workspace before there is anything to polish (#182).
*/
export declare const DEFAULT_MAX_PASSES = 5;
/** The deploy decision to narrate at the end (plan-only in v1: it does not ship). */
export interface DeployDecision {
render: 'ssr' | 'ssg' | 'spa';
target: string;
reason: string;
}
/**
* How to actually boot and serve the generated app so the loop can gate on it
* *running*, not just on an agent's review. When set, the run's checklist step
* installs, (builds,) starts the app, and fetches it; a failure becomes a
* blocker the loop hands back to the agent to fix.
*/
export interface ServeConfig {
/** The command that starts the app (e.g. `npm run dev`). */
command: string;
/** Install command run first (e.g. `npm install`). */
install?: string;
/** Build command run after install (e.g. `npm run build`). */
build?: string;
/** Port the app listens on. Default 3000. */
port?: number;
/** How long to wait for it to accept connections. Default 15000ms. */
waitMs?: number;
/** Path to fetch once it is up. Default `/`. */
healthPath?: string;
/**
* Keep the app serving after a successful run and hand back an
* {@link AppPreview} the caller must {@link AppPreview.stop}. Default `false`:
* the serve gate boots the app only to check it, then tears it down. The CLI
* sets this so the dashboard can show a live preview link until Ctrl+C.
*/
keepAlive?: boolean;
}
/** Options for {@link runFramework}. */
export interface RunFrameworkOptions {
/** What the user wants built (the one scope question's answer). */
intent: string;
/** How much app: a quick prototype (no full-fledged loop) or the full thing. Default `"full"`. */
scope?: BootstrapScope;
/** The wrapped coding agent. */
driver: Driver;
/** Absolute workspace path the agent builds in. */
cwd: string;
/** Model id to pass through to the driver. */
model?: string;
/** Signals for preset detection (deps/files). Default: none, so the flagship preset wins. */
signals?: FrameworkSignals;
/**
* A user-authored system prompt (from `SYSTEM.md`) injected into every prompt
* (#301). Load with `loadUserSystemPrompt(cwd)`. Composed after the built-in
* #326 system prompt, so a repo can add its own instructions on top of the default.
*/
systemPrompt?: string;
/**
* Inject the built-in #326 system prompt into every prompt (#301). Default
* `true`; pass `false` (e.g. from `the-framework.yml`) to remove it. The name
* is the historical config key: #326 is the anti-lazy-pill's (#297) successor.
*/
antiLazyPill?: boolean;
/** This run has a real browser (#824), so the system channel says so. */
browser?: boolean;
/**
* This is a project-less "topic" run (#1120): advertise the bind gate (#1121) in the system
* channel and wire {@link bind} so an `await-bind-project` / `await-create-project` gate resolves.
*/
topic?: boolean;
/** The bind seams (#1121) a topic run's gate resolves against. Only meaningful with {@link topic}. */
bind?: BindProjectDeps;
/** Transparent mode (#625): empty the system channel entirely (raw `claude -p`); overrides antiLazyPill/eco. */
transparent?: boolean;
/** Eco fine-grained control (#314): drop the enabled #326 sections to save tokens. */
eco?: EcoOptions;
/** In-context directories (#439): added as one `Context:` line to the system prompt. */
context?: readonly string[];
/**
* A user-picked Open Loop domain preset ({loops, prompts}) to run the build
* under (#251). Its loops + prompts are materialized into a driver-backed {@link LoopEngine}
* exposed as {@link RunFrameworkResult.loop}. Load it with `loadDomainPreset` /
* `softwareDevelopmentPreset` (pass `modes` there to activate variants). Omit
* for the framework-only run.
*/
preset?: DomainPreset;
/**
* The active modes for the run (e.g. `['autopilot']`). Narrated with the
* {@link preset}, which is expected to be loaded with them already applied.
* (`autopilot` no longer steers the system prompt: #556 moved the maintenance
* section out, leaving the choice-gate countdown as its whole effect. See #801.)
*/
modes?: readonly string[];
/**
* The loop event kind the review phase dispatches (#265) — this is what makes a
* run a bug fix vs a feature: `bug-fix` fires the preset's bug-fix loop, the
* default `major-change` fires its major-change loop. Overrides the preset's own
* `defaultEvent`. A kind the preset has no loop for reviews nothing (#1372).
* No-op without a preset.
*/
buildEvent?: string;
/** Max full-fledged passes. Default {@link DEFAULT_MAX_PASSES} (5). */
maxPasses?: number;
/** A deploy decision to narrate at the end. Omit to skip the deploy phase. */
deploy?: DeployDecision;
/**
* A real {@link DeployTarget} to *execute* the decided plan (e.g.
* `cloudflareTarget` / `dokployTarget`). Requires {@link deploy}. Omit to only
* narrate a plan-only decision.
*/
deployTarget?: DeployTarget;
/**
* Boot-and-serve verification for the full-fledged loop: when set, the
* checklist gates on the app actually running, not just an agent review.
*/
serve?: ServeConfig;
/**
* Where the {@link serve} verification runs (#229). `"local"` (default) boots the
* app on the host, adopting the agent's cwd in place. `"docker"` sandboxes it: a
* throwaway container is booted, the source is copied in fresh before each check
* (the build still runs on the host in this slice), deps install inside the
* container, and the app serves on a mapped port — so agent-authored code never
* installs or runs on the host. Requires a reachable Docker daemon; no-op without
* {@link serve}.
*/
sandbox?: 'local' | 'docker';
/**
* A pre-provisioned {@link RunnerSession} to run the serve check in, bypassing
* {@link sandbox} provisioning. Advanced / testing seam — the caller owns its
* lifecycle is handed to the run (it is disposed with the run). Omit to let
* {@link sandbox} provision one.
*/
runner?: RunnerSession;
/**
* A link to the live agent session, shown on the dashboard. Either a literal
* URL, or a template with `{sessionId}` (see {@link SESSION_ID_PLACEHOLDER})
* that resolves once the wrapped agent reports its real id via `session-update`.
*/
sessionLink?: string;
/** Interrupt the run between phases. */
signal?: AbortSignal;
/**
* Pause the run on an interactive choice and await a pick (#304). Called when a
* build turn stops to ask (#337/#358): the run emits a `choice` event, calls
* this, and resumes on the returned option. Omit for a headless run: the gate
* then auto-accepts the recommended option without pausing. The CLI wires this
* to the dashboard's Accept button + autopilot countdown.
*/
requestChoice?: (req: ChoiceRequest) => Promise<ChoicePick>;
/**
* Stop the run once cumulative agent cost reaches this many USD (budget cap,
* #322). Checked after each turn that reports usage: the turn that crosses the
* cap finishes, then the run stops itself (a clean stop, not a failure). Omit
* for no cap. This gates on what *this run* spent, which is a separate question
* from where the account's quota stands (readable via #517 / #521, and gated on
* by #519's consumption limits).
*/
budgetUsd?: number;
/**
* Consult the consumption limits between turns (#529): return the limit that
* has been reached to pause the run, or `null` to carry on.
*
* Must answer from a cached reading — a live quota read spawns the whole agent
* CLI (~5s). Compose one from a `QuotaPoller` and `consumptionStatus`. Omit to
* leave the run ungated, which is also what a gate that throws resolves to:
* an unreadable quota must never stop the user's work (Rom's call on #519).
*/
consumptionGate?: () => string | null;
/**
* Run the backlog loop (#323) after the build settles: consume the agent's own
* `TODO_AGENTS.md` one entry per turn until empty, gating
* before each entry when {@link requestChoice} is wired. Default: on for real
* drivers, off for the fake one (its scripted demo writes no backlog and must
* stay deterministic). Set explicitly to force either way.
*/
todoLoop?: boolean;
/** Per-run cap on backlog entries worked (#323). Default 25. */
todoMaxItems?: number;
/**
* Continue a stopped build run's conversation (#1467): the captured agent session id to
* `--resume`. When set, the build turn sends {@link RunFrameworkOptions.intent} verbatim as a
* continuation message instead of rendering the build/extend prompt — the resumed transcript
* already carries the scope→build framing, which is exactly why #782 refused to bolt
* `--resume-session` onto a fresh build run. Everything around the turn still runs: the
* bootstrap narration with its synthesize framing, the review checklist where configured, the
* backlog loop and live chat — the flow resumes, not just the conversation.
*/
resumeSessionId?: string;
/**
* Live chat (#714): once the build settles, take the user's own messages, each
* resuming the build session for full context. The session then ends itself when
* the queue is idle (#1390) unless {@link stayOpenChat} parks it. Wired only for
* an interactive run — a headless run leaves it unset and ends when the build is
* done, exactly as before.
*/
messages?: RunMessages;
/**
* Keep the chat parked for the next message instead of ending on an idle queue (#1390).
* Only for a run whose own terminal dashboard is the single surface — it has no daemon
* to resume the session through, so ending would leave its composer a dead end.
*/
stayOpenChat?: boolean;
/** Record each chat turn to the committed conversation (#908). Best-effort; unset = not recorded. */
recordMessage?: RecordMessage;
/** Observe the unified event stream. */
onEvent?: (event: FrameworkEvent) => void;
}
/**
* A running instance of the generated app, handed back so the caller can show a
* live preview link and keep it up until the user is done (then {@link stop}).
*/
export interface AppPreview {
/** The localhost URL the app is served at. */
url: string;
/** The command that started it (e.g. `npm run dev`). */
command: string;
/** Stop the app and free its runner. Idempotent. */
stop(): Promise<void>;
}
/** What a run returns. */
export interface RunFrameworkResult {
result: BootstrapResult;
detection: FrameworkDetection;
events: FrameworkEvent[];
/**
* The generated app, left running when a {@link ServeConfig} was supplied and
* the run finished. The caller owns its lifecycle: show {@link AppPreview.url},
* then call {@link AppPreview.stop} (e.g. on Ctrl+C). Absent when no serve
* config was set or the app could not be booted.
*/
preview?: AppPreview;
/**
* The domain preset's review policy, materialized against this run's driver:
* its loops plus its prompts as driver-backed passes. Present only when a
* {@link RunFrameworkOptions.preset} was supplied. It also drives the run's
* review phase (#252): each checklist pass dispatches a `major-change` event
* through it.
*/
loop?: LoopEngine;
/** How the backlog loop (#323) ended, when it ran. */
todo?: TodoLoopResult;
}
/**
* Run the whole turnkey flow: detect the framework preset, frame the wrapped
* agent with its framework skill (page builder + docs), then drive ai-autopilot's `Bootstrap`
* (scope → build → full-fledged loop → deploy) entirely *through*
* the driver (option A). Every phase, plus the agent's own progress, streams as
* a {@link FrameworkEvent}. Reversible: swap in a real deploy target, or a
* different `Driver`, without touching this wiring.
*/
export declare function runFramework(opts: RunFrameworkOptions): Promise<RunFrameworkResult>;
//# sourceMappingURL=run.d.ts.map