Skip to content

AI change proposals (ai-patch)

Let AI suggest edits to a stored feature.ts — and keep a human between suggestion and source. A small change-proposal feature reads the current source server-side from pattern-storage, bridges into the enterprise ai-patch feature via ctx.query, and stores the suggested changes as a reviewable proposal. Nothing is ever applied automatically.

  • Server-side source lookup, like the Designer — the client only names the path; the handler resolves it through pattern-storage’s deterministic (tenantId, path) aggregate-id and hands ai-patch the CURRENT stored source. Stale client copies can’t sneak into the prompt.
  • Proposals are data — the model’s PatternChange[] lands verbatim in a jsonb field next to the aggregated rationale; the integration test proves the round-trip. The review queue is just the entity’s list/detail handlers; there is deliberately no free-form create.
  • Handling the result union explicitlypatch is stored for review, clarify hands the model’s question back as a structured 422, error rejects without storing anything.
  • Access stays honest — ai-patch and pattern-storage are both admin-gated, and ctx.query runs as the current user, so the bridging handler carries the same gate instead of escalating through queryAs.

Whenever AI-suggested code changes need an approval step — Designer-style editing flows, migration assistants, config-as-code tooling. Pair it with the ai-generate recipe: generate creates the file, patch evolves it, a human reviews both.

ai-patch 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, seeds a pattern file over HTTP, and scripts the provider, so every assertion above is proven, not described.

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

The complete feature — embedded straight from the source file, so the code here is exactly what runs:

// AI-Patch Sample
// Shows: a change-proposal queue on top of the enterprise `ai-patch` feature.
// The handler reads the CURRENT source server-side from pattern-storage
// (exactly like the Designer does — the client only names the path), asks
// ai-patch for suggested changes, and stores them as a proposal for human
// review. Nothing is ever applied automatically.
import {
createEntity,
createEntityExecutor,
createJsonbField,
createLongTextField,
createTextField,
defineEntityDetailHandler,
defineEntityListHandler,
defineFeature,
} from "@cosmicdrift/kumiko-framework/engine";
import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
import {
aiPatchFeature,
SUGGEST_HANDLER_QN,
type SuggestPatchResult,
} from "@cosmicdriftgamestudio/kumiko-ai-patch";
import {
patternFileAggregateId,
patternStorageFeature,
} from "@cosmicdriftgamestudio/kumiko-pattern-storage";
import { z } from "zod";
const PATTERN_DETAIL_QN = "pattern-storage:query:pattern-file:detail";
export const proposalEntity = createEntity({
table: "read_sample_change_proposals",
fields: {
path: createTextField({ required: true }),
prompt: createLongTextField({ required: true, maxLength: 10_000 }),
rationale: createLongTextField({ maxLength: 10_000 }),
// The model's suggested PatternChange list, verbatim — the reviewer needs
// to see WHAT would change, so this is data, not a display artifact.
changes: createJsonbField(),
},
});
// ai-patch's query and pattern-storage's reads are both gated to
// TenantAdmin/SystemAdmin, and ctx.query runs 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 changeProposalsFeature = defineFeature("change-proposals", (r) => {
r.requires(aiPatchFeature.name);
r.requires(patternStorageFeature.name);
r.entity("change-proposal", proposalEntity);
const { executor } = createEntityExecutor("change-proposal", proposalEntity);
// Proposals are only born through `propose` — the review queue exposes
// read handlers, no free-form create.
r.queryHandler(defineEntityListHandler("change-proposal", proposalEntity, adminOnly));
r.queryHandler(defineEntityDetailHandler("change-proposal", proposalEntity, adminOnly));
// The point of this sample: name a stored file + describe the change,
// get a reviewable proposal back.
r.writeHandler(
"propose",
z.object({ path: z.string().min(1), prompt: z.string().min(1) }),
async (event, ctx) => {
// Server-side source lookup, like the Designer: the client names the
// path; the deterministic (tenantId, path) aggregate-id finds the file.
const aggregateId = patternFileAggregateId(event.user.tenantId, event.payload.path);
const file = (await ctx.query(PATTERN_DETAIL_QN, { id: aggregateId })) as {
source?: unknown;
} | null; // boundary: ctx.query erases the target handler's return type
if (!file || typeof file.source !== "string") {
return failNotFound("pattern-file", event.payload.path);
}
// Cross-feature bridge: ai-patch sees the CURRENT stored source, not
// whatever the client believes the source is.
const result = (await ctx.query(SUGGEST_HANDLER_QN, {
featureSourcePath: event.payload.path,
currentFeatureSource: file.source,
userPrompt: event.payload.prompt,
})) as SuggestPatchResult; // boundary: ctx.query erases the target handler's return type
switch (result.type) {
case "patch":
// Store the suggestion for review; applying it stays a separate,
// human-triggered step.
return executor.create(
{
path: event.payload.path,
prompt: event.payload.prompt,
rationale: result.rationale,
changes: result.changes,
},
event.user,
ctx.db,
);
case "clarify":
return failUnprocessable("proposal needs clarification", {
question: result.question,
reason: result.reason,
});
case "error":
return failUnprocessable("proposal failed", { reason: result.reason });
}
},
adminOnly,
);
});

Enterprise recipe — samples/recipes/ai-patch/src/feature.ts in the private kumiko-enterprise workspace.