/** * KHAELOR * File: src/tools/design.ts * Description: The design tool — submit a DesignArtifact through the phase gate to unlock implement (v2 design §1). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import type { ToolDefinition } from "./registry.js"; import type { ToolContext, ToolDesignArtifact, ToolResult } from "./types.js"; export interface DesignInput { goal: string; approach: string; files: string; risks?: string; verification: string; } const DESCRIPTION = "Submit your design before implementing. KHAELOR works in three phases: understand → design → " + "implement. Write tools stay locked until a design is approved. Provide: goal (restate the need in " + "your own words), approach (the technical plan, 5-15 lines), files (one file path per line you plan " + 'to modify), risks (one per line; prefix a line with "out of scope:" to declare a non-goal), and ' + "verification (how you will prove it works). Small designs are auto-approved; larger ones may need " + "user approval."; const OUT_OF_SCOPE_PREFIX = /^out[ -]of[ -]scope:\s*/i; function splitLines(value: string | undefined): string[] { if (value === undefined) return []; return value .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0); } /** Parse the flat tool input into the structured artifact (risks vs out-of-scope). */ export function parseDesignInput(input: DesignInput): ToolDesignArtifact { const riskLines = splitLines(input.risks); const risks: string[] = []; const outOfScope: string[] = []; for (const line of riskLines) { const match = OUT_OF_SCOPE_PREFIX.exec(line); if (match !== null) outOfScope.push(line.slice(match[0].length)); else risks.push(line); } return { goal: input.goal.trim(), approach: input.approach.trim(), filesTouched: splitLines(input.files), risks, verification: input.verification.trim(), outOfScope, }; } async function executeDesign(input: DesignInput, ctx: ToolContext): Promise { const startedAt = Date.now(); const artifact = parseDesignInput(input); if (ctx.phases === undefined || ctx.phases.mode === "off") { return { content: "Design recorded (phase gates are off in this session — implementation was already unlocked).", metadata: { title: "Design recorded (gates off)", durationMs: Date.now() - startedAt }, }; } if (artifact.filesTouched.length === 0) { return { content: 'The "files" parameter must list at least one file you plan to modify (one path per line). ' + "If the task changes no files, say so in your answer instead of designing.", isError: true, metadata: { title: "Design rejected · no files", durationMs: Date.now() - startedAt }, }; } const decision = await ctx.phases.submitDesign(artifact); const fileCount = artifact.filesTouched.length; switch (decision.status) { case "approved": return { content: `Design approved (${fileCount} file${fileCount === 1 ? "" : "s"}). ` + "You are now in the implement phase — write, edit, and full bash are unlocked. " + "Follow your design; verify as planned before claiming completion.", metadata: { title: `Design approved · ${fileCount} file${fileCount === 1 ? "" : "s"}`, durationMs: Date.now() - startedAt, extra: { artifactId: decision.artifactId }, }, }; case "rejected": return { content: `Design rejected: ${decision.reason ?? "no reason given"}. ` + "Revise the design based on this feedback and submit again, or ask the user for direction.", isError: true, metadata: { title: "Design rejected", durationMs: Date.now() - startedAt }, }; case "pending": return { content: `Design submitted but not yet approved: ${decision.reason ?? "awaiting approval"}. ` + "Tell the user what you plan to do and wait for their approval before implementing.", metadata: { title: "Design pending approval", durationMs: Date.now() - startedAt }, }; } } /** Build the design tool (phase gates, v2 §1). */ export function createDesignTool(): ToolDefinition { return { name: "design", description: DESCRIPTION, inputSchema: { type: "object", properties: { goal: { type: "string", description: "The need, restated in your own words." }, approach: { type: "string", description: "Technical approach, 5-15 lines." }, files: { type: "string", description: "Files you plan to modify, one path per line." }, risks: { type: "string", description: 'Identified risks, one per line. Prefix with "out of scope:" for explicit non-goals.', }, verification: { type: "string", description: "How you will prove the change works." }, }, required: ["goal", "approach", "files", "verification"], }, capability: "file.read", execute: executeDesign, }; }