From 0b7d2239905d4bc46650b6b04af92a7032b1c737 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sat, 25 Apr 2026 21:50:06 +0200 Subject: [PATCH 01/27] feat: add compile build command using Copilot SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `build` subcommand to the spec compiler that uses `@github/copilot-sdk` to launch a Copilot agent session, feed it the generated compilation prompt, and stream the agent's work to the terminal. The build command: - Validates Copilot auth before starting (with clear error messages) - Prompts for model selection via `gum choose` (falls back to defaults) - Prompts for reasoning effort (skipped if model doesn't support it) - Creates an autopilot session (approveAll — no permission prompts) - Streams output in two modes: - Normal: compact phase-level status with tool activity - Verbose (--verbose): raw agent transcript with streaming deltas - Tracks metrics: tokens, tool calls, files written - Prints a compilation summary (time, files, LOC, tokens) - Auto-locks specs on success (skip with --no-lock) - Handles SIGINT for graceful cancellation New flags: --model, --effort, --verbose, --no-lock Existing commands (status, prompt, lock, clean) are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bun.lock | 19 ++ package.json | 1 + scripts/compile.ts | 624 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 631 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 46dc876..eb78b13 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "workspaces": { "": { "dependencies": { + "@github/copilot-sdk": "^0.3.0", "chalk": "^5.6.2", "fast-glob": "^3.3.3", "gray-matter": "^4.0.3", @@ -14,6 +15,22 @@ }, }, "packages": { + "@github/copilot": ["@github/copilot@1.0.36", "", { "optionalDependencies": { "@github/copilot-darwin-arm64": "1.0.36", "@github/copilot-darwin-x64": "1.0.36", "@github/copilot-linux-arm64": "1.0.36", "@github/copilot-linux-x64": "1.0.36", "@github/copilot-win32-arm64": "1.0.36", "@github/copilot-win32-x64": "1.0.36" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-x0N5wLzw+tANzb+vCFYLHn3BV3qii2oyn14wC20RO7SsS8/YeBH8olvwlDLJ4PB0mL17QOiytNCdkvjvprm28w=="], + + "@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@1.0.36", "", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-5qkb7frTS4K/LdTDLrzKo78VR4aw/EZ6JzLz4KfmaW4UYyPiNirExDFXa/By22X0o8YMfOp4MCA2KSCAxKdgTg=="], + + "@github/copilot-darwin-x64": ["@github/copilot-darwin-x64@1.0.36", "", { "os": "darwin", "cpu": "x64", "bin": { "copilot-darwin-x64": "copilot" } }, "sha512-AdsM8QtM5QSzMLpavLREh8HALO5G+VWzGNQqIHu4f0YQC/s1cGoiwo3wsgkpxRcLGBykFc+bDX3yK3MDQ8XvSw=="], + + "@github/copilot-linux-arm64": ["@github/copilot-linux-arm64@1.0.36", "", { "os": "linux", "cpu": "arm64", "bin": { "copilot-linux-arm64": "copilot" } }, "sha512-n7K1I6r0ggOJ4A9uAMS11USTvn6BKtAwvrOkzEaeRK89VNUJzpTe6p0mE13ItzRe5eot9WLBQOxvXLtL9f6E+g=="], + + "@github/copilot-linux-x64": ["@github/copilot-linux-x64@1.0.36", "", { "os": "linux", "cpu": "x64", "bin": { "copilot-linux-x64": "copilot" } }, "sha512-wBtCdR3ITZcq07BJbkwHfwI6ayiwbH5pF1ex+Ycl4UI+Lf1vP9eQD6wJppPgsrjwFcdeWRThaYTPCRTkSGHv5g=="], + + "@github/copilot-sdk": ["@github/copilot-sdk@0.3.0", "", { "dependencies": { "@github/copilot": "^1.0.36-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" } }, "sha512-SUo35k56pzzgYgwmDPHcu7kZxPrzXbH66IWXaEf6pmb94DlA709F82HrrDeja087TL4djJ9OuvRFWWOKCosAsg=="], + + "@github/copilot-win32-arm64": ["@github/copilot-win32-arm64@1.0.36", "", { "os": "win32", "cpu": "arm64", "bin": { "copilot-win32-arm64": "copilot.exe" } }, "sha512-0GzZUZQn07alI8BgbzK0NlR5+ta/Rd0sWmd8kbRCns7oybAIkSALy6BKVwJmVHtXUi6h4iUE8oiFhkn0spymvw=="], + + "@github/copilot-win32-x64": ["@github/copilot-win32-x64@1.0.36", "", { "os": "win32", "cpu": "x64", "bin": { "copilot-win32-x64": "copilot.exe" } }, "sha512-UBX9qj0McCK/SLq93XIr1i80fj3b3XmE3befVFrzxQuTeOoxLURN35vi7W+4x+4ZfsDHQpRTlJNjZw9w0fPr+Q=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -184,6 +201,8 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], diff --git a/package.json b/package.json index a763e54..4680619 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "compile": "bun scripts/compile.ts" }, "dependencies": { + "@github/copilot-sdk": "^0.3.0", "chalk": "^5.6.2", "fast-glob": "^3.3.3", "gray-matter": "^4.0.3", diff --git a/scripts/compile.ts b/scripts/compile.ts index 31969a5..4398705 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -3,19 +3,21 @@ * TUIkit spec compiler * * Detects changed specs via content hashing and generates self-contained - * compilation prompts for LLM agents. Lock files track which spec versions - * have been compiled per target. + * compilation prompts for LLM agents. The `build` command uses the Copilot SDK + * to launch an agent session that compiles specs into code automatically. * * Usage: - * bun run compile status [--target ] - * bun run compile prompt --target [--component ] - * bun run compile lock --target [--component ] | --all-targets - * bun run compile clean --target | --all-targets + * bun run compile status [--target ] + * bun run compile prompt --target [--component ] + * bun run compile build --target [--component ] [--model ] [--effort ] [--verbose] [--no-lock] + * bun run compile lock --target [--component ] | --all-targets + * bun run compile clean --target | --all-targets */ import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; +import { spawnSync } from "node:child_process"; import chalk from "chalk"; // biome-ignore lint/suspicious/noConsole: CLI tool — stdout is the interface @@ -58,6 +60,26 @@ interface LockFile { entries: Record; } +type ReasoningEffort = "low" | "medium" | "high" | "xhigh"; + +interface BuildConfig { + model: string; + effort: ReasoningEffort | undefined; + distDir: string; + supportsEffort: boolean; +} + +interface CompileMetrics { + startTime: number; + inputTokens: number; + outputTokens: number; + reasoningTokens: number; + toolCalls: number; + filesWritten: Set; + lastAssistantMessage: string; + errors: string[]; +} + // ── Helpers ──────────────────────────────────────────────────────────────── function sha256(content: string): string { @@ -355,6 +377,536 @@ function generatePrompt(target: string, specs: SpecEntry[], allSpecs: SpecEntry[ return sections.join("\n"); } +// ── Gum helpers ──────────────────────────────────────────────────────────── + +function hasGum(): boolean { + const result = spawnSync("gum", ["--version"], { stdio: "ignore" }); + return result.error === undefined && result.status === 0; +} + +function gum(args: string[], input?: string): string { + const result = spawnSync("gum", args, { + encoding: "utf-8", + stdio: [input ? "pipe" : "inherit", "pipe", "inherit"], + input, + }); + return (result.stdout ?? "").trim(); +} + +function gumStyle(text: string, opts: Record = {}): void { + const flags = Object.entries(opts).flatMap(([k, v]) => [`--${k}`, String(v)]); + spawnSync("gum", ["style", ...flags, text], { stdio: "inherit" }); +} + +function gumLog(level: string, msg: string): void { + spawnSync("gum", ["log", "--level", level, msg], { stdio: "inherit" }); +} + +// ── Build helpers ────────────────────────────────────────────────────────── + +function formatDuration(ms: number): string { + const secs = Math.floor(ms / 1000); + if (secs < 60) return `${secs}s`; + const mins = Math.floor(secs / 60); + const rem = secs % 60; + return `${mins}m ${rem}s`; +} + +function scanOutput(dir: string): { files: number; lines: number } { + if (!existsSync(dir)) return { files: 0, lines: 0 }; + let files = 0; + let lines = 0; + + function walk(d: string): void { + for (const entry of readdirSync(d)) { + if (entry.startsWith(".") || entry.startsWith("_")) continue; + const p = join(d, entry); + if (statSync(p).isDirectory()) { + walk(p); + } else { + files++; + lines += readFileSync(p, "utf-8").split("\n").length; + } + } + } + + walk(dir); + return { files, lines }; +} + +function summarizeArgs(args: unknown): string { + if (!args || typeof args !== "object") return ""; + const obj = args as Record; + const path = obj.path ?? obj.file_path ?? obj.command; + if (typeof path === "string") { + const short = path.length > 60 ? `…${path.slice(-57)}` : path; + return short; + } + return ""; +} + +function detectPhase(toolName: string, args: unknown): string { + const obj = (args ?? {}) as Record; + const path = String(obj.path ?? obj.file_path ?? ""); + const cmd = String(obj.command ?? ""); + + if (toolName === "read_file" || toolName === "view") { + if (path.includes("tokens/") || path.includes("components/") || path.includes("docs/")) { + return "Reading specs"; + } + return "Reading files"; + } + if (toolName === "edit_file" || toolName === "create_file" || toolName === "write_file") { + const match = path.match(/components\/(\w+)/); + if (match) return `Implementing ${match[1]}`; + if (path.includes("tokens/")) return "Implementing tokens"; + if (path.includes("demo")) return "Building demo"; + return "Writing files"; + } + if (toolName === "bash" || toolName === "shell") { + if (cmd.includes("test")) return "Running tests"; + if (cmd.includes("build") || cmd.includes("compile")) return "Building"; + if (cmd.includes("run")) return "Running"; + return "Executing command"; + } + if (toolName === "glob" || toolName === "grep") return "Searching files"; + return "Working"; +} + +// ── Build command ────────────────────────────────────────────────────────── + +async function ensureCopilotAuth(): Promise { + const { CopilotClient } = await import("@github/copilot-sdk"); + const client = new CopilotClient({ useLoggedInUser: true }); + + try { + await client.start(); + await client.ping(); + } catch (err) { + log(chalk.red("✗") + " Copilot authentication failed.\n"); + log(" The build command requires a valid GitHub Copilot subscription."); + log(" Try one of:\n"); + log(` ${chalk.cyan("copilot auth login")} Sign in via browser`); + log(` ${chalk.cyan("export GITHUB_TOKEN=ghp_...")} Use a personal access token`); + log(` ${chalk.cyan("export GH_TOKEN=ghp_...")} GitHub CLI token\n`); + if (err instanceof Error) log(chalk.dim(` Error: ${err.message}`)); + process.exit(1); + } + + return client; +} + +async function pickModel( + client: import("@github/copilot-sdk").CopilotClient, + useGum: boolean, +): Promise<{ id: string; name: string }> { + const models = await client.listModels(); + if (models.length === 0) { + log(chalk.red("✗") + " No models available. Check your Copilot subscription."); + process.exit(1); + } + + const defaultModel = models.find((m) => m.id === "claude-sonnet-4") ?? models[0]; + + if (!process.stdin.isTTY || !useGum) { + return { id: defaultModel.id, name: defaultModel.name }; + } + + const items = models.map((m) => `${m.name} (${m.id})`); + const selected = gum( + [ + "choose", + "--header", + "Select model:", + "--selected", + `${defaultModel.name} (${defaultModel.id})`, + ...items, + ], + ); + + const match = selected.match(/\(([^)]+)\)$/); + const id = match?.[1] ?? defaultModel.id; + const model = models.find((m) => m.id === id) ?? defaultModel; + return { id: model.id, name: model.name }; +} + +async function pickEffort(useGum: boolean): Promise { + if (!process.stdin.isTTY || !useGum) return "high"; + + const result = gum(["choose", "--header", "Reasoning effort:", "--selected", "high", "low", "medium", "high", "xhigh"]); + if (["low", "medium", "high", "xhigh"].includes(result)) return result as ReasoningEffort; + return "high"; +} + +async function promptBuildConfig( + client: import("@github/copilot-sdk").CopilotClient, + flags: { model?: string; effort?: string; out?: string }, +): Promise { + const useGum = hasGum(); + if (!useGum && process.stdin.isTTY) { + log(chalk.dim(" Tip: install gum for interactive prompts → brew install gum\n")); + } + + // Fetch available models to validate and check capabilities + const models = await client.listModels(); + + let model: { id: string; name: string }; + if (flags.model) { + const found = models.find((m) => m.id === flags.model); + if (!found) { + log(chalk.red("✗") + ` Model "${flags.model}" not found.`); + log(` Available: ${models.map((m) => m.id).join(", ")}`); + process.exit(1); + } + model = { id: found.id, name: found.name }; + } else { + model = await pickModel(client, useGum); + } + + // Check if model supports reasoning effort + const modelInfo = models.find((m) => m.id === model.id); + const supportsEffort = !!(modelInfo?.supportedReasoningEfforts && modelInfo.supportedReasoningEfforts.length > 0); + + let effort: ReasoningEffort | undefined; + if (supportsEffort) { + if (flags.effort && ["low", "medium", "high", "xhigh"].includes(flags.effort)) { + effort = flags.effort as ReasoningEffort; + } else { + effort = await pickEffort(useGum); + } + } else if (flags.effort) { + log(chalk.yellow("⚠") + ` Model "${model.id}" does not support reasoning effort — ignoring --effort flag.`); + } + + return { + model: model.id, + effort, + distDir: flags.out ? join(process.cwd(), flags.out) : DEFAULT_DIST_DIR, + supportsEffort, + }; +} + +function printBuildHeader(target: string, config: BuildConfig, useGum: boolean): void { + const effortStr = config.effort ? ` · Effort: ${config.effort}` : ""; + const header = [ + `TUIkit compiler`, + `Target: ${target} · Model: ${config.model}`, + `${effortStr ? effortStr.slice(3) : ""}Output: ${relative(SPECS_DIR, config.distDir)}/${target}/`, + ].join("\n"); + + if (useGum) { + gumStyle(header, { border: "rounded", padding: "1 2", "border-foreground": "6" }); + } else { + log(`\n${chalk.cyan("●")} ${chalk.bold("TUIkit compiler")}`); + log(` Target: ${chalk.bold(target)} · Model: ${chalk.bold(config.model)}${effortStr}`); + log(` Output: ${relative(SPECS_DIR, config.distDir)}/${target}/\n`); + } +} + +function printSummary( + target: string, + config: BuildConfig, + metrics: CompileMetrics, + outDir: string, + useGum: boolean, + noLock: boolean, +): void { + const elapsed = Date.now() - metrics.startTime; + const { files, lines } = scanOutput(outDir); + const totalTokens = metrics.inputTokens + metrics.outputTokens; + + const tokenDetail = + `(${metrics.inputTokens.toLocaleString()} in / ${metrics.outputTokens.toLocaleString()} out` + + `${metrics.reasoningTokens ? ` / ${metrics.reasoningTokens.toLocaleString()} reasoning` : ""})`; + + const body = [ + `✓ Compilation complete — target: ${target}`, + ``, + ` Model: ${config.model}${config.effort ? ` (${config.effort} effort)` : ""}`, + ` Time: ${formatDuration(elapsed)}`, + ` Files: ${files} written`, + ` LOC: ~${lines.toLocaleString()} lines`, + ` Tokens: ~${totalTokens.toLocaleString()} total ${tokenDetail}`, + ` Tools: ${metrics.toolCalls} calls`, + ``, + ` Output: ${relative(SPECS_DIR, outDir)}/`, + noLock ? ` Lock: skipped (--no-lock)` : ` Lock: ${relative(SPECS_DIR, lockPath(target))} updated`, + ].join("\n"); + + log(""); + if (useGum) { + gumStyle(body, { border: "rounded", padding: "1 2", "border-foreground": "2" }); + } else { + log(body); + } + log(""); +} + +async function cmdBuild( + target: string, + componentFilter?: string, + distDir: string = DEFAULT_DIST_DIR, + flagModel?: string, + flagEffort?: string, + verbose = false, + noLock = false, +): Promise { + const useGum = hasGum(); + const { approveAll } = await import("@github/copilot-sdk"); + + // 1. Discover dirty specs + const specs = discoverSpecs(); + const schemaHash = sha256(readFile(SCHEMA_PATH)); + const lock = readLock(target); + let dirty = computeDirty(specs, lock, schemaHash); + + if (componentFilter) { + dirty = dirty.filter( + (d) => d.spec.name === `components/${componentFilter}` || d.spec.name === `tokens/${componentFilter}`, + ); + } + + if (dirty.length === 0) { + log(`${chalk.green("✓")} No dirty specs for target "${target}". Nothing to compile.`); + return; + } + + // 2. Generate prompt (reuse existing logic) + const dirtySpecs = dirty.map((d) => d.spec); + const prompt = generatePrompt(target, dirtySpecs, specs, distDir); + const outDir = join(distDir, target); + mkdirSync(outDir, { recursive: true }); + const promptPath = join(outDir, "_compile-prompt.md"); + writeFileSync(promptPath, prompt); + + // 3. Auth + if (useGum) { + gumLog("info", "Authenticating with Copilot..."); + } else { + log(chalk.dim(" Authenticating with Copilot...")); + } + const client = await ensureCopilotAuth(); + + // 4. Config (model/effort) + const config = await promptBuildConfig(client, { + model: flagModel, + effort: flagEffort, + out: distDir !== DEFAULT_DIST_DIR ? relative(process.cwd(), distDir) : undefined, + }); + + // 5. Print header + printBuildHeader(target, config, useGum); + log(` ${chalk.dim(`${dirty.length} dirty specs to compile`)}\n`); + + // 6. Metrics + const metrics: CompileMetrics = { + startTime: Date.now(), + inputTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + toolCalls: 0, + filesWritten: new Set(), + lastAssistantMessage: "", + errors: [], + }; + + // 7. Create session + const sessionConfig: Record = { + model: config.model, + onPermissionRequest: approveAll, + streaming: true, + systemMessage: { + content: ` + +You are a TUIkit spec compiler. Your job is to read component specifications +and generate idiomatic code for the target framework. + +Working directory: ${SPECS_DIR} +Output directory: ${relative(SPECS_DIR, outDir)} + +IMPORTANT: +- Do NOT spawn sub-agents or delegate to the task tool. Do ALL work yourself directly. +- Do NOT ask the user questions. Proceed with your best judgment. +- Read ALL referenced spec files from disk before implementing. +- Output all generated code to the specified output directory. +- Run tests after implementation and fix any failures. + +`, + }, + }; + if (config.effort && config.supportsEffort) { + sessionConfig.reasoningEffort = config.effort; + } + + let session: Awaited>; + try { + // biome-ignore lint/suspicious/noExplicitAny: SDK config types are complex + session = await client.createSession(sessionConfig as any); + } catch (err) { + log(chalk.red("✗") + " Failed to create agent session."); + if (err instanceof Error) log(chalk.dim(` Error: ${err.message}`)); + log("\n This could mean:"); + log(" • The model is unavailable or unsupported"); + log(" • Your Copilot subscription doesn't include this model"); + log(" • A transient service error — try again\n"); + await client.stop(); + process.exit(1); + } + + // 8. SIGINT handler + let aborted = false; + const sigintHandler = async () => { + if (aborted) return; + aborted = true; + log(chalk.yellow("\n\n⚠ Compilation interrupted")); + try { + await session.abort(); + await session.disconnect(); + await client.stop(); + } catch { + /* best-effort cleanup */ + } + process.exit(130); + }; + process.on("SIGINT", sigintHandler); + + // 9. Event handlers + let currentPhase = "Starting"; + + if (verbose) { + // ── Verbose mode: raw transcript ── + session.on("assistant.message_delta", (event) => { + process.stdout.write(event.data.deltaContent); + }); + + session.on("assistant.reasoning_delta", (event) => { + process.stdout.write(chalk.dim(event.data.deltaContent)); + }); + + session.on("tool.execution_start", (event) => { + const { toolName } = event.data; + const argStr = summarizeArgs(event.data.arguments); + log(chalk.dim(`\n⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`)); + }); + + session.on("tool.execution_complete", (event) => { + const icon = event.data.success ? chalk.green("✓") : chalk.red("✗"); + const toolId = event.data.toolCallId.slice(0, 8); + log(chalk.dim(` ${icon} ${toolId}`)); + }); + } else { + // ── Normal mode: compact status ── + session.on("assistant.message_delta", () => { + // Suppress in normal mode — we show phase-level status instead + }); + + session.on("tool.execution_start", (event) => { + const { toolName } = event.data; + const argStr = summarizeArgs(event.data.arguments); + const phase = detectPhase(toolName, event.data.arguments); + + if (phase !== currentPhase) { + // Complete previous phase + if (currentPhase !== "Starting") { + if (useGum) { + gumLog("info", `✓ ${currentPhase}`); + } else { + log(` ${chalk.green("✓")} ${currentPhase}`); + } + } + currentPhase = phase; + } + + // Show current tool activity + if (useGum) { + gumLog("debug", ` ⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`); + } else { + log(chalk.dim(` ⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`)); + } + }); + } + + // Common event handlers for both modes + session.on("assistant.message", (event) => { + metrics.lastAssistantMessage = event.data.content; + }); + + session.on("assistant.usage", (event) => { + metrics.inputTokens += event.data.inputTokens ?? 0; + metrics.outputTokens += event.data.outputTokens ?? 0; + metrics.reasoningTokens += event.data.reasoningTokens ?? 0; + }); + + session.on("tool.execution_start", (event) => { + metrics.toolCalls++; + const { toolName } = event.data; + const args = event.data.arguments as Record | undefined; + if ( + (toolName === "edit_file" || toolName === "create_file" || toolName === "write_file") && + args + ) { + const filePath = (args.path ?? args.file_path) as string | undefined; + if (filePath) metrics.filesWritten.add(filePath); + } + }); + + session.on("session.error", (event) => { + const msg = (event.data as { message?: string }).message ?? "Unknown error"; + metrics.errors.push(msg); + if (verbose) { + log(chalk.red(`\n✗ Session error: ${msg}`)); + } else { + if (useGum) { + gumLog("error", msg); + } else { + log(` ${chalk.red("✗")} ${msg}`); + } + } + }); + + // 10. Send prompt and wait for idle + const done = new Promise((resolve) => { + session.on("session.idle", () => resolve()); + }); + + await session.send({ prompt }); + await done; + + // 11. Complete final phase in normal mode + if (!verbose && currentPhase !== "Starting") { + if (useGum) { + gumLog("info", `✓ ${currentPhase}`); + } else { + log(` ${chalk.green("✓")} ${currentPhase}`); + } + } + + // 12. Auto-lock (unless --no-lock or errors occurred) + if (!noLock && metrics.errors.length === 0) { + cmdLock(target, componentFilter); + } + + // 13. Summary + printSummary(target, config, metrics, outDir, useGum, noLock); + + if (metrics.errors.length > 0) { + log(chalk.yellow("⚠ Completed with errors:")); + for (const err of metrics.errors) { + log(` ${chalk.red("•")} ${err}`); + } + log(""); + } + + // 14. Cleanup + try { + await session.disconnect(); + await client.stop(); + } catch { + /* best-effort */ + } + process.removeListener("SIGINT", sigintHandler); +} + // ── Commands ─────────────────────────────────────────────────────────────── function cmdStatus(targetFilter?: string): void { @@ -485,36 +1037,62 @@ function cmdClean(target: string, distDir: string = DEFAULT_DIST_DIR): void { function usage(): void { log(` -TUIkit spec compiler — detect changes, generate prompts, track state. +TUIkit spec compiler — detect changes, generate prompts, compile via Copilot SDK. Commands: status [--target ] Show dirty/clean status prompt --target [--component ] Generate compilation prompt --all-targets Generate prompts for all targets + build --target [--component ] Compile specs via Copilot SDK agent + --all-targets Build all targets sequentially lock --target [--component ] Snapshot spec hashes to lock file --all-targets Lock all targets clean --target Remove lock file + prompt --all-targets Clean all targets -Options: - --out Output directory for compiled code (default: specs/dist/) +Build options: + --model Model to use (e.g. claude-sonnet-4, gpt-5). Prompts if omitted. + --effort Reasoning effort: low | medium | high | xhigh (default: high) + --verbose Show full agent transcript (raw streaming output) + --no-lock Skip auto-lock after successful build + +Common options: + --out Output directory for compiled code (default: dist/) Examples: bun run compile status bun run compile prompt --target go - bun run compile prompt --target go --out ./my-tuikit - bun run compile prompt --target rust --component HintBar + bun run compile build --target go + bun run compile build --target rust --model claude-sonnet-4 --effort high --verbose + bun run compile build --target node --component HintBar + bun run compile build --all-targets --model gpt-5 --effort xhigh bun run compile lock --target go bun run compile clean --target bun `); } -function parseArgs(argv: string[]): { command: string; target?: string; allTargets: boolean; component?: string; out?: string } { +interface ParsedArgs { + command: string; + target?: string; + allTargets: boolean; + component?: string; + out?: string; + model?: string; + effort?: string; + verbose: boolean; + noLock: boolean; +} + +function parseArgs(argv: string[]): ParsedArgs { const command = argv[0] || "status"; let target: string | undefined; let allTargets = false; let component: string | undefined; let out: string | undefined; + let model: string | undefined; + let effort: string | undefined; + let verbose = false; + let noLock = false; for (let i = 1; i < argv.length; i++) { if (argv[i] === "--target" && argv[i + 1]) { @@ -525,10 +1103,18 @@ function parseArgs(argv: string[]): { command: string; target?: string; allTarge component = argv[++i]; } else if (argv[i] === "--out" && argv[i + 1]) { out = argv[++i]; + } else if (argv[i] === "--model" && argv[i + 1]) { + model = argv[++i]; + } else if (argv[i] === "--effort" && argv[i + 1]) { + effort = argv[++i]; + } else if (argv[i] === "--verbose") { + verbose = true; + } else if (argv[i] === "--no-lock") { + noLock = true; } } - return { command, target, allTargets, component, out }; + return { command, target, allTargets, component, out, model, effort, verbose, noLock }; } const args = parseArgs(process.argv.slice(2)); @@ -552,6 +1138,18 @@ switch (args.command) { process.exit(1); } break; + case "build": + if (args.allTargets) { + for (const t of discoverTargets()) { + await cmdBuild(t, args.component, distDir, args.model, args.effort, args.verbose, args.noLock); + } + } else if (args.target) { + await cmdBuild(args.target, args.component, distDir, args.model, args.effort, args.verbose, args.noLock); + } else { + log("Error: --target or --all-targets is required for build command"); + process.exit(1); + } + break; case "lock": if (args.allTargets) { for (const t of discoverTargets()) { From f35f9d312b306b841fc382d45242ff31ff9490fb Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sun, 26 Apr 2026 21:38:31 +0200 Subject: [PATCH 02/27] feat: always prompt for model, effort, and output dir with good defaults Flags (--model, --effort, --out) now pre-select the default value in the interactive prompt rather than skipping it entirely. Adds a new output directory picker via `gum input`. Flow order changed: auth runs first (fail fast), then interactive config, then prompt generation uses the user-chosen distDir. Non-TTY / no gum: falls back to sensible defaults silently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 105 +++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 4398705..dc6f535 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -499,6 +499,7 @@ async function ensureCopilotAuth(): Promise { const models = await client.listModels(); if (models.length === 0) { @@ -506,7 +507,10 @@ async function pickModel( process.exit(1); } - const defaultModel = models.find((m) => m.id === "claude-sonnet-4") ?? models[0]; + const defaultModel = + (preselected ? models.find((m) => m.id === preselected) : undefined) ?? + models.find((m) => m.id === "claude-sonnet-4") ?? + models[0]; if (!process.stdin.isTTY || !useGum) { return { id: defaultModel.id, name: defaultModel.name }; @@ -530,17 +534,35 @@ async function pickModel( return { id: model.id, name: model.name }; } -async function pickEffort(useGum: boolean): Promise { - if (!process.stdin.isTTY || !useGum) return "high"; +async function pickEffort(useGum: boolean, preselected?: string): Promise { + const defaultEffort = (preselected && ["low", "medium", "high", "xhigh"].includes(preselected)) + ? preselected + : "high"; - const result = gum(["choose", "--header", "Reasoning effort:", "--selected", "high", "low", "medium", "high", "xhigh"]); + if (!process.stdin.isTTY || !useGum) return defaultEffort as ReasoningEffort; + + const result = gum(["choose", "--header", "Reasoning effort:", "--selected", defaultEffort, "low", "medium", "high", "xhigh"]); if (["low", "medium", "high", "xhigh"].includes(result)) return result as ReasoningEffort; - return "high"; + return defaultEffort as ReasoningEffort; +} + +async function pickOutputDir(useGum: boolean, target: string, preselected?: string): Promise { + const defaultDir = preselected + ? join(process.cwd(), preselected) + : DEFAULT_DIST_DIR; + const displayDefault = relative(SPECS_DIR, defaultDir) || "."; + + if (!process.stdin.isTTY || !useGum) return defaultDir; + + const result = gum(["input", "--header", "Output directory:", "--value", displayDefault]); + if (!result || result === displayDefault) return defaultDir; + return join(SPECS_DIR, result); } async function promptBuildConfig( client: import("@github/copilot-sdk").CopilotClient, flags: { model?: string; effort?: string; out?: string }, + target: string, ): Promise { const useGum = hasGum(); if (!useGum && process.stdin.isTTY) { @@ -550,40 +572,23 @@ async function promptBuildConfig( // Fetch available models to validate and check capabilities const models = await client.listModels(); - let model: { id: string; name: string }; - if (flags.model) { - const found = models.find((m) => m.id === flags.model); - if (!found) { - log(chalk.red("✗") + ` Model "${flags.model}" not found.`); - log(` Available: ${models.map((m) => m.id).join(", ")}`); - process.exit(1); - } - model = { id: found.id, name: found.name }; - } else { - model = await pickModel(client, useGum); - } + // Always prompt for model (flag value becomes the pre-selected default) + const model = await pickModel(client, useGum, flags.model); // Check if model supports reasoning effort const modelInfo = models.find((m) => m.id === model.id); const supportsEffort = !!(modelInfo?.supportedReasoningEfforts && modelInfo.supportedReasoningEfforts.length > 0); + // Always prompt for effort if model supports it (flag becomes default) let effort: ReasoningEffort | undefined; if (supportsEffort) { - if (flags.effort && ["low", "medium", "high", "xhigh"].includes(flags.effort)) { - effort = flags.effort as ReasoningEffort; - } else { - effort = await pickEffort(useGum); - } - } else if (flags.effort) { - log(chalk.yellow("⚠") + ` Model "${model.id}" does not support reasoning effort — ignoring --effort flag.`); + effort = await pickEffort(useGum, flags.effort); } - return { - model: model.id, - effort, - distDir: flags.out ? join(process.cwd(), flags.out) : DEFAULT_DIST_DIR, - supportsEffort, - }; + // Always prompt for output location (flag or dist/ as default) + const distDir = await pickOutputDir(useGum, target, flags.out); + + return { model: model.id, effort, distDir, supportsEffort }; } function printBuildHeader(target: string, config: BuildConfig, useGum: boolean): void { @@ -645,7 +650,7 @@ function printSummary( async function cmdBuild( target: string, componentFilter?: string, - distDir: string = DEFAULT_DIST_DIR, + _distDir: string = DEFAULT_DIST_DIR, flagModel?: string, flagEffort?: string, verbose = false, @@ -654,7 +659,7 @@ async function cmdBuild( const useGum = hasGum(); const { approveAll } = await import("@github/copilot-sdk"); - // 1. Discover dirty specs + // 1. Quick check — any dirty specs at all? const specs = discoverSpecs(); const schemaHash = sha256(readFile(SCHEMA_PATH)); const lock = readLock(target); @@ -671,15 +676,7 @@ async function cmdBuild( return; } - // 2. Generate prompt (reuse existing logic) - const dirtySpecs = dirty.map((d) => d.spec); - const prompt = generatePrompt(target, dirtySpecs, specs, distDir); - const outDir = join(distDir, target); - mkdirSync(outDir, { recursive: true }); - const promptPath = join(outDir, "_compile-prompt.md"); - writeFileSync(promptPath, prompt); - - // 3. Auth + // 2. Auth first (fail fast before interactive prompts) if (useGum) { gumLog("info", "Authenticating with Copilot..."); } else { @@ -687,12 +684,26 @@ async function cmdBuild( } const client = await ensureCopilotAuth(); - // 4. Config (model/effort) - const config = await promptBuildConfig(client, { - model: flagModel, - effort: flagEffort, - out: distDir !== DEFAULT_DIST_DIR ? relative(process.cwd(), distDir) : undefined, - }); + // 3. Interactive config — always prompts with good defaults + // Flags pre-select the default; user can still change it. + const config = await promptBuildConfig( + client, + { + model: flagModel, + effort: flagEffort, + out: _distDir !== DEFAULT_DIST_DIR ? relative(process.cwd(), _distDir) : undefined, + }, + target, + ); + + // 4. Generate prompt (uses config.distDir chosen by the user) + const distDir = config.distDir; + const dirtySpecs = dirty.map((d) => d.spec); + const prompt = generatePrompt(target, dirtySpecs, specs, distDir); + const outDir = join(distDir, target); + mkdirSync(outDir, { recursive: true }); + const promptPath = join(outDir, "_compile-prompt.md"); + writeFileSync(promptPath, prompt); // 5. Print header printBuildHeader(target, config, useGum); From 790e3fe872d08360e98aa4093c12a0f944a20fb2 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sun, 26 Apr 2026 21:41:17 +0200 Subject: [PATCH 03/27] feat: add multi-pass compilation loop with improvement prompts After each compilation pass, the user is prompted to run another pass. Subsequent passes send the agent a focused improvement prompt that re-reads specs and fixes missed implementations, failing tests, and inconsistencies. - printSummary now shows pass number and cumulative pass count - Session stays alive between passes (only disconnects on exit) - gum confirm with readline fallback for pass prompt - Auto-lock deferred until all passes complete Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 115 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 16 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index dc6f535..a1a0a42 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -475,6 +475,20 @@ function detectPhase(toolName: string, args: unknown): string { // ── Build command ────────────────────────────────────────────────────────── +function confirmPass(): Promise { + return new Promise((resolve) => { + const rl = require("node:readline").createInterface({ + input: process.stdin, + output: process.stdout, + }); + rl.question(`\n Do another pass? ${chalk.dim("[Y/n]")} `, (answer: string) => { + rl.close(); + const a = answer.trim().toLowerCase(); + resolve(a === "" || a === "y" || a === "yes"); + }); + }); +} + async function ensureCopilotAuth(): Promise { const { CopilotClient } = await import("@github/copilot-sdk"); const client = new CopilotClient({ useLoggedInUser: true }); @@ -615,6 +629,7 @@ function printSummary( outDir: string, useGum: boolean, noLock: boolean, + passNumber = 1, ): void { const elapsed = Date.now() - metrics.startTime; const { files, lines } = scanOutput(outDir); @@ -624,15 +639,17 @@ function printSummary( `(${metrics.inputTokens.toLocaleString()} in / ${metrics.outputTokens.toLocaleString()} out` + `${metrics.reasoningTokens ? ` / ${metrics.reasoningTokens.toLocaleString()} reasoning` : ""})`; + const passLabel = passNumber > 1 ? ` (pass ${passNumber})` : ""; const body = [ - `✓ Compilation complete — target: ${target}`, + `✓ Compilation complete — target: ${target}${passLabel}`, ``, ` Model: ${config.model}${config.effort ? ` (${config.effort} effort)` : ""}`, ` Time: ${formatDuration(elapsed)}`, - ` Files: ${files} written`, + ` Files: ${files} in output`, ` LOC: ~${lines.toLocaleString()} lines`, ` Tokens: ~${totalTokens.toLocaleString()} total ${tokenDetail}`, ` Tools: ${metrics.toolCalls} calls`, + ` Passes: ${passNumber}`, ``, ` Output: ${relative(SPECS_DIR, outDir)}/`, noLock ? ` Lock: skipped (--no-lock)` : ` Lock: ${relative(SPECS_DIR, lockPath(target))} updated`, @@ -875,15 +892,21 @@ IMPORTANT: } }); - // 10. Send prompt and wait for idle - const done = new Promise((resolve) => { - session.on("session.idle", () => resolve()); - }); + // 10. Send prompt and wait for idle — with multi-pass loop + let passNumber = 1; + + const waitForIdle = (): Promise => + new Promise((resolve) => { + const unsub = session.on("session.idle", () => { + unsub(); + resolve(); + }); + }); await session.send({ prompt }); - await done; + await waitForIdle(); - // 11. Complete final phase in normal mode + // Complete final phase in normal mode if (!verbose && currentPhase !== "Starting") { if (useGum) { gumLog("info", `✓ ${currentPhase}`); @@ -892,13 +915,8 @@ IMPORTANT: } } - // 12. Auto-lock (unless --no-lock or errors occurred) - if (!noLock && metrics.errors.length === 0) { - cmdLock(target, componentFilter); - } - - // 13. Summary - printSummary(target, config, metrics, outDir, useGum, noLock); + // Show summary for this pass + printSummary(target, config, metrics, outDir, useGum, noLock, passNumber); if (metrics.errors.length > 0) { log(chalk.yellow("⚠ Completed with errors:")); @@ -908,7 +926,72 @@ IMPORTANT: log(""); } - // 14. Cleanup + // 11. Multi-pass loop — offer to do another pass + while (process.stdin.isTTY && !aborted) { + let wantMore: boolean; + if (useGum) { + const r = spawnSync("gum", ["confirm", "--default=yes", "Do another pass? (improves consistency)"], { stdio: "inherit" }); + wantMore = r.status === 0; + } else { + wantMore = await confirmPass(); + } + + if (!wantMore) break; + + passNumber++; + currentPhase = "Starting"; + // Reset per-pass metrics (keep cumulative totals) + const prevTokensIn = metrics.inputTokens; + const prevTokensOut = metrics.outputTokens; + const prevReasoning = metrics.reasoningTokens; + const prevToolCalls = metrics.toolCalls; + metrics.errors = []; + + log(`\n${chalk.cyan("●")} Pass ${passNumber} — sending improvement prompt...\n`); + + await session.send({ + prompt: [ + "Do another pass over the compilation output.", + "Re-read the original spec files and the compile prompt at " + + `\`${relative(SPECS_DIR, promptPath)}\` to check what you may have missed.`, + "Focus on:", + "- Missing or incomplete component implementations", + "- Tests that are failing or missing", + "- Inconsistencies between the spec and the generated code", + "- Demo wiring for any new components", + "- Token usage correctness", + "After fixing, run the tests again and report results.", + ].join("\n"), + }); + + await waitForIdle(); + + // Complete final phase + if (!verbose && currentPhase !== "Starting") { + if (useGum) { + gumLog("info", `✓ ${currentPhase}`); + } else { + log(` ${chalk.green("✓")} ${currentPhase}`); + } + } + + printSummary(target, config, metrics, outDir, useGum, noLock, passNumber); + + if (metrics.errors.length > 0) { + log(chalk.yellow("⚠ Pass completed with errors:")); + for (const err of metrics.errors) { + log(` ${chalk.red("•")} ${err}`); + } + log(""); + } + } + + // 12. Auto-lock (unless --no-lock or errors occurred) + if (!noLock && metrics.errors.length === 0) { + cmdLock(target, componentFilter); + } + + // 13. Cleanup try { await session.disconnect(); await client.stop(); From 1ec27abd1591d92fe7b7a4ed4e5cf1497b418777 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sun, 26 Apr 2026 21:50:12 +0200 Subject: [PATCH 04/27] fix: count only agent-written files in build metrics Replace scanOutput() filesystem walk with countAgentOutput() that uses the tracked filesWritten set from tool execution events. This excludes node_modules and other non-agent files from the count. Also track file deletions separately so the summary shows 'N written, M deleted' when files are removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 47 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index a1a0a42..eb0bb2c 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -76,6 +76,7 @@ interface CompileMetrics { reasoningTokens: number; toolCalls: number; filesWritten: Set; + filesDeleted: Set; lastAssistantMessage: string; errors: string[]; } @@ -412,25 +413,15 @@ function formatDuration(ms: number): string { return `${mins}m ${rem}s`; } -function scanOutput(dir: string): { files: number; lines: number } { - if (!existsSync(dir)) return { files: 0, lines: 0 }; - let files = 0; +/** Count LOC across files the agent actually wrote (ignores node_modules etc.) */ +function countAgentOutput(filesWritten: Set): { files: number; lines: number } { let lines = 0; - - function walk(d: string): void { - for (const entry of readdirSync(d)) { - if (entry.startsWith(".") || entry.startsWith("_")) continue; - const p = join(d, entry); - if (statSync(p).isDirectory()) { - walk(p); - } else { - files++; - lines += readFileSync(p, "utf-8").split("\n").length; - } - } + let files = 0; + for (const fp of filesWritten) { + if (!existsSync(fp)) continue; + files++; + lines += readFileSync(fp, "utf-8").split("\n").length; } - - walk(dir); return { files, lines }; } @@ -632,7 +623,8 @@ function printSummary( passNumber = 1, ): void { const elapsed = Date.now() - metrics.startTime; - const { files, lines } = scanOutput(outDir); + const { files, lines } = countAgentOutput(metrics.filesWritten); + const deleted = metrics.filesDeleted.size; const totalTokens = metrics.inputTokens + metrics.outputTokens; const tokenDetail = @@ -640,12 +632,13 @@ function printSummary( `${metrics.reasoningTokens ? ` / ${metrics.reasoningTokens.toLocaleString()} reasoning` : ""})`; const passLabel = passNumber > 1 ? ` (pass ${passNumber})` : ""; + const filesLine = deleted > 0 ? `${files} written, ${deleted} deleted` : `${files} written`; const body = [ `✓ Compilation complete — target: ${target}${passLabel}`, ``, ` Model: ${config.model}${config.effort ? ` (${config.effort} effort)` : ""}`, ` Time: ${formatDuration(elapsed)}`, - ` Files: ${files} in output`, + ` Files: ${filesLine}`, ` LOC: ~${lines.toLocaleString()} lines`, ` Tokens: ~${totalTokens.toLocaleString()} total ${tokenDetail}`, ` Tools: ${metrics.toolCalls} calls`, @@ -734,6 +727,7 @@ async function cmdBuild( reasoningTokens: 0, toolCalls: 0, filesWritten: new Set(), + filesDeleted: new Set(), lastAssistantMessage: "", errors: [], }; @@ -869,12 +863,17 @@ IMPORTANT: metrics.toolCalls++; const { toolName } = event.data; const args = event.data.arguments as Record | undefined; - if ( - (toolName === "edit_file" || toolName === "create_file" || toolName === "write_file") && - args - ) { + if (args) { const filePath = (args.path ?? args.file_path) as string | undefined; - if (filePath) metrics.filesWritten.add(filePath); + if ( + (toolName === "edit_file" || toolName === "create_file" || toolName === "write_file") && + filePath + ) { + metrics.filesWritten.add(filePath); + } else if (toolName === "delete_file" && filePath) { + metrics.filesDeleted.add(filePath); + metrics.filesWritten.delete(filePath); + } } }); From 5d6d4dcf95dee868895fbdf559616d55cca529b2 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sun, 26 Apr 2026 21:52:21 +0200 Subject: [PATCH 05/27] fix: show lock file path when no dirty specs found Helps users understand which lock file is keeping specs clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/compile.ts b/scripts/compile.ts index eb0bb2c..f01d862 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -683,6 +683,7 @@ async function cmdBuild( if (dirty.length === 0) { log(`${chalk.green("✓")} No dirty specs for target "${target}". Nothing to compile.`); + log(` ${chalk.dim(`Lock: ${relative(SPECS_DIR, lockPath(target))}`)}`); return; } @@ -1050,6 +1051,7 @@ function cmdPrompt(target: string, componentFilter?: string, distDir: string = D if (dirty.length === 0) { log(`${chalk.green("✓")} No dirty specs for target "${target}".`); + log(` ${chalk.dim(`Lock: ${relative(SPECS_DIR, lockPath(target))}`)}`); return; } From 65b278a299fd794a27b31208423f55d45177d3e9 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sun, 26 Apr 2026 21:55:41 +0200 Subject: [PATCH 06/27] refactor: replace gum log with chalk for all status output Use chalk-based console.log consistently for compilation status messages instead of shelling out to gum log. Removes gumLog helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 40 ++++++---------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index f01d862..4864158 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -399,10 +399,6 @@ function gumStyle(text: string, opts: Record = {}): voi spawnSync("gum", ["style", ...flags, text], { stdio: "inherit" }); } -function gumLog(level: string, msg: string): void { - spawnSync("gum", ["log", "--level", level, msg], { stdio: "inherit" }); -} - // ── Build helpers ────────────────────────────────────────────────────────── function formatDuration(ms: number): string { @@ -688,11 +684,7 @@ async function cmdBuild( } // 2. Auth first (fail fast before interactive prompts) - if (useGum) { - gumLog("info", "Authenticating with Copilot..."); - } else { - log(chalk.dim(" Authenticating with Copilot...")); - } + log(chalk.dim(" Authenticating with Copilot...")); const client = await ensureCopilotAuth(); // 3. Interactive config — always prompts with good defaults @@ -831,21 +823,13 @@ IMPORTANT: if (phase !== currentPhase) { // Complete previous phase if (currentPhase !== "Starting") { - if (useGum) { - gumLog("info", `✓ ${currentPhase}`); - } else { - log(` ${chalk.green("✓")} ${currentPhase}`); - } + log(` ${chalk.green("✓")} ${currentPhase}`); } currentPhase = phase; } // Show current tool activity - if (useGum) { - gumLog("debug", ` ⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`); - } else { - log(chalk.dim(` ⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`)); - } + log(chalk.dim(` ⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`)); }); } @@ -884,11 +868,7 @@ IMPORTANT: if (verbose) { log(chalk.red(`\n✗ Session error: ${msg}`)); } else { - if (useGum) { - gumLog("error", msg); - } else { - log(` ${chalk.red("✗")} ${msg}`); - } + log(` ${chalk.red("✗")} ${msg}`); } }); @@ -908,11 +888,7 @@ IMPORTANT: // Complete final phase in normal mode if (!verbose && currentPhase !== "Starting") { - if (useGum) { - gumLog("info", `✓ ${currentPhase}`); - } else { - log(` ${chalk.green("✓")} ${currentPhase}`); - } + log(` ${chalk.green("✓")} ${currentPhase}`); } // Show summary for this pass @@ -968,11 +944,7 @@ IMPORTANT: // Complete final phase if (!verbose && currentPhase !== "Starting") { - if (useGum) { - gumLog("info", `✓ ${currentPhase}`); - } else { - log(` ${chalk.green("✓")} ${currentPhase}`); - } + log(` ${chalk.green("✓")} ${currentPhase}`); } printSummary(target, config, metrics, outDir, useGum, noLock, passNumber); From 875829a9b754813ca5de8f865bbbe22fb1026315 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sun, 26 Apr 2026 23:23:38 +0200 Subject: [PATCH 07/27] fix: broaden tool name matching for file metrics and phase detection The Copilot SDK agent may use varying tool names (edit, create, write_to_file, str_replace_editor, etc.) depending on the model. Use substring matching on normalized tool names instead of exact string comparisons so file writes are always tracked. Also broadens the path argument lookup to check path, file_path, filePath, and file argument names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 4864158..def851c 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -434,29 +434,31 @@ function summarizeArgs(args: unknown): string { function detectPhase(toolName: string, args: unknown): string { const obj = (args ?? {}) as Record; - const path = String(obj.path ?? obj.file_path ?? ""); + const path = String(obj.path ?? obj.file_path ?? obj.filePath ?? obj.file ?? ""); const cmd = String(obj.command ?? ""); + const tn = toolName.toLowerCase(); - if (toolName === "read_file" || toolName === "view") { + if (tn.includes("read") || tn === "view") { if (path.includes("tokens/") || path.includes("components/") || path.includes("docs/")) { return "Reading specs"; } return "Reading files"; } - if (toolName === "edit_file" || toolName === "create_file" || toolName === "write_file") { + if (tn.includes("edit") || tn.includes("create") || tn.includes("write")) { const match = path.match(/components\/(\w+)/); if (match) return `Implementing ${match[1]}`; if (path.includes("tokens/")) return "Implementing tokens"; if (path.includes("demo")) return "Building demo"; return "Writing files"; } - if (toolName === "bash" || toolName === "shell") { + if (tn === "bash" || tn === "shell" || tn.includes("terminal") || tn.includes("command")) { if (cmd.includes("test")) return "Running tests"; if (cmd.includes("build") || cmd.includes("compile")) return "Building"; if (cmd.includes("run")) return "Running"; return "Executing command"; } - if (toolName === "glob" || toolName === "grep") return "Searching files"; + if (tn === "glob" || tn === "grep" || tn.includes("search") || tn.includes("find")) return "Searching files"; + if (tn.includes("delete")) return "Cleaning up"; return "Working"; } @@ -849,15 +851,27 @@ IMPORTANT: const { toolName } = event.data; const args = event.data.arguments as Record | undefined; if (args) { - const filePath = (args.path ?? args.file_path) as string | undefined; - if ( - (toolName === "edit_file" || toolName === "create_file" || toolName === "write_file") && - filePath - ) { - metrics.filesWritten.add(filePath); - } else if (toolName === "delete_file" && filePath) { + const filePath = (args.path ?? args.file_path ?? args.filePath ?? args.file) as string | undefined; + const isWrite = + toolName === "edit_file" || + toolName === "create_file" || + toolName === "write_file" || + toolName === "create" || + toolName === "edit" || + toolName === "write" || + toolName === "write_to_file" || + toolName === "str_replace_editor" || + toolName === "insert_edit_into_file" || + toolName.includes("edit") || + toolName.includes("create") || + toolName.includes("write"); + const isDelete = toolName === "delete_file" || toolName === "delete" || toolName.includes("delete"); + + if (isDelete && filePath) { metrics.filesDeleted.add(filePath); metrics.filesWritten.delete(filePath); + } else if (isWrite && filePath) { + metrics.filesWritten.add(filePath); } } }); From 71040f755b9b36ca9dbcea7b413ba7edb3137c0d Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Sun, 26 Apr 2026 23:29:02 +0200 Subject: [PATCH 08/27] fix: use user-chosen dir directly as output, don't append target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The output dir picker default is now dist// (e.g. dist/bun/). If the user changes it to dist/bun-claude/, that's used as-is — no extra / suffix appended. The prompt command still correctly joins distDir + target since it receives the base dist/ dir. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index def851c..319126a 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -346,7 +346,7 @@ function generatePrompt(target: string, specs: SpecEntry[], allSpecs: SpecEntry[ sections.push("3. For each component with a test file, **read the test spec** and implement runnable tests."); sections.push("4. For each component with a preview file, **read the preview spec** and build a demo screen."); sections.push("5. For each token listed above, **read the full spec file** from disk, then implement it."); - sections.push(`6. Output all files to: \`${relative(SPECS_DIR, join(distDir, target))}/\``); + sections.push(`6. Output all files to: \`${relative(SPECS_DIR, distDir)}/\``); sections.push(` This is the dist directory — keep all generated code here, separate from specs.`); sections.push(""); @@ -552,7 +552,7 @@ async function pickEffort(useGum: boolean, preselected?: string): Promise { const defaultDir = preselected ? join(process.cwd(), preselected) - : DEFAULT_DIST_DIR; + : join(DEFAULT_DIST_DIR, target); const displayDefault = relative(SPECS_DIR, defaultDir) || "."; if (!process.stdin.isTTY || !useGum) return defaultDir; @@ -599,7 +599,7 @@ function printBuildHeader(target: string, config: BuildConfig, useGum: boolean): const header = [ `TUIkit compiler`, `Target: ${target} · Model: ${config.model}`, - `${effortStr ? effortStr.slice(3) : ""}Output: ${relative(SPECS_DIR, config.distDir)}/${target}/`, + `${effortStr ? effortStr.slice(3) : ""}Output: ${relative(SPECS_DIR, config.distDir)}/`, ].join("\n"); if (useGum) { @@ -607,7 +607,7 @@ function printBuildHeader(target: string, config: BuildConfig, useGum: boolean): } else { log(`\n${chalk.cyan("●")} ${chalk.bold("TUIkit compiler")}`); log(` Target: ${chalk.bold(target)} · Model: ${chalk.bold(config.model)}${effortStr}`); - log(` Output: ${relative(SPECS_DIR, config.distDir)}/${target}/\n`); + log(` Output: ${relative(SPECS_DIR, config.distDir)}/\n`); } } @@ -705,7 +705,7 @@ async function cmdBuild( const distDir = config.distDir; const dirtySpecs = dirty.map((d) => d.spec); const prompt = generatePrompt(target, dirtySpecs, specs, distDir); - const outDir = join(distDir, target); + const outDir = distDir; mkdirSync(outDir, { recursive: true }); const promptPath = join(outDir, "_compile-prompt.md"); writeFileSync(promptPath, prompt); @@ -1042,10 +1042,10 @@ function cmdPrompt(target: string, componentFilter?: string, distDir: string = D } const dirtySpecs = dirty.map((d) => d.spec); - const prompt = generatePrompt(target, dirtySpecs, specs, distDir); + const outDir = join(distDir, target); + const prompt = generatePrompt(target, dirtySpecs, specs, outDir); // Write prompt to dist directory - const outDir = join(distDir, target); mkdirSync(outDir, { recursive: true }); const outPath = join(outDir, "_compile-prompt.md"); writeFileSync(outPath, prompt); From 6a3168d64d41132c3e4b75764ee9ba702ac2190a Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Mon, 27 Apr 2026 14:28:29 +0200 Subject: [PATCH 09/27] style: remove gear icons from tool activity output Use indentation and dim gray text only for cleaner output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 319126a..51ec5cd 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -803,7 +803,7 @@ IMPORTANT: session.on("tool.execution_start", (event) => { const { toolName } = event.data; const argStr = summarizeArgs(event.data.arguments); - log(chalk.dim(`\n⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`)); + log(chalk.dim(`\n ${toolName}${argStr ? ` ${argStr}` : ""}`)); }); session.on("tool.execution_complete", (event) => { @@ -831,7 +831,7 @@ IMPORTANT: } // Show current tool activity - log(chalk.dim(` ⚙ ${toolName}${argStr ? ` ${argStr}` : ""}`)); + log(chalk.dim(` ${toolName}${argStr ? ` ${argStr}` : ""}`)); }); } From 0e745657bbcdd034283b2e9b9d1b0ce8da2234b6 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Mon, 27 Apr 2026 21:23:07 +0200 Subject: [PATCH 10/27] refactor: replace gum with @clack/prompts for interactive CLI Remove gum (external Go binary) dependency entirely. All interactive prompts (model picker, effort picker, output dir, multi-pass confirm) now use @clack/prompts which runs in-process with zero external deps. - Single code path instead of gum-vs-chalk branching - No install prerequisites beyond Bun - Add Copilot subscription to README prerequisites Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 1 + bun.lock | 13 ++++ package.json | 1 + scripts/compile.ts | 160 ++++++++++++++++++--------------------------- 4 files changed, 79 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index d524f43..300eab6 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ flowchart LR ### Prerequisites - [Bun](https://bun.sh/) 1.1+ installed (`bun --version`) +- A [GitHub Copilot](https://github.com/features/copilot) subscription (for the `compile build` command) ### Install dependencies diff --git a/bun.lock b/bun.lock index eb78b13..6c45872 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "workspaces": { "": { "dependencies": { + "@clack/prompts": "^1.2.0", "@github/copilot-sdk": "^0.3.0", "chalk": "^5.6.2", "fast-glob": "^3.3.3", @@ -15,6 +16,10 @@ }, }, "packages": { + "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], + + "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], + "@github/copilot": ["@github/copilot@1.0.36", "", { "optionalDependencies": { "@github/copilot-darwin-arm64": "1.0.36", "@github/copilot-darwin-x64": "1.0.36", "@github/copilot-linux-arm64": "1.0.36", "@github/copilot-linux-x64": "1.0.36", "@github/copilot-win32-arm64": "1.0.36", "@github/copilot-win32-x64": "1.0.36" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-x0N5wLzw+tANzb+vCFYLHn3BV3qii2oyn14wC20RO7SsS8/YeBH8olvwlDLJ4PB0mL17QOiytNCdkvjvprm28w=="], "@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@1.0.36", "", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-5qkb7frTS4K/LdTDLrzKo78VR4aw/EZ6JzLz4KfmaW4UYyPiNirExDFXa/By22X0o8YMfOp4MCA2KSCAxKdgTg=="], @@ -73,6 +78,12 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], + + "fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], @@ -179,6 +190,8 @@ "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], diff --git a/package.json b/package.json index 4680619..aadeaef 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "compile": "bun scripts/compile.ts" }, "dependencies": { + "@clack/prompts": "^1.2.0", "@github/copilot-sdk": "^0.3.0", "chalk": "^5.6.2", "fast-glob": "^3.3.3", diff --git a/scripts/compile.ts b/scripts/compile.ts index 51ec5cd..51bb161 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -17,8 +17,8 @@ import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; -import { spawnSync } from "node:child_process"; import chalk from "chalk"; +import * as clack from "@clack/prompts"; // biome-ignore lint/suspicious/noConsole: CLI tool — stdout is the interface const log = (...args: unknown[]) => console.log(...args); @@ -378,27 +378,6 @@ function generatePrompt(target: string, specs: SpecEntry[], allSpecs: SpecEntry[ return sections.join("\n"); } -// ── Gum helpers ──────────────────────────────────────────────────────────── - -function hasGum(): boolean { - const result = spawnSync("gum", ["--version"], { stdio: "ignore" }); - return result.error === undefined && result.status === 0; -} - -function gum(args: string[], input?: string): string { - const result = spawnSync("gum", args, { - encoding: "utf-8", - stdio: [input ? "pipe" : "inherit", "pipe", "inherit"], - input, - }); - return (result.stdout ?? "").trim(); -} - -function gumStyle(text: string, opts: Record = {}): void { - const flags = Object.entries(opts).flatMap(([k, v]) => [`--${k}`, String(v)]); - spawnSync("gum", ["style", ...flags, text], { stdio: "inherit" }); -} - // ── Build helpers ────────────────────────────────────────────────────────── function formatDuration(ms: number): string { @@ -464,18 +443,13 @@ function detectPhase(toolName: string, args: unknown): string { // ── Build command ────────────────────────────────────────────────────────── -function confirmPass(): Promise { - return new Promise((resolve) => { - const rl = require("node:readline").createInterface({ - input: process.stdin, - output: process.stdout, - }); - rl.question(`\n Do another pass? ${chalk.dim("[Y/n]")} `, (answer: string) => { - rl.close(); - const a = answer.trim().toLowerCase(); - resolve(a === "" || a === "y" || a === "yes"); - }); +async function confirmPass(): Promise { + const result = await clack.confirm({ + message: "Do another pass? (improves consistency)", + initialValue: true, }); + if (clack.isCancel(result)) return false; + return result; } async function ensureCopilotAuth(): Promise { @@ -501,7 +475,6 @@ async function ensureCopilotAuth(): Promise { const models = await client.listModels(); @@ -515,49 +488,69 @@ async function pickModel( models.find((m) => m.id === "claude-sonnet-4") ?? models[0]; - if (!process.stdin.isTTY || !useGum) { + if (!process.stdin.isTTY) { return { id: defaultModel.id, name: defaultModel.name }; } - const items = models.map((m) => `${m.name} (${m.id})`); - const selected = gum( - [ - "choose", - "--header", - "Select model:", - "--selected", - `${defaultModel.name} (${defaultModel.id})`, - ...items, - ], - ); + const result = await clack.select({ + message: "Select model:", + options: models.map((m) => ({ value: m.id, label: `${m.name} (${m.id})` })), + initialValue: defaultModel.id, + }); - const match = selected.match(/\(([^)]+)\)$/); - const id = match?.[1] ?? defaultModel.id; - const model = models.find((m) => m.id === id) ?? defaultModel; + if (clack.isCancel(result)) { + clack.cancel("Build cancelled."); + process.exit(0); + } + + const model = models.find((m) => m.id === result) ?? defaultModel; return { id: model.id, name: model.name }; } -async function pickEffort(useGum: boolean, preselected?: string): Promise { +async function pickEffort(preselected?: string): Promise { const defaultEffort = (preselected && ["low", "medium", "high", "xhigh"].includes(preselected)) ? preselected : "high"; - if (!process.stdin.isTTY || !useGum) return defaultEffort as ReasoningEffort; + if (!process.stdin.isTTY) return defaultEffort as ReasoningEffort; - const result = gum(["choose", "--header", "Reasoning effort:", "--selected", defaultEffort, "low", "medium", "high", "xhigh"]); - if (["low", "medium", "high", "xhigh"].includes(result)) return result as ReasoningEffort; - return defaultEffort as ReasoningEffort; + const result = await clack.select({ + message: "Reasoning effort:", + options: [ + { value: "low", label: "low" }, + { value: "medium", label: "medium" }, + { value: "high", label: "high" }, + { value: "xhigh", label: "xhigh" }, + ], + initialValue: defaultEffort, + }); + + if (clack.isCancel(result)) { + clack.cancel("Build cancelled."); + process.exit(0); + } + + return result as ReasoningEffort; } -async function pickOutputDir(useGum: boolean, target: string, preselected?: string): Promise { +async function pickOutputDir(target: string, preselected?: string): Promise { const defaultDir = preselected ? join(process.cwd(), preselected) : join(DEFAULT_DIST_DIR, target); const displayDefault = relative(SPECS_DIR, defaultDir) || "."; - if (!process.stdin.isTTY || !useGum) return defaultDir; + if (!process.stdin.isTTY) return defaultDir; + + const result = await clack.text({ + message: "Output directory:", + initialValue: displayDefault, + }); + + if (clack.isCancel(result)) { + clack.cancel("Build cancelled."); + process.exit(0); + } - const result = gum(["input", "--header", "Output directory:", "--value", displayDefault]); if (!result || result === displayDefault) return defaultDir; return join(SPECS_DIR, result); } @@ -567,16 +560,13 @@ async function promptBuildConfig( flags: { model?: string; effort?: string; out?: string }, target: string, ): Promise { - const useGum = hasGum(); - if (!useGum && process.stdin.isTTY) { - log(chalk.dim(" Tip: install gum for interactive prompts → brew install gum\n")); - } + clack.intro(chalk.cyan("TUIkit compiler")); // Fetch available models to validate and check capabilities const models = await client.listModels(); // Always prompt for model (flag value becomes the pre-selected default) - const model = await pickModel(client, useGum, flags.model); + const model = await pickModel(client, flags.model); // Check if model supports reasoning effort const modelInfo = models.find((m) => m.id === model.id); @@ -585,30 +575,20 @@ async function promptBuildConfig( // Always prompt for effort if model supports it (flag becomes default) let effort: ReasoningEffort | undefined; if (supportsEffort) { - effort = await pickEffort(useGum, flags.effort); + effort = await pickEffort(flags.effort); } // Always prompt for output location (flag or dist/ as default) - const distDir = await pickOutputDir(useGum, target, flags.out); + const distDir = await pickOutputDir(target, flags.out); return { model: model.id, effort, distDir, supportsEffort }; } -function printBuildHeader(target: string, config: BuildConfig, useGum: boolean): void { +function printBuildHeader(target: string, config: BuildConfig): void { const effortStr = config.effort ? ` · Effort: ${config.effort}` : ""; - const header = [ - `TUIkit compiler`, - `Target: ${target} · Model: ${config.model}`, - `${effortStr ? effortStr.slice(3) : ""}Output: ${relative(SPECS_DIR, config.distDir)}/`, - ].join("\n"); - - if (useGum) { - gumStyle(header, { border: "rounded", padding: "1 2", "border-foreground": "6" }); - } else { - log(`\n${chalk.cyan("●")} ${chalk.bold("TUIkit compiler")}`); - log(` Target: ${chalk.bold(target)} · Model: ${chalk.bold(config.model)}${effortStr}`); - log(` Output: ${relative(SPECS_DIR, config.distDir)}/\n`); - } + log(`\n${chalk.cyan("●")} ${chalk.bold("TUIkit compiler")}`); + log(` Target: ${chalk.bold(target)} · Model: ${chalk.bold(config.model)}${effortStr}`); + log(` Output: ${relative(SPECS_DIR, config.distDir)}/\n`); } function printSummary( @@ -616,7 +596,6 @@ function printSummary( config: BuildConfig, metrics: CompileMetrics, outDir: string, - useGum: boolean, noLock: boolean, passNumber = 1, ): void { @@ -632,7 +611,7 @@ function printSummary( const passLabel = passNumber > 1 ? ` (pass ${passNumber})` : ""; const filesLine = deleted > 0 ? `${files} written, ${deleted} deleted` : `${files} written`; const body = [ - `✓ Compilation complete — target: ${target}${passLabel}`, + `${chalk.green("✓")} Compilation complete — target: ${target}${passLabel}`, ``, ` Model: ${config.model}${config.effort ? ` (${config.effort} effort)` : ""}`, ` Time: ${formatDuration(elapsed)}`, @@ -647,11 +626,7 @@ function printSummary( ].join("\n"); log(""); - if (useGum) { - gumStyle(body, { border: "rounded", padding: "1 2", "border-foreground": "2" }); - } else { - log(body); - } + log(body); log(""); } @@ -664,7 +639,6 @@ async function cmdBuild( verbose = false, noLock = false, ): Promise { - const useGum = hasGum(); const { approveAll } = await import("@github/copilot-sdk"); // 1. Quick check — any dirty specs at all? @@ -711,7 +685,7 @@ async function cmdBuild( writeFileSync(promptPath, prompt); // 5. Print header - printBuildHeader(target, config, useGum); + printBuildHeader(target, config); log(` ${chalk.dim(`${dirty.length} dirty specs to compile`)}\n`); // 6. Metrics @@ -906,7 +880,7 @@ IMPORTANT: } // Show summary for this pass - printSummary(target, config, metrics, outDir, useGum, noLock, passNumber); + printSummary(target, config, metrics, outDir, noLock, passNumber); if (metrics.errors.length > 0) { log(chalk.yellow("⚠ Completed with errors:")); @@ -918,13 +892,7 @@ IMPORTANT: // 11. Multi-pass loop — offer to do another pass while (process.stdin.isTTY && !aborted) { - let wantMore: boolean; - if (useGum) { - const r = spawnSync("gum", ["confirm", "--default=yes", "Do another pass? (improves consistency)"], { stdio: "inherit" }); - wantMore = r.status === 0; - } else { - wantMore = await confirmPass(); - } + const wantMore = await confirmPass(); if (!wantMore) break; @@ -961,7 +929,7 @@ IMPORTANT: log(` ${chalk.green("✓")} ${currentPhase}`); } - printSummary(target, config, metrics, outDir, useGum, noLock, passNumber); + printSummary(target, config, metrics, outDir, noLock, passNumber); if (metrics.errors.length > 0) { log(chalk.yellow("⚠ Pass completed with errors:")); From 3d6d447aff9ef5d5e49b1b8d0fb57533f8baab69 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Tue, 28 Apr 2026 12:16:45 +0200 Subject: [PATCH 11/27] feat: show agent's last message after each pass completes Displays the agent's final message between the phase checkmarks and the summary stats. Gives visibility into what the agent accomplished in each pass without needing verbose mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/compile.ts b/scripts/compile.ts index 51bb161..30e2178 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -879,6 +879,12 @@ IMPORTANT: log(` ${chalk.green("✓")} ${currentPhase}`); } + // Show the agent's last message as a pass recap + if (metrics.lastAssistantMessage) { + log(`\n Agent finished:`); + log(` ${chalk.dim(metrics.lastAssistantMessage.trim())}`); + } + // Show summary for this pass printSummary(target, config, metrics, outDir, noLock, passNumber); @@ -929,6 +935,12 @@ IMPORTANT: log(` ${chalk.green("✓")} ${currentPhase}`); } + // Show the agent's last message as a pass recap + if (metrics.lastAssistantMessage) { + log(`\n Agent finished:`); + log(` ${chalk.dim(metrics.lastAssistantMessage.trim())}`); + } + printSummary(target, config, metrics, outDir, noLock, passNumber); if (metrics.errors.length > 0) { From b984541d75bb7e507ba2c245bb868d1b5e4b9fbc Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Tue, 28 Apr 2026 12:35:30 +0200 Subject: [PATCH 12/27] feat: reframe prompt for depth-first compilation Restructure the compilation prompt to prioritize a working interactive playground over surface coverage. Key changes: - Add 'depth over breadth' philosophy section - Require components to be fully complete (impl + tests + demo) before moving to the next one - Make --interactive the primary output, not a stub - Verification happens per-component, not just at the end - Multi-pass improvement prompt reinforces interactive demo priority Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 83 +++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 30e2178..6b7ffbd 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -341,37 +341,61 @@ function generatePrompt(target: string, specs: SpecEntry[], allSpecs: SpecEntry[ sections.push(""); sections.push("IMPORTANT: Do NOT spawn sub-agents or delegate to the task tool. Do ALL work yourself directly."); sections.push(""); + sections.push("### Philosophy: depth over breadth"); + sections.push(""); + sections.push("It is MUCH better to have a few components that work perfectly — with full"); + sections.push("interactivity, passing tests, and a working interactive demo — than many"); + sections.push("components that are half-baked. Each component you implement must be"); + sections.push("**complete and polished** before moving to the next one."); + sections.push(""); + sections.push("### Workflow"); + sections.push(""); sections.push("1. Read the target definition to understand the framework and paradigm."); - sections.push("2. For each component listed above, **read the full spec file** from disk, then implement it."); - sections.push("3. For each component with a test file, **read the test spec** and implement runnable tests."); - sections.push("4. For each component with a preview file, **read the preview spec** and build a demo screen."); - sections.push("5. For each token listed above, **read the full spec file** from disk, then implement it."); - sections.push(`6. Output all files to: \`${relative(SPECS_DIR, distDir)}/\``); + sections.push("2. Implement all **tokens first** — read each token spec from disk, implement it."); + sections.push("3. Then implement components **one at a time, fully**, in this order:"); + sections.push(" a. Read the full spec file from disk."); + sections.push(" b. Implement the component with all variants and interactions."); + sections.push(" c. Read the test spec and implement runnable tests. Run them — they must pass."); + sections.push(" d. Wire the component into the interactive demo (see below)."); + sections.push(" e. Verify the component works in the demo with `--component --snapshot`."); + sections.push(" f. Only then move to the next component."); + sections.push(`4. Output all files to: \`${relative(SPECS_DIR, distDir)}/\``); sections.push(` This is the dist directory — keep all generated code here, separate from specs.`); sections.push(""); if (existsSync(DEMO_PATH)) { sections.push("---"); - sections.push("## Demo specification"); + sections.push("## Demo specification — INTERACTIVE PLAYGROUND (required)"); + sections.push(""); + sections.push("The demo is NOT a static listing. It is a **fully interactive playground**"); + sections.push("where you can navigate between components and interact with live instances."); + sections.push(`Read the full spec: \`${relative(SPECS_DIR, DEMO_PATH)}\``); sections.push(""); - sections.push("The demo app is an interactive component preview browser."); - sections.push(`Read the full spec before building the demo: \`${relative(SPECS_DIR, DEMO_PATH)}\``); + sections.push("Key requirements:"); + sections.push("- `--interactive` MUST launch a full-screen TUI with sidebar + preview panel."); + sections.push("- Every previewed component MUST be a live, interactive instance (e.g., you can"); + sections.push(" type in an Input, navigate a Select, scroll a ScrollBox)."); + sections.push("- Implement the interactive mode **from the first component** — do not leave it"); + sections.push(" as a stub. It's better to have 3 components in a working playground than"); + sections.push(" 10 components with `--interactive` not implemented."); + sections.push("- `--list` and `--snapshot` modes are secondary — they must work, but the"); + sections.push(" interactive playground is the primary output."); sections.push(""); } sections.push("---"); sections.push("## Verification (REQUIRED)"); sections.push(""); - sections.push("After generating ALL files, you MUST verify in this order:"); + sections.push("After implementing each component (not just at the end), verify:"); sections.push(""); - sections.push("1. **Run unit tests**: Execute the target's test command and ensure ALL tests pass."); - sections.push(" Fix any failures before proceeding."); - sections.push("2. **Build the demo**: Compile/build the demo CLI and verify it starts without errors."); - sections.push("3. **Verify demo --list**: Run the demo with `--list` and confirm all components/tokens appear."); - sections.push("4. **Verify demo --snapshot**: For EVERY component from `--list`, run"); - sections.push(" `--component --snapshot` and confirm it exits 0 with non-empty output."); - sections.push(" If any snapshot fails, fix the demo wiring before continuing."); - sections.push("5. **Run demo smoke tests**: Execute the demo test file and ensure all snapshot tests pass."); + sections.push("1. **Unit tests pass**: Run the target's test command for that component."); + sections.push("2. **Demo snapshot works**: `--component --snapshot` exits 0 with output."); + sections.push("3. **Interactive demo works**: `--interactive` launches and the component is navigable."); + sections.push(""); + sections.push("After ALL components are done:"); + sections.push(""); + sections.push("4. **Full test suite**: Run all tests, ensure everything passes."); + sections.push("5. **Demo smoke tests**: Run the demo test file, all snapshots pass."); sections.push("6. **Report**: State the final unit test count, demo smoke test count, and pass/fail status."); sections.push(""); @@ -715,12 +739,21 @@ and generate idiomatic code for the target framework. Working directory: ${SPECS_DIR} Output directory: ${relative(SPECS_DIR, outDir)} -IMPORTANT: +PHILOSOPHY: Depth over breadth. +It is far better to deliver a few components that are fully complete — +with passing tests and working interactive demo — than many components +that are half-implemented. Completeness means: the component renders +correctly, responds to user input, is wired into the interactive +playground, and all tests pass. + +RULES: - Do NOT spawn sub-agents or delegate to the task tool. Do ALL work yourself directly. - Do NOT ask the user questions. Proceed with your best judgment. - Read ALL referenced spec files from disk before implementing. - Output all generated code to the specified output directory. -- Run tests after implementation and fix any failures. +- Implement one component at a time, fully, before starting the next. +- The interactive demo (--interactive) is the PRIMARY deliverable, not an afterthought. +- Run tests after EACH component and fix any failures before moving on. `, }, @@ -918,13 +951,17 @@ IMPORTANT: "Do another pass over the compilation output.", "Re-read the original spec files and the compile prompt at " + `\`${relative(SPECS_DIR, promptPath)}\` to check what you may have missed.`, + "", + "Remember: DEPTH OVER BREADTH. A few components working perfectly", + "(with interactive demo) is better than many half-working ones.", + "", "Focus on:", - "- Missing or incomplete component implementations", + "- The interactive demo (`--interactive`) — it MUST work as a full-screen playground", + "- Components already implemented: polish, fix bugs, ensure full interactivity", "- Tests that are failing or missing", - "- Inconsistencies between the spec and the generated code", - "- Demo wiring for any new components", + "- Add the NEXT component (fully: implementation + tests + demo wiring)", "- Token usage correctness", - "After fixing, run the tests again and report results.", + "After fixing, run the tests and verify `--interactive` works, then report results.", ].join("\n"), }); From 76da164c1df3517b3b12d09870e78c8ebd2d6b4e Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Tue, 28 Apr 2026 13:21:08 +0200 Subject: [PATCH 13/27] feat: render agent summary as markdown in a box Use marked + marked-terminal to render the agent's final message with proper markdown formatting (headings, bold, code, lists) and wrap it in a boxen container with dim border and 'Agent summary' title. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bun.lock | 139 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + scripts/compile.ts | 13 +++-- 3 files changed, 151 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 6c45872..d0a19c9 100644 --- a/bun.lock +++ b/bun.lock @@ -6,9 +6,12 @@ "dependencies": { "@clack/prompts": "^1.2.0", "@github/copilot-sdk": "^0.3.0", + "boxen": "^8.0.1", "chalk": "^5.6.2", "fast-glob": "^3.3.3", "gray-matter": "^4.0.3", + "marked": "14", + "marked-terminal": "7", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "zod": "^4.3.6", @@ -20,6 +23,8 @@ "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@github/copilot": ["@github/copilot@1.0.36", "", { "optionalDependencies": { "@github/copilot-darwin-arm64": "1.0.36", "@github/copilot-darwin-x64": "1.0.36", "@github/copilot-linux-arm64": "1.0.36", "@github/copilot-linux-x64": "1.0.36", "@github/copilot-win32-arm64": "1.0.36", "@github/copilot-win32-x64": "1.0.36" }, "bin": { "copilot": "npm-loader.js" } }, "sha512-x0N5wLzw+tANzb+vCFYLHn3BV3qii2oyn14wC20RO7SsS8/YeBH8olvwlDLJ4PB0mL17QOiytNCdkvjvprm28w=="], "@github/copilot-darwin-arm64": ["@github/copilot-darwin-arm64@1.0.36", "", { "os": "darwin", "cpu": "arm64", "bin": { "copilot-darwin-arm64": "copilot" } }, "sha512-5qkb7frTS4K/LdTDLrzKo78VR4aw/EZ6JzLz4KfmaW4UYyPiNirExDFXa/By22X0o8YMfOp4MCA2KSCAxKdgTg=="], @@ -42,6 +47,8 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -50,16 +57,44 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], + + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], + + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + + "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -68,6 +103,14 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -92,14 +135,24 @@ "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], @@ -112,6 +165,10 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "marked": ["marked@14.1.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg=="], + + "marked-terminal": ["marked-terminal@7.3.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "ansi-regex": "^6.1.0", "chalk": "^5.4.1", "cli-highlight": "^2.1.11", "cli-table3": "^0.6.5", "node-emoji": "^2.2.0", "supports-hyperlinks": "^3.1.0" }, "peerDependencies": { "marked": ">=1 <16" } }, "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], @@ -172,6 +229,16 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "node-emoji": ["node-emoji@2.2.0", "", { "dependencies": { "@sindresorhus/is": "^4.6.0", "char-regex": "^1.0.2", "emojilib": "^2.4.0", "skin-tone": "^2.0.0" } }, "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -184,6 +251,8 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -192,14 +261,32 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "skin-tone": ["skin-tone@2.0.0", "", { "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" } }, "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -216,8 +303,60 @@ "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + + "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cli-table3/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cli-table3/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package.json b/package.json index aadeaef..5c8a32b 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,12 @@ "dependencies": { "@clack/prompts": "^1.2.0", "@github/copilot-sdk": "^0.3.0", + "boxen": "^8.0.1", "chalk": "^5.6.2", "fast-glob": "^3.3.3", "gray-matter": "^4.0.3", + "marked": "14", + "marked-terminal": "7", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "zod": "^4.3.6" diff --git a/scripts/compile.ts b/scripts/compile.ts index 6b7ffbd..8556101 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -19,6 +19,11 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, wri import { dirname, join, relative } from "node:path"; import chalk from "chalk"; import * as clack from "@clack/prompts"; +import { marked } from "marked"; +import { markedTerminal } from "marked-terminal"; +import boxen from "boxen"; + +marked.use(markedTerminal()); // biome-ignore lint/suspicious/noConsole: CLI tool — stdout is the interface const log = (...args: unknown[]) => console.log(...args); @@ -914,8 +919,8 @@ RULES: // Show the agent's last message as a pass recap if (metrics.lastAssistantMessage) { - log(`\n Agent finished:`); - log(` ${chalk.dim(metrics.lastAssistantMessage.trim())}`); + const rendered = marked(metrics.lastAssistantMessage.trim()) as string; + log(`\n${boxen(rendered.trimEnd(), { padding: 1, dimBorder: true, title: "Agent summary", titleAlignment: "left" })}`); } // Show summary for this pass @@ -974,8 +979,8 @@ RULES: // Show the agent's last message as a pass recap if (metrics.lastAssistantMessage) { - log(`\n Agent finished:`); - log(` ${chalk.dim(metrics.lastAssistantMessage.trim())}`); + const rendered = marked(metrics.lastAssistantMessage.trim()) as string; + log(`\n${boxen(rendered.trimEnd(), { padding: 1, dimBorder: true, title: "Agent summary", titleAlignment: "left" })}`); } printSummary(target, config, metrics, outDir, noLock, passNumber); From 3e7c972272911f4f4c91bb5140bb2141859e7ae4 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Tue, 28 Apr 2026 15:13:57 +0200 Subject: [PATCH 14/27] feat: add multi-pass summary instructions to system prompt Tell the agent to end each pass with a structured summary of what was accomplished and explicit next-pass priorities. This makes the boxed agent summary actionable and helps the user decide whether to run another pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/compile.ts b/scripts/compile.ts index 8556101..df510fa 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -759,6 +759,21 @@ RULES: - Implement one component at a time, fully, before starting the next. - The interactive demo (--interactive) is the PRIMARY deliverable, not an afterthought. - Run tests after EACH component and fix any failures before moving on. + +MULTI-PASS APPROACH: +This session may receive multiple passes. At the END of each pass, you MUST +include a clear summary of what was accomplished and what remains. Structure +your final message like this: + +## Pass summary +- What was completed (components, tests, demo wiring) +- Current test results (X passing, Y failing) +- Interactive demo status + +## Next pass priorities +- List specific components or work items that should be tackled next +- Note any known issues or failing tests to fix +- If everything is complete, say so explicitly `, }, From f72647e9c602e0f199eb8ad0f677e54522725f6e Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Tue, 28 Apr 2026 18:01:13 +0200 Subject: [PATCH 15/27] feat: instruct agent to verify dependencies via web browsing Add rule to system prompt telling the agent to check npm/GitHub for libraries before assuming they don't exist. Prevents knowledge cutoff issues where the agent falls back to polyfills for packages that are actually published. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/compile.ts b/scripts/compile.ts index df510fa..6767a46 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -760,6 +760,14 @@ RULES: - The interactive demo (--interactive) is the PRIMARY deliverable, not an afterthought. - Run tests after EACH component and fix any failures before moving on. +DEPENDENCIES & KNOWLEDGE CUTOFF: +Your training data may be outdated. Before assuming a library doesn't exist or +falling back to self-contained polyfills, you MUST use web browsing / fetch to +check the library's actual npm registry page, GitHub repo, or documentation. +Install the real package if it exists. Only polyfill if you've confirmed the +package genuinely isn't published. This applies to ALL dependencies referenced +in the target spec (e.g., @opentui/*, ink, bubbletea crates, etc.). + MULTI-PASS APPROACH: This session may receive multiple passes. At the END of each pass, you MUST include a clear summary of what was accomplished and what remains. Structure From 25209c7b966e6defabd4b4065fd4ef6ea28f1ffa Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Wed, 29 Apr 2026 12:02:35 +0200 Subject: [PATCH 16/27] feat: lock after each pass to bank completed work Previously the lock only ran once after all passes finished. Now each successful pass (no errors) locks immediately, so completed components are banked incrementally. A bad subsequent pass won't lose the progress from earlier passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 6767a46..862cd81 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -957,6 +957,12 @@ your final message like this: log(""); } + // Lock after pass 1 to bank completed work + if (!noLock && metrics.errors.length === 0) { + cmdLock(target, componentFilter); + log(chalk.dim(` Lock updated after pass ${passNumber}\n`)); + } + // 11. Multi-pass loop — offer to do another pass while (process.stdin.isTTY && !aborted) { const wantMore = await confirmPass(); @@ -1015,14 +1021,15 @@ your final message like this: } log(""); } - } - // 12. Auto-lock (unless --no-lock or errors occurred) - if (!noLock && metrics.errors.length === 0) { - cmdLock(target, componentFilter); + // Lock after each pass to bank completed work + if (!noLock && metrics.errors.length === 0) { + cmdLock(target, componentFilter); + log(chalk.dim(` Lock updated after pass ${passNumber}\n`)); + } } - // 13. Cleanup + // 12. Cleanup try { await session.disconnect(); await client.stop(); From d90c6ef2ccb49e4d2bde34fbf42e18ea78554397 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Wed, 29 Apr 2026 12:08:29 +0200 Subject: [PATCH 17/27] feat: delegate lock to the agent per-component Remove auto-lock after passes. Instead, instruct the agent in the system prompt to run 'bun run compile lock --target --component ' after fully completing each component (tests pass + demo wired). This ensures only genuinely completed components get locked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 862cd81..62b3d6c 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -760,6 +760,14 @@ RULES: - The interactive demo (--interactive) is the PRIMARY deliverable, not an afterthought. - Run tests after EACH component and fix any failures before moving on. +LOCKING COMPLETED COMPONENTS: +After you fully complete a component (implementation + tests passing + demo wired), +lock it by running: + bun run compile lock --target ${target} --component +This records the component as compiled so it won't be recompiled in future runs. +Only lock a component when you are confident it is DONE — tests pass, demo works. +Lock tokens the same way: bun run compile lock --target ${target} --component + DEPENDENCIES & KNOWLEDGE CUTOFF: Your training data may be outdated. Before assuming a library doesn't exist or falling back to self-contained polyfills, you MUST use web browsing / fetch to @@ -957,12 +965,6 @@ your final message like this: log(""); } - // Lock after pass 1 to bank completed work - if (!noLock && metrics.errors.length === 0) { - cmdLock(target, componentFilter); - log(chalk.dim(` Lock updated after pass ${passNumber}\n`)); - } - // 11. Multi-pass loop — offer to do another pass while (process.stdin.isTTY && !aborted) { const wantMore = await confirmPass(); @@ -1021,12 +1023,6 @@ your final message like this: } log(""); } - - // Lock after each pass to bank completed work - if (!noLock && metrics.errors.length === 0) { - cmdLock(target, componentFilter); - log(chalk.dim(` Lock updated after pass ${passNumber}\n`)); - } } // 12. Cleanup From bff4fde8fdff7c32e7d57269f3512b4a7665b56d Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Wed, 29 Apr 2026 12:14:45 +0200 Subject: [PATCH 18/27] feat: add --autopilot flag for unattended multi-pass builds Auto-continues passes without prompting, up to a max of (dirty specs + 5) passes. Shows pass count as 'Pass N/max' in the log. Useful for running full compilations unattended. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 62b3d6c..7dd28e4 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -9,7 +9,7 @@ * Usage: * bun run compile status [--target ] * bun run compile prompt --target [--component ] - * bun run compile build --target [--component ] [--model ] [--effort ] [--verbose] [--no-lock] + * bun run compile build --target [--component ] [--model ] [--effort ] [--verbose] [--no-lock] [--autopilot] * bun run compile lock --target [--component ] | --all-targets * bun run compile clean --target | --all-targets */ @@ -667,6 +667,7 @@ async function cmdBuild( flagEffort?: string, verbose = false, noLock = false, + autopilot = false, ): Promise { const { approveAll } = await import("@github/copilot-sdk"); @@ -965,11 +966,22 @@ your final message like this: log(""); } - // 11. Multi-pass loop — offer to do another pass - while (process.stdin.isTTY && !aborted) { - const wantMore = await confirmPass(); - - if (!wantMore) break; + // 11. Multi-pass loop + const maxPasses = dirty.length + 5; + while (!aborted) { + // In autopilot mode, auto-continue up to maxPasses + // In interactive mode, prompt the user + if (autopilot) { + if (passNumber >= maxPasses) { + log(chalk.dim(` Autopilot: reached max passes (${maxPasses}), stopping.\n`)); + break; + } + } else if (process.stdin.isTTY) { + const wantMore = await confirmPass(); + if (!wantMore) break; + } else { + break; + } passNumber++; currentPhase = "Starting"; @@ -980,7 +992,8 @@ your final message like this: const prevToolCalls = metrics.toolCalls; metrics.errors = []; - log(`\n${chalk.cyan("●")} Pass ${passNumber} — sending improvement prompt...\n`); + const passLabel = autopilot ? `Pass ${passNumber}/${maxPasses}` : `Pass ${passNumber}`; + log(`\n${chalk.cyan("●")} ${passLabel} — sending improvement prompt...\n`); await session.send({ prompt: [ @@ -1184,6 +1197,7 @@ Build options: --effort Reasoning effort: low | medium | high | xhigh (default: high) --verbose Show full agent transcript (raw streaming output) --no-lock Skip auto-lock after successful build + --autopilot Auto-run passes without confirmation (max: components + 5) Common options: --out Output directory for compiled code (default: dist/) @@ -1210,6 +1224,7 @@ interface ParsedArgs { effort?: string; verbose: boolean; noLock: boolean; + autopilot: boolean; } function parseArgs(argv: string[]): ParsedArgs { @@ -1222,6 +1237,7 @@ function parseArgs(argv: string[]): ParsedArgs { let effort: string | undefined; let verbose = false; let noLock = false; + let autopilot = false; for (let i = 1; i < argv.length; i++) { if (argv[i] === "--target" && argv[i + 1]) { @@ -1240,10 +1256,12 @@ function parseArgs(argv: string[]): ParsedArgs { verbose = true; } else if (argv[i] === "--no-lock") { noLock = true; + } else if (argv[i] === "--autopilot") { + autopilot = true; } } - return { command, target, allTargets, component, out, model, effort, verbose, noLock }; + return { command, target, allTargets, component, out, model, effort, verbose, noLock, autopilot }; } const args = parseArgs(process.argv.slice(2)); @@ -1270,10 +1288,10 @@ switch (args.command) { case "build": if (args.allTargets) { for (const t of discoverTargets()) { - await cmdBuild(t, args.component, distDir, args.model, args.effort, args.verbose, args.noLock); + await cmdBuild(t, args.component, distDir, args.model, args.effort, args.verbose, args.noLock, args.autopilot); } } else if (args.target) { - await cmdBuild(args.target, args.component, distDir, args.model, args.effort, args.verbose, args.noLock); + await cmdBuild(args.target, args.component, distDir, args.model, args.effort, args.verbose, args.noLock, args.autopilot); } else { log("Error: --target or --all-targets is required for build command"); process.exit(1); From 3af87b0e13e1e3f173b0b045f15ca421ad1310f4 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Wed, 29 Apr 2026 12:21:43 +0200 Subject: [PATCH 19/27] docs: update README with build command, flags, and autopilot Rewrite the 'Compiling to a target' section to document: - All CLI commands in a table - Full build flags reference (including --autopilot) - Interactive vs autopilot workflow examples - Agent-driven per-component locking - Multi-pass philosophy (depth over breadth) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 105 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 300eab6..b0d84b9 100644 --- a/README.md +++ b/README.md @@ -172,63 +172,104 @@ All normative sections (Visual rules, Behavior, Edge cases) use | `bun` | TypeScript | OpenTUI + React (Bun) | `targets/bun.md` | | `rust` | Rust | Ratatui + Crossterm | `targets/rust.md` | +### Commands + +| Command | Purpose | +| ------- | ------- | +| `compile status` | Show dirty/locked specs per target | +| `compile prompt` | Generate a compilation prompt file | +| `compile build` | Run an agent session to compile specs (requires Copilot) | +| `compile lock` | Lock spec hashes after verified compilation | +| `compile clean` | Remove lock file for a target | + +### Build flags + +``` +bun run compile build --target [flags] + +Required: + --target Target to compile (go, node, bun, rust) + +Optional: + --component Compile a single component/token only + --out Output directory (default: dist/) + --model Model to use (e.g. claude-sonnet-4, gpt-5) + --effort Reasoning effort: low | medium | high | xhigh + --verbose Show full agent transcript (raw streaming) + --no-lock Prevent the agent from locking components + --autopilot Auto-run passes without confirmation (max: specs + 5) + --all-targets Compile all targets sequentially +``` + ### Workflow ```bash # 1. See what's changed bun run compile status -# 2. Generate the compilation prompt -bun run compile prompt --target go - -# 3. Feed dist/go/_compile-prompt.md to an LLM agent -# The agent generates code into dist/go/ +# 2. Compile interactively (prompts for model, effort, output dir) +bun run compile build --target bun -# 4. Verify: run tests, check the demo CLI -cd dist/go && go test ./... && go run ./cmd/demo +# 3. Or compile non-interactively with all options +bun run compile build --target bun --model claude-sonnet-4 --out dist/bun-claude -# 5. Lock the hashes -bun run compile lock --target go +# 4. Or fire-and-forget with autopilot (auto-runs multiple passes) +bun run compile build --target bun --model claude-sonnet-4 --autopilot ``` ### Multi-pass compilation -A single compilation pass across the full component suite (17 components + -tokens + demo) is usually not enough to reach production quality. We've found -that **2–3 passes** produce notably better results: +The compiler supports multiple passes within a single session. Each pass builds +on the previous output — the agent reviews, fixes, and extends its own work. | Pass | Focus | Typical outcome | | ---- | ----- | --------------- | -| **1st** | Initial generation | All components scaffold correctly, most tests pass, demo wires up. Expect rough edges — missing edge cases, incomplete keybindings, demo wiring bugs. | -| **2nd** | Review & fix | Agent reviews its own output against specs, fixes test failures, fills in missing behavior, improves demo interactivity. Test count typically grows 30–50%. | -| **3rd** | Polish | Catches subtle spec violations, improves accessibility, hardens demo `--snapshot` smoke tests. Diminishing returns after this point. | +| **1st** | Initial generation | Core tokens, first components fully wired into interactive demo. | +| **2nd** | Extend & fix | More components added, test failures fixed, demo polished. | +| **3rd** | Polish | Catches subtle spec violations, hardens edge cases. | -To run a follow-up pass, generate a new prompt and tell the agent to review -and complete its existing work: +**Interactive mode** (default): After each pass, the compiler asks whether to +continue. You see a boxed markdown summary of what the agent accomplished. -```bash -# Generate a fresh prompt (it sees the current dist/ state) -bun run compile prompt --target go +**Autopilot mode** (`--autopilot`): Passes auto-continue without prompting, +up to a maximum of `(dirty specs + 5)` passes. The agent stops early if +everything is complete. -# Feed to the agent with instructions like: -# "Review your existing implementation against the specs. -# Fix any test failures, fill in missing behavior, -# and ensure all --snapshot smoke tests pass." +The agent is instructed to follow a **depth-over-breadth** philosophy: it fully +completes each component (implementation + tests + interactive demo) before +moving to the next one. + +### Component locking + +The agent locks components individually as it completes them by running: + +```bash +bun run compile lock --target bun --component Select ``` -Each pass is fast because the agent builds on its own prior output rather than -starting from scratch. The demo's `--list` and `--snapshot` flags make it easy -for the agent to self-verify between passes. +This records the spec hash so the component won't be recompiled unless its spec +changes. You can also lock manually after verifying generated code: + +```bash +# Lock a single component +bun run compile lock --target go --component Input + +# Lock all specs for a target +bun run compile lock --target go + +# Lock all targets +bun run compile lock --all-targets +``` ### Custom output directory -By default, compiled code goes to `dist/`. Override with `--out`: +By default, compiled code goes to `dist//`. Override with `--out`: ```bash -# Output to a separate repo or directory -bun run compile prompt --target go --out ~/my-tuikit-go +# Output to a custom directory +bun run compile build --target go --out dist/go-experimental -# The prompt and generated code go to ~/my-tuikit-go/go/ +# The prompt and generated code go directly to dist/go-experimental/ ``` ### Adding a new target @@ -238,7 +279,7 @@ bun run compile prompt --target go --out ~/my-tuikit-go machine pattern, token access, styling, composition, test pattern, key mapping, dependencies, and demo CLI 3. Run `bun run compile status` — your target will show up with all specs dirty -4. Run `bun run compile prompt --target {name}` and compile +4. Run `bun run compile build --target {name}` to compile ## Linting From e45e6b81a9816cae86197ce79962a89fe22965bd Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Wed, 29 Apr 2026 12:40:17 +0200 Subject: [PATCH 20/27] docs: add 'Generating prompts manually' section to README Document the compile prompt command as an alternative to compile build, for users who want to feed prompts to external agents manually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index b0d84b9..4efc64c 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,36 @@ bun run compile build --target go --out dist/go-experimental # The prompt and generated code go directly to dist/go-experimental/ ``` +### Generating prompts manually + +If you prefer to feed the prompt to an external agent (Claude, ChatGPT, Copilot +Chat, etc.) instead of using `compile build`, use the `prompt` command: + +```bash +# Generate a prompt for a target +bun run compile prompt --target go + +# Generate for a single component +bun run compile prompt --target bun --component Select + +# Generate to a custom directory +bun run compile prompt --target node --out ~/my-project +``` + +The prompt is written to `/_compile-prompt.md`. It contains: + +- The target definition (framework, paradigm, file structure) +- An index of all dirty specs with file paths and summaries +- Instructions for the agent (depth-first, verification steps) +- Demo specification reference + +Feed this file to any LLM agent, then lock manually once verified: + +```bash +# After the agent generates code and tests pass: +bun run compile lock --target go +``` + ### Adding a new target 1. Create `targets/{name}.md` following the target spec format in `docs/schema.md` From 0097439aadea3d4c59ef1d47d2823915280aa199 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Wed, 29 Apr 2026 13:00:10 +0200 Subject: [PATCH 21/27] docs: note that build sessions must run from dist target folder Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 4efc64c..16ede07 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,11 @@ The prompt is written to `/_compile-prompt.md`. It contains: - Instructions for the agent (depth-first, verification steps) - Demo specification reference +> **Important:** Any coding session that uses this prompt should set its working +> directory to the dist target folder (where `_compile-prompt.md` lives). The +> prompt references spec files using relative paths that resolve correctly only +> from that location. + Feed this file to any LLM agent, then lock manually once verified: ```bash From e5b979a531b74c5b1c8cbb2e2ca24445d61fdb30 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Thu, 30 Apr 2026 13:48:19 +0200 Subject: [PATCH 22/27] fix: address PR review -- no-lock suppresses agent instructions, fix prompt path docs - --no-lock now conditionally omits lock instructions from system prompt - Updated help text to reflect actual behavior - Fixed README prompt path: //_compile-prompt.md - Fixed working directory guidance: repo root, not dist folder Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 7 +++---- scripts/compile.ts | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 16ede07..f4305fc 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ bun run compile prompt --target bun --component Select bun run compile prompt --target node --out ~/my-project ``` -The prompt is written to `/_compile-prompt.md`. It contains: +The prompt is written to `//_compile-prompt.md` (e.g. `dist/go/_compile-prompt.md`). It contains: - The target definition (framework, paradigm, file structure) - An index of all dirty specs with file paths and summaries @@ -296,9 +296,8 @@ The prompt is written to `/_compile-prompt.md`. It contains: - Demo specification reference > **Important:** Any coding session that uses this prompt should set its working -> directory to the dist target folder (where `_compile-prompt.md` lives). The -> prompt references spec files using relative paths that resolve correctly only -> from that location. +> directory to the repository root (where `components/`, `tokens/`, and `docs/` +> live). The prompt references spec files using paths relative to the repo root. Feed this file to any LLM agent, then lock manually once verified: diff --git a/scripts/compile.ts b/scripts/compile.ts index 7dd28e4..7c0d681 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -761,13 +761,14 @@ RULES: - The interactive demo (--interactive) is the PRIMARY deliverable, not an afterthought. - Run tests after EACH component and fix any failures before moving on. -LOCKING COMPLETED COMPONENTS: +${noLock ? "" : `LOCKING COMPLETED COMPONENTS: After you fully complete a component (implementation + tests passing + demo wired), lock it by running: bun run compile lock --target ${target} --component This records the component as compiled so it won't be recompiled in future runs. Only lock a component when you are confident it is DONE — tests pass, demo works. Lock tokens the same way: bun run compile lock --target ${target} --component +`} DEPENDENCIES & KNOWLEDGE CUTOFF: Your training data may be outdated. Before assuming a library doesn't exist or @@ -1196,7 +1197,7 @@ Build options: --model Model to use (e.g. claude-sonnet-4, gpt-5). Prompts if omitted. --effort Reasoning effort: low | medium | high | xhigh (default: high) --verbose Show full agent transcript (raw streaming output) - --no-lock Skip auto-lock after successful build + --no-lock Suppress agent lock instructions (agent won't lock components) --autopilot Auto-run passes without confirmation (max: components + 5) Common options: From 0d75e3d0009e62deee7acd0789a04dae5c037456 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Thu, 30 Apr 2026 15:22:59 +0200 Subject: [PATCH 23/27] feat: use SDK session modes -- autopilot vs interactive Set session.rpc.mode.set() based on --autopilot flag: - Default: 'interactive' mode, user confirms each pass - --autopilot: 'autopilot' mode, agent runs autonomously Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 13 +++++++------ scripts/compile.ts | 19 +++++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f4305fc..e4407d9 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Optional: --effort Reasoning effort: low | medium | high | xhigh --verbose Show full agent transcript (raw streaming) --no-lock Prevent the agent from locking components - --autopilot Auto-run passes without confirmation (max: specs + 5) + --autopilot Use SDK autopilot mode (agent runs autonomously, max: specs + 5 passes) --all-targets Compile all targets sequentially ``` @@ -228,12 +228,13 @@ on the previous output — the agent reviews, fixes, and extends its own work. | **2nd** | Extend & fix | More components added, test failures fixed, demo polished. | | **3rd** | Polish | Catches subtle spec violations, hardens edge cases. | -**Interactive mode** (default): After each pass, the compiler asks whether to -continue. You see a boxed markdown summary of what the agent accomplished. +**Interactive mode** (default): The agent runs in the SDK's `interactive` mode. +After each pass, the compiler asks whether to continue. You see a boxed markdown +summary of what the agent accomplished. -**Autopilot mode** (`--autopilot`): Passes auto-continue without prompting, -up to a maximum of `(dirty specs + 5)` passes. The agent stops early if -everything is complete. +**Autopilot mode** (`--autopilot`): Sets the SDK agent mode to `autopilot`. +Passes auto-continue without prompting, up to a maximum of `(dirty specs + 5)` +passes. The agent stops early if everything is complete. The agent is instructed to follow a **depth-over-breadth** philosophy: it fully completes each component (implementation + tests + interactive demo) before diff --git a/scripts/compile.ts b/scripts/compile.ts index 7c0d681..3043f64 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -613,11 +613,11 @@ async function promptBuildConfig( return { model: model.id, effort, distDir, supportsEffort }; } -function printBuildHeader(target: string, config: BuildConfig): void { +function printBuildHeader(target: string, config: BuildConfig, mode: string): void { const effortStr = config.effort ? ` · Effort: ${config.effort}` : ""; log(`\n${chalk.cyan("●")} ${chalk.bold("TUIkit compiler")}`); log(` Target: ${chalk.bold(target)} · Model: ${chalk.bold(config.model)}${effortStr}`); - log(` Output: ${relative(SPECS_DIR, config.distDir)}/\n`); + log(` Output: ${relative(SPECS_DIR, config.distDir)}/ · Mode: ${mode}\n`); } function printSummary( @@ -715,7 +715,7 @@ async function cmdBuild( writeFileSync(promptPath, prompt); // 5. Print header - printBuildHeader(target, config); + printBuildHeader(target, config, sessionMode); log(` ${chalk.dim(`${dirty.length} dirty specs to compile`)}\n`); // 6. Metrics @@ -732,6 +732,7 @@ async function cmdBuild( }; // 7. Create session + const sessionMode = autopilot ? "autopilot" : "interactive"; const sessionConfig: Record = { model: config.model, onPermissionRequest: approveAll, @@ -815,7 +816,13 @@ your final message like this: process.exit(1); } - // 8. SIGINT handler + // 8. Set SDK agent mode + await session.rpc.mode.set({ mode: sessionMode }); + if (verbose) { + log(chalk.dim(` Agent mode: ${sessionMode}`)); + } + + // 9. SIGINT handler let aborted = false; const sigintHandler = async () => { if (aborted) return; @@ -832,7 +839,7 @@ your final message like this: }; process.on("SIGINT", sigintHandler); - // 9. Event handlers + // 10. Event handlers let currentPhase = "Starting"; if (verbose) { @@ -1198,7 +1205,7 @@ Build options: --effort Reasoning effort: low | medium | high | xhigh (default: high) --verbose Show full agent transcript (raw streaming output) --no-lock Suppress agent lock instructions (agent won't lock components) - --autopilot Auto-run passes without confirmation (max: components + 5) + --autopilot Use SDK autopilot mode — agent runs all passes autonomously Common options: --out Output directory for compiled code (default: dist/) From ac0ba5bfb6fe037b4bbc954236dc74f949e6ec42 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Thu, 30 Apr 2026 15:26:12 +0200 Subject: [PATCH 24/27] feat: keep manual multi-pass loop in both modes, SDK mode sets agent behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 3043f64..164ddb0 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -977,8 +977,6 @@ your final message like this: // 11. Multi-pass loop const maxPasses = dirty.length + 5; while (!aborted) { - // In autopilot mode, auto-continue up to maxPasses - // In interactive mode, prompt the user if (autopilot) { if (passNumber >= maxPasses) { log(chalk.dim(` Autopilot: reached max passes (${maxPasses}), stopping.\n`)); @@ -993,11 +991,6 @@ your final message like this: passNumber++; currentPhase = "Starting"; - // Reset per-pass metrics (keep cumulative totals) - const prevTokensIn = metrics.inputTokens; - const prevTokensOut = metrics.outputTokens; - const prevReasoning = metrics.reasoningTokens; - const prevToolCalls = metrics.toolCalls; metrics.errors = []; const passLabel = autopilot ? `Pass ${passNumber}/${maxPasses}` : `Pass ${passNumber}`; From cc51de0dbbbbf42f8dc2e5479b77d521d385bb4a Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Thu, 30 Apr 2026 15:36:05 +0200 Subject: [PATCH 25/27] refactor: autopilot flag only sets SDK mode, no custom pass logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 3 +- scripts/compile.ts | 95 ++++++++++++++++++++-------------------------- 2 files changed, 43 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index e4407d9..6885eba 100644 --- a/README.md +++ b/README.md @@ -233,8 +233,7 @@ After each pass, the compiler asks whether to continue. You see a boxed markdown summary of what the agent accomplished. **Autopilot mode** (`--autopilot`): Sets the SDK agent mode to `autopilot`. -Passes auto-continue without prompting, up to a maximum of `(dirty specs + 5)` -passes. The agent stops early if everything is complete. +The agent runs autonomously without user confirmation between actions. The agent is instructed to follow a **depth-over-breadth** philosophy: it fully completes each component (implementation + tests + interactive demo) before diff --git a/scripts/compile.ts b/scripts/compile.ts index 164ddb0..741f3a5 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -974,68 +974,57 @@ your final message like this: log(""); } - // 11. Multi-pass loop - const maxPasses = dirty.length + 5; - while (!aborted) { - if (autopilot) { - if (passNumber >= maxPasses) { - log(chalk.dim(` Autopilot: reached max passes (${maxPasses}), stopping.\n`)); - break; - } - } else if (process.stdin.isTTY) { + // 11. Multi-pass loop (interactive mode only — autopilot is handled by the SDK) + if (!autopilot) { + while (!aborted && process.stdin.isTTY) { const wantMore = await confirmPass(); if (!wantMore) break; - } else { - break; - } - passNumber++; - currentPhase = "Starting"; - metrics.errors = []; - - const passLabel = autopilot ? `Pass ${passNumber}/${maxPasses}` : `Pass ${passNumber}`; - log(`\n${chalk.cyan("●")} ${passLabel} — sending improvement prompt...\n`); - - await session.send({ - prompt: [ - "Do another pass over the compilation output.", - "Re-read the original spec files and the compile prompt at " + - `\`${relative(SPECS_DIR, promptPath)}\` to check what you may have missed.`, - "", - "Remember: DEPTH OVER BREADTH. A few components working perfectly", - "(with interactive demo) is better than many half-working ones.", - "", - "Focus on:", - "- The interactive demo (`--interactive`) — it MUST work as a full-screen playground", - "- Components already implemented: polish, fix bugs, ensure full interactivity", - "- Tests that are failing or missing", - "- Add the NEXT component (fully: implementation + tests + demo wiring)", - "- Token usage correctness", - "After fixing, run the tests and verify `--interactive` works, then report results.", - ].join("\n"), - }); + passNumber++; + currentPhase = "Starting"; + metrics.errors = []; + + log(`\n${chalk.cyan("●")} Pass ${passNumber} — sending improvement prompt...\n`); + + await session.send({ + prompt: [ + "Do another pass over the compilation output.", + "Re-read the original spec files and the compile prompt at " + + `\`${relative(SPECS_DIR, promptPath)}\` to check what you may have missed.`, + "", + "Remember: DEPTH OVER BREADTH. A few components working perfectly", + "(with interactive demo) is better than many half-working ones.", + "", + "Focus on:", + "- The interactive demo (`--interactive`) — it MUST work as a full-screen playground", + "- Components already implemented: polish, fix bugs, ensure full interactivity", + "- Tests that are failing or missing", + "- Add the NEXT component (fully: implementation + tests + demo wiring)", + "- Token usage correctness", + "After fixing, run the tests and verify `--interactive` works, then report results.", + ].join("\n"), + }); - await waitForIdle(); + await waitForIdle(); - // Complete final phase - if (!verbose && currentPhase !== "Starting") { - log(` ${chalk.green("✓")} ${currentPhase}`); - } + if (!verbose && currentPhase !== "Starting") { + log(` ${chalk.green("✓")} ${currentPhase}`); + } - // Show the agent's last message as a pass recap - if (metrics.lastAssistantMessage) { - const rendered = marked(metrics.lastAssistantMessage.trim()) as string; - log(`\n${boxen(rendered.trimEnd(), { padding: 1, dimBorder: true, title: "Agent summary", titleAlignment: "left" })}`); - } + if (metrics.lastAssistantMessage) { + const rendered = marked(metrics.lastAssistantMessage.trim()) as string; + log(`\n${boxen(rendered.trimEnd(), { padding: 1, dimBorder: true, title: "Agent summary", titleAlignment: "left" })}`); + } - printSummary(target, config, metrics, outDir, noLock, passNumber); + printSummary(target, config, metrics, outDir, noLock, passNumber); - if (metrics.errors.length > 0) { - log(chalk.yellow("⚠ Pass completed with errors:")); - for (const err of metrics.errors) { - log(` ${chalk.red("•")} ${err}`); + if (metrics.errors.length > 0) { + log(chalk.yellow("⚠ Pass completed with errors:")); + for (const err of metrics.errors) { + log(` ${chalk.red("•")} ${err}`); + } + log(""); } - log(""); } } From b35c673448f2ba00350cd2428fb40b3eeaaaa9da Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Thu, 30 Apr 2026 15:37:46 +0200 Subject: [PATCH 26/27] fix: move sessionMode declaration before first use Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/compile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/compile.ts b/scripts/compile.ts index 741f3a5..0f86ad2 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -715,6 +715,7 @@ async function cmdBuild( writeFileSync(promptPath, prompt); // 5. Print header + const sessionMode = autopilot ? "autopilot" : "interactive"; printBuildHeader(target, config, sessionMode); log(` ${chalk.dim(`${dirty.length} dirty specs to compile`)}\n`); @@ -732,7 +733,6 @@ async function cmdBuild( }; // 7. Create session - const sessionMode = autopilot ? "autopilot" : "interactive"; const sessionConfig: Record = { model: config.model, onPermissionRequest: approveAll, From 72c8969f5cbef48c340fb5e6a751ed008654a7e5 Mon Sep 17 00:00:00 2001 From: Ismael Canal Date: Thu, 30 Apr 2026 16:55:46 +0200 Subject: [PATCH 27/27] docs: update README with new autopilot approach (SDK-driven, no custom loop) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6885eba..00165be 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Optional: --effort Reasoning effort: low | medium | high | xhigh --verbose Show full agent transcript (raw streaming) --no-lock Prevent the agent from locking components - --autopilot Use SDK autopilot mode (agent runs autonomously, max: specs + 5 passes) + --autopilot Use SDK autopilot mode (agent runs fully autonomously) --all-targets Compile all targets sequentially ``` @@ -213,14 +213,22 @@ bun run compile build --target bun # 3. Or compile non-interactively with all options bun run compile build --target bun --model claude-sonnet-4 --out dist/bun-claude -# 4. Or fire-and-forget with autopilot (auto-runs multiple passes) +# 4. Or fire-and-forget with autopilot (SDK handles everything) bun run compile build --target bun --model claude-sonnet-4 --autopilot ``` ### Multi-pass compilation -The compiler supports multiple passes within a single session. Each pass builds -on the previous output — the agent reviews, fixes, and extends its own work. +The compiler supports two modes, controlled by the `--autopilot` flag: + +**Interactive mode** (default): The SDK agent runs in `interactive` mode. +After the initial compilation pass, the compiler asks whether to continue with +another pass. Each pass sends an improvement prompt — the agent reviews, fixes, +and extends its own work. You see a boxed markdown summary after each pass. + +**Autopilot mode** (`--autopilot`): Sets the SDK agent mode to `autopilot`. +The agent runs fully autonomously — it decides when to iterate, how many passes +to make, and when the work is complete. No user confirmation is needed. | Pass | Focus | Typical outcome | | ---- | ----- | --------------- | @@ -228,13 +236,6 @@ on the previous output — the agent reviews, fixes, and extends its own work. | **2nd** | Extend & fix | More components added, test failures fixed, demo polished. | | **3rd** | Polish | Catches subtle spec violations, hardens edge cases. | -**Interactive mode** (default): The agent runs in the SDK's `interactive` mode. -After each pass, the compiler asks whether to continue. You see a boxed markdown -summary of what the agent accomplished. - -**Autopilot mode** (`--autopilot`): Sets the SDK agent mode to `autopilot`. -The agent runs autonomously without user confirmation between actions. - The agent is instructed to follow a **depth-over-breadth** philosophy: it fully completes each component (implementation + tests + interactive demo) before moving to the next one.