Skip to content

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.

  • The generate → store pipelineai-generate runs its L3 validation loop (AST parse, retry with tool_result feedback) and returns source that is known to parse; the sample writes it to pattern-storage:write:pattern-file:create in the same transaction.
  • Handling all four union membersfeature is stored, clarify hands the model’s question back as a structured 422, validation_failed surfaces the validator’s per-line findings (and how many attempts the loop spent), error rejects 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 a 409 version conflict, proven in the test.
  • Access stays honest — ai-generate and pattern-storage are both admin-gated, and ctx.query/ctx.write run as the current user, so the bridging handler carries the same gate.

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.

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.

Terminal window
bun --env-file=../.env test --config=bunfig.integration.toml samples/recipes/ai-generate

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.