Prompt-to-feature scaffolding (ai-generate)
Turn a natural-language prompt into a validated feature.ts and land it
where the Designer can review it. A small workbench feature bridges into the
enterprise ai-generate feature via ctx.query, then stores the generated
source as a pattern file via ctx.write into pattern-storage — generated
code becomes reviewable source, never a deployed feature.
What it shows
Section titled “What it shows”- The generate → store pipeline —
ai-generateruns its L3 validation loop (AST parse, retry withtool_resultfeedback) and returns source that is known to parse; the sample writes it topattern-storage:write:pattern-file:createin the same transaction. - Handling all four union members —
featureis stored,clarifyhands the model’s question back as a structured422,validation_failedsurfaces the validator’s per-line findings (and how many attempts the loop spent),errorrejects plainly. Nothing half-parsed ever lands anywhere. - No silent overwrite — pattern-storage derives its aggregate-id from
(tenantId, path), so re-scaffolding the same feature name is a409version conflict, proven in the test. - Access stays honest — ai-generate and pattern-storage are both
admin-gated, and
ctx.query/ctx.writerun as the current user, so the bridging handler carries the same gate.
When to reach for it
Section titled “When to reach for it”Internal tooling where admins bootstrap new features from a description — a
self-service workbench, a CLI backend, the first step of a Designer flow.
Pair it with the ai-patch recipe: generate creates the file, patch evolves
it, a human reviews both.
Wiring
Section titled “Wiring”ai-generate requires ai-foundation (provider host); pattern-storage
requires config and tenant. Mount an LLM provider plugin
(ai-provider-anthropic, ai-provider-openai-compat, or a mock in tests)
alongside. The integration test under src/__tests__/ boots that exact stack
against real Postgres + Redis and scripts the provider — including the
3-invalid-attempts path — so every assertion above is proven, not described.
bun --env-file=../.env test --config=bunfig.integration.toml samples/recipes/ai-generateSource code
Section titled “Source code”The complete feature — embedded straight from the source file, so the code here is exactly what runs:
// AI-Generate Sample// Shows: a scaffolding workbench that consumes the enterprise `ai-generate`// feature server-side through ctx.query and lands the generated feature.ts// in pattern-storage — the same store the Designer edits. The generated// source becomes a reviewable pattern file, never a deployed feature.
import { defineFeature, withResponseData } from "@cosmicdrift/kumiko-framework/engine";import { failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";import { aiGenerateFeature, GENERATE_HANDLER_QN, type GenerateFeatureResult,} from "@cosmicdriftgamestudio/kumiko-ai-generate";import { patternStorageFeature } from "@cosmicdriftgamestudio/kumiko-pattern-storage";import { z } from "zod";
const PATTERN_CREATE_QN = "pattern-storage:write:pattern-file:create";
// ai-generate's query and pattern-storage's writes are both gated to// TenantAdmin/SystemAdmin, and ctx.query/ctx.write run as the CURRENT user —// so the bridging handler carries the same gate instead of escalating.const adminOnly = { access: { roles: ["TenantAdmin", "SystemAdmin"] } } as const;
export const featureScaffolderFeature = defineFeature("feature-scaffolder", (r) => { r.requires(aiGenerateFeature.name); r.requires(patternStorageFeature.name);
// The point of this sample: prompt → validated feature source → reviewable // pattern file. Storage is the only success path; every other union member // becomes a structured rejection. r.writeHandler( "scaffold", z.object({ prompt: z.string().min(1) }), async (event, ctx) => { // Cross-feature bridge: runs ai-generate's L3 validation loop (parse, // retry with tool_result feedback) as the current user. const result = (await ctx.query(GENERATE_HANDLER_QN, { userPrompt: event.payload.prompt, })) as GenerateFeatureResult; // boundary: ctx.query erases the target handler's return type
switch (result.type) { case "feature": { // The model names the feature; the path mirrors where hand-written // features live. pattern-storage derives its aggregate-id from // (tenantId, path), so re-scaffolding the same name is a 409 — // never a silent overwrite. const path = `features/${result.featureName}/feature.ts`; const stored = await ctx.write(PATTERN_CREATE_QN, { path, source: result.source, }); return withResponseData(stored, { path, featureName: result.featureName, rationale: result.rationale, attempts: result.attempts, }); } case "clarify": return failUnprocessable("scaffold needs clarification", { question: result.question, reason: result.reason, }); case "validation_failed": // The L3 loop retried and still could not produce parseable source — // surface the validator's findings, store nothing. return failUnprocessable("generated source failed validation", { errors: result.errors, attempts: result.attempts, }); case "error": return failUnprocessable("generation failed", { reason: result.reason }); } }, adminOnly, );});Enterprise recipe — samples/recipes/ai-generate/src/feature.ts in the private kumiko-enterprise workspace.