ai-spec-dev 0.30.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ import { Command } from "commander";
2
+ import * as path from "path";
3
+ import * as fs from "fs-extra";
4
+ import chalk from "chalk";
5
+ import { loadRunLogs } from "../../core/run-trend";
6
+
7
+ export function registerLogs(program: Command): void {
8
+ program
9
+ .command("logs")
10
+ .description("List recent run logs with stage timing")
11
+ .argument("[runId]", "Show detailed stage breakdown for a specific run ID")
12
+ .option("--last <n>", "Number of runs to list (default: 10)", "10")
13
+ .action(async (runId: string | undefined, opts: { last: string }) => {
14
+ const currentDir = process.cwd();
15
+ const logDir = path.join(currentDir, ".ai-spec-logs");
16
+
17
+ if (!(await fs.pathExists(logDir))) {
18
+ console.log(chalk.yellow("\n No run logs found (.ai-spec-logs/ does not exist).\n"));
19
+ return;
20
+ }
21
+
22
+ if (runId) {
23
+ // ── Detail view for a single run ──────────────────────────────────────
24
+ const logPath = path.join(logDir, `${runId}.json`);
25
+ if (!(await fs.pathExists(logPath))) {
26
+ console.log(chalk.red(`\n Run not found: ${runId}\n`));
27
+ return;
28
+ }
29
+ const log = await fs.readJson(logPath);
30
+
31
+ console.log(chalk.cyan(`\n─── Run: ${log.runId} ─────────────────────────────────`));
32
+ console.log(chalk.gray(` Started : ${log.startedAt}`));
33
+ if (log.endedAt) console.log(chalk.gray(` Ended : ${log.endedAt}`));
34
+ if (log.totalDurationMs !== undefined)
35
+ console.log(chalk.gray(` Duration: ${(log.totalDurationMs / 1000).toFixed(1)}s`));
36
+ if (log.provider) console.log(chalk.gray(` Provider: ${log.provider} / ${log.model ?? "?"}`));
37
+ if (log.promptHash) console.log(chalk.gray(` Prompt : ${log.promptHash}`));
38
+ if (log.harnessScore !== undefined)
39
+ console.log(chalk.white(` Score : ${log.harnessScore}/10`));
40
+ if (log.filesWritten?.length)
41
+ console.log(chalk.gray(` Files : ${log.filesWritten.length} written`));
42
+ if (log.errors?.length)
43
+ console.log(chalk.yellow(` Errors : ${log.errors.length}`));
44
+
45
+ if (log.entries?.length) {
46
+ console.log(chalk.bold("\n Stages:\n"));
47
+ const doneEvents = (log.entries as Array<{ event: string; data?: Record<string, unknown>; ts: string }>)
48
+ .filter((e) => e.event.endsWith(":done") || e.event.endsWith(":failed"));
49
+
50
+ for (const entry of doneEvents) {
51
+ const isOk = entry.event.endsWith(":done");
52
+ const stage = entry.event.replace(/:done$|:failed$/, "");
53
+ const dur = entry.data?.durationMs
54
+ ? chalk.gray(` ${(Number(entry.data.durationMs) / 1000).toFixed(1)}s`)
55
+ : "";
56
+ const mark = isOk ? chalk.green("✔") : chalk.red("✘");
57
+ console.log(` ${mark} ${stage.padEnd(20)}${dur}`);
58
+ }
59
+ }
60
+ console.log(chalk.cyan("─".repeat(52)));
61
+ return;
62
+ }
63
+
64
+ // ── List view ─────────────────────────────────────────────────────────────
65
+ const logs = await loadRunLogs(currentDir);
66
+ const last = parseInt(opts.last, 10) || 10;
67
+ const shown = logs.slice(0, last);
68
+
69
+ if (shown.length === 0) {
70
+ console.log(chalk.yellow("\n No run logs found.\n"));
71
+ return;
72
+ }
73
+
74
+ console.log(chalk.cyan("\n─── Run Logs ────────────────────────────────────────────────"));
75
+ console.log(chalk.gray(
76
+ "\n " +
77
+ "Run ID ".padEnd(26) +
78
+ "Date " +
79
+ "Score ".padStart(6) +
80
+ " Files Dur\n"
81
+ ));
82
+
83
+ for (const log of shown) {
84
+ const date = log.startedAt.slice(0, 10);
85
+ const score = log.harnessScore !== undefined
86
+ ? (log.harnessScore >= 8 ? chalk.green : log.harnessScore >= 6 ? chalk.yellow : chalk.red)(
87
+ log.harnessScore.toFixed(1).padStart(5)
88
+ )
89
+ : chalk.gray(" —");
90
+ const files = String(log.filesWritten?.length ?? 0).padStart(5);
91
+ const dur = log.totalDurationMs !== undefined
92
+ ? chalk.gray((log.totalDurationMs / 1000).toFixed(0) + "s")
93
+ : chalk.gray("—");
94
+ const errMark = (log.errors?.length ?? 0) > 0
95
+ ? chalk.yellow(` ⚠${log.errors.length}`)
96
+ : "";
97
+
98
+ console.log(` ${chalk.white(log.runId.padEnd(25))} ${chalk.gray(date)} ${score} ${chalk.gray(files)} ${dur}${errMark}`);
99
+ }
100
+
101
+ console.log(chalk.gray(`\n Showing ${shown.length} of ${logs.length} run(s) · logs: .ai-spec-logs/`));
102
+ console.log(chalk.cyan("─".repeat(63)));
103
+ console.log(chalk.gray(` Tip: ai-spec logs <runId> to see stage breakdown`));
104
+ console.log(chalk.gray(` ai-spec trend to see score trend by prompt version\n`));
105
+ });
106
+ }
@@ -0,0 +1,156 @@
1
+ import { Command } from "commander";
2
+ import * as path from "path";
3
+ import * as fs from "fs-extra";
4
+ import chalk from "chalk";
5
+ import { input, select, confirm } from "@inquirer/prompts";
6
+ import {
7
+ DEFAULT_MODELS,
8
+ ENV_KEY_MAP,
9
+ PROVIDER_CATALOG,
10
+ } from "../../core/spec-generator";
11
+ import { AiSpecConfig, CONFIG_FILE, loadConfig } from "../utils";
12
+
13
+ export function registerModel(program: Command): void {
14
+ program
15
+ .command("model")
16
+ .description("Interactively switch the active AI provider/model and save to .ai-spec.json")
17
+ .option("--list", "List all available providers and models")
18
+ .action(async (opts) => {
19
+ const currentDir = process.cwd();
20
+ const configPath = path.join(currentDir, CONFIG_FILE);
21
+
22
+ // ── --list ──────────────────────────────────────────────────────────────
23
+ if (opts.list) {
24
+ console.log(chalk.bold("\nAvailable providers & models:\n"));
25
+ for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
26
+ console.log(
27
+ ` ${chalk.bold.cyan(key.padEnd(10))} ${chalk.white(meta.displayName)}`
28
+ );
29
+ console.log(chalk.gray(` ${meta.description}`));
30
+ console.log(
31
+ chalk.gray(
32
+ ` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
33
+ )
34
+ );
35
+ console.log();
36
+ }
37
+ return;
38
+ }
39
+
40
+ const existing: AiSpecConfig = await loadConfig(currentDir);
41
+
42
+ console.log(chalk.blue("\n─── Model Switcher ─────────────────────────────"));
43
+ if (Object.keys(existing).length > 0) {
44
+ console.log(
45
+ chalk.gray(
46
+ ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` +
47
+ (existing.codegenProvider
48
+ ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}`
49
+ : "")
50
+ )
51
+ );
52
+ }
53
+ console.log();
54
+
55
+ const target = await select({
56
+ message: "Configure model for:",
57
+ choices: [
58
+ { name: "Spec generation (used for spec writing & refinement)", value: "spec" },
59
+ { name: "Code generation (used when --codegen api is active)", value: "codegen" },
60
+ { name: "Both (same provider/model for all tasks)", value: "both" },
61
+ ],
62
+ });
63
+
64
+ async function pickProviderAndModel(label: string): Promise<{ provider: string; model: string }> {
65
+ const providerKey = await select({
66
+ message: `${label} — select provider:`,
67
+ choices: Object.entries(PROVIDER_CATALOG).map(([key, meta]) => ({
68
+ name: `${meta.displayName.padEnd(22)} ${chalk.gray(meta.description)}`,
69
+ value: key,
70
+ short: meta.displayName,
71
+ })),
72
+ });
73
+
74
+ const meta = PROVIDER_CATALOG[providerKey];
75
+ const modelChoices = [
76
+ ...meta.models.map((m) => ({ name: m, value: m })),
77
+ { name: chalk.italic("✎ Enter custom model name..."), value: "__custom__" },
78
+ ];
79
+
80
+ let chosenModel = await select({
81
+ message: `${label} — select model (${meta.displayName}):`,
82
+ choices: modelChoices,
83
+ });
84
+
85
+ if (chosenModel === "__custom__") {
86
+ chosenModel = await input({
87
+ message: "Enter model name:",
88
+ validate: (v) => v.trim().length > 0 || "Model name cannot be empty",
89
+ });
90
+ }
91
+
92
+ return { provider: providerKey, model: chosenModel };
93
+ }
94
+
95
+ const updated: AiSpecConfig = { ...existing };
96
+
97
+ if (target === "spec" || target === "both") {
98
+ const { provider, model } = await pickProviderAndModel("Spec");
99
+ updated.provider = provider;
100
+ updated.model = model;
101
+ }
102
+
103
+ if (target === "codegen" || target === "both") {
104
+ if (target === "both") {
105
+ updated.codegenProvider = updated.provider;
106
+ updated.codegenModel = updated.model;
107
+ } else {
108
+ const { provider, model } = await pickProviderAndModel("Codegen");
109
+ updated.codegenProvider = provider;
110
+ updated.codegenModel = model;
111
+ }
112
+
113
+ const effectiveCodegenProvider = updated.codegenProvider ?? updated.provider ?? "gemini";
114
+ if (effectiveCodegenProvider !== "claude") {
115
+ if (!updated.codegen || updated.codegen === "claude-code") {
116
+ updated.codegen = "api";
117
+ console.log(
118
+ chalk.yellow(
119
+ `\n ⚠ provider "${effectiveCodegenProvider}" 不支持 "claude-code" 模式。`
120
+ )
121
+ );
122
+ console.log(chalk.gray(` 已自动将 codegen 模式设为 "api"。`));
123
+ }
124
+ }
125
+ }
126
+
127
+ console.log(chalk.blue("\n Preview:"));
128
+ console.log(chalk.gray(` spec → ${updated.provider}/${updated.model}`));
129
+ if (updated.codegenProvider) {
130
+ console.log(
131
+ chalk.gray(
132
+ ` codegen → ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
133
+ )
134
+ );
135
+ }
136
+
137
+ const ok = await confirm({ message: "Save to .ai-spec.json?", default: true });
138
+ if (!ok) {
139
+ console.log(chalk.gray(" Cancelled."));
140
+ return;
141
+ }
142
+
143
+ await fs.writeJson(configPath, updated, { spaces: 2 });
144
+ console.log(chalk.green(`\n ✔ Saved to ${configPath}`));
145
+
146
+ const providerToCheck = updated.provider ?? "gemini";
147
+ const envKey = ENV_KEY_MAP[providerToCheck];
148
+ if (envKey && !process.env[envKey]) {
149
+ console.log(
150
+ chalk.yellow(
151
+ ` ⚠ Remember to set ${envKey} in your environment or .env file.`
152
+ )
153
+ );
154
+ }
155
+ });
156
+ }
@@ -0,0 +1,22 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { RunSnapshot } from "../../core/run-snapshot";
4
+
5
+ export function registerRestore(program: Command): void {
6
+ program
7
+ .command("restore")
8
+ .description("Restore files modified by a previous run")
9
+ .argument("<runId>", "Run ID shown at the end of a create / generate run")
10
+ .action(async (runId: string) => {
11
+ const currentDir = process.cwd();
12
+ const snapshot = new RunSnapshot(currentDir, runId);
13
+ console.log(chalk.blue(`Restoring run: ${runId}...`));
14
+ const restored = await snapshot.restore();
15
+ if (restored.length === 0) {
16
+ console.log(chalk.yellow(" No backup found for this run ID."));
17
+ } else {
18
+ restored.forEach((f) => console.log(chalk.green(` ✔ restored: ${f}`)));
19
+ console.log(chalk.bold.green(`\n✔ ${restored.length} file(s) restored.`));
20
+ }
21
+ });
22
+ }
@@ -0,0 +1,63 @@
1
+ import { Command } from "commander";
2
+ import * as path from "path";
3
+ import * as fs from "fs-extra";
4
+ import chalk from "chalk";
5
+ import { createProvider, DEFAULT_MODELS, SUPPORTED_PROVIDERS } from "../../core/spec-generator";
6
+ import { CodeReviewer } from "../../core/reviewer";
7
+ import { loadConfig, resolveApiKey } from "../utils";
8
+
9
+ export function registerReview(program: Command): void {
10
+ program
11
+ .command("review")
12
+ .description("Run AI code review on current git diff against a spec")
13
+ .argument("[specFile]", "Path to spec file (auto-detects latest in specs/ if omitted)")
14
+ .option(
15
+ "--provider <name>",
16
+ `AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
17
+ undefined
18
+ )
19
+ .option("--model <name>", "Model name")
20
+ .option("-k, --key <apiKey>", "API key")
21
+ .action(async (specFile: string | undefined, opts) => {
22
+ const currentDir = process.cwd();
23
+ const config = await loadConfig(currentDir);
24
+
25
+ const providerName = opts.provider || config.provider || "gemini";
26
+ const modelName = opts.model || config.model || DEFAULT_MODELS[providerName];
27
+ const apiKey = await resolveApiKey(providerName, opts.key);
28
+
29
+ const provider = createProvider(providerName, apiKey, modelName);
30
+ const reviewer = new CodeReviewer(provider, currentDir);
31
+
32
+ let specContent = "";
33
+ let resolvedSpecFile: string | undefined;
34
+
35
+ if (specFile && (await fs.pathExists(specFile))) {
36
+ specContent = await fs.readFile(specFile, "utf-8");
37
+ resolvedSpecFile = specFile;
38
+ console.log(chalk.gray(`Using spec: ${specFile}`));
39
+ } else {
40
+ // Auto-detect the latest spec in specs/
41
+ const specsDir = path.join(currentDir, "specs");
42
+ if (await fs.pathExists(specsDir)) {
43
+ const files = (await fs.readdir(specsDir))
44
+ .filter((f) => f.endsWith(".md"))
45
+ .sort()
46
+ .reverse();
47
+ if (files.length > 0) {
48
+ const latest = path.join(specsDir, files[0]);
49
+ specContent = await fs.readFile(latest, "utf-8");
50
+ resolvedSpecFile = latest;
51
+ console.log(chalk.gray(`Auto-detected spec: specs/${files[0]}`));
52
+ }
53
+ }
54
+ }
55
+
56
+ if (!specContent) {
57
+ console.log(chalk.yellow("No spec file found. Running review without spec context."));
58
+ }
59
+
60
+ await reviewer.reviewCode(specContent, resolvedSpecFile);
61
+ await reviewer.printScoreTrend();
62
+ });
63
+ }
@@ -0,0 +1,36 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { loadRunLogs, buildTrendReport, printTrendReport } from "../../core/run-trend";
4
+
5
+ export function registerTrend(program: Command): void {
6
+ program
7
+ .command("trend")
8
+ .description("Show harness score trend across past create runs")
9
+ .option("--last <n>", "Number of recent scored runs to show (default: 15)", "15")
10
+ .option("--prompt <hash>", "Filter to a specific prompt hash (prefix match)")
11
+ .option("--json", "Output raw JSON instead of formatted table")
12
+ .action(async (opts: { last: string; prompt?: string; json?: boolean }) => {
13
+ const currentDir = process.cwd();
14
+ const last = parseInt(opts.last, 10) || 15;
15
+
16
+ const logs = await loadRunLogs(currentDir);
17
+ if (logs.length === 0) {
18
+ console.log(chalk.yellow(
19
+ "\n No run logs found. Run `ai-spec create` at least once to start tracking.\n"
20
+ ));
21
+ return;
22
+ }
23
+
24
+ const report = buildTrendReport(logs, {
25
+ last,
26
+ promptFilter: opts.prompt,
27
+ });
28
+
29
+ if (opts.json) {
30
+ console.log(JSON.stringify(report, null, 2));
31
+ return;
32
+ }
33
+
34
+ printTrendReport(report, currentDir);
35
+ });
36
+ }
@@ -0,0 +1,178 @@
1
+ import { Command } from "commander";
2
+ import * as path from "path";
3
+ import * as fs from "fs-extra";
4
+ import chalk from "chalk";
5
+ import { input } from "@inquirer/prompts";
6
+ import { createProvider, DEFAULT_MODELS, SUPPORTED_PROVIDERS } from "../../core/spec-generator";
7
+ import { ContextLoader } from "../../core/context-loader";
8
+ import { CodeGenerator } from "../../core/code-generator";
9
+ import { CodeReviewer } from "../../core/reviewer";
10
+ import { SpecUpdater } from "../../core/spec-updater";
11
+ import { accumulateReviewKnowledge } from "../../core/knowledge-memory";
12
+ import { generateRunId, RunLogger, setActiveLogger } from "../../core/run-logger";
13
+ import { RunSnapshot, setActiveSnapshot } from "../../core/run-snapshot";
14
+ import { detectRepoType } from "../../core/workspace-loader";
15
+ import { getCodeGenSystemPrompt } from "../../prompts/codegen.prompt";
16
+ import { loadConfig, resolveApiKey } from "../utils";
17
+
18
+ export function registerUpdate(program: Command): void {
19
+ program
20
+ .command("update")
21
+ .description("Update an existing spec with a change request, re-extract DSL, and identify affected files")
22
+ .argument("[change]", "Change description (prompted if omitted)")
23
+ .option("--provider <name>", `AI provider (${SUPPORTED_PROVIDERS.join("|")})`, undefined)
24
+ .option("--model <name>", "Model name")
25
+ .option("-k, --key <apiKey>", "API key")
26
+ .option("--spec <path>", "Path to the existing spec file (auto-detected if omitted)")
27
+ .option("--codegen", "Regenerate affected files automatically after updating spec")
28
+ .option("--codegen-provider <name>", "Provider for code generation")
29
+ .option("--codegen-model <name>", "Model for code generation")
30
+ .option("--codegen-key <key>", "API key for code generation")
31
+ .option("--skip-affected", "Skip identifying affected files")
32
+ .action(async (change: string | undefined, opts) => {
33
+ const currentDir = process.cwd();
34
+ const config = await loadConfig(currentDir);
35
+
36
+ if (!change) {
37
+ change = await input({
38
+ message: "Describe the change you want to make:",
39
+ validate: (v) => v.trim().length > 0 || "Change description cannot be empty",
40
+ });
41
+ }
42
+
43
+ const providerName = opts.provider || config.provider || "gemini";
44
+ const modelName = opts.model || config.model || DEFAULT_MODELS[providerName];
45
+ const apiKey = await resolveApiKey(providerName, opts.key);
46
+ const provider = createProvider(providerName, apiKey, modelName);
47
+
48
+ console.log(chalk.blue("\n─── ai-spec update ─────────────────────────────"));
49
+ console.log(chalk.gray(` Provider: ${providerName}/${modelName}`));
50
+
51
+ const updateRunId = generateRunId();
52
+ const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
53
+ setActiveSnapshot(updateSnapshot);
54
+ const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
55
+ setActiveLogger(updateLogger);
56
+ console.log(chalk.gray(` Run ID: ${updateRunId}`));
57
+
58
+ let specPath: string | null = opts.spec ?? null;
59
+ if (!specPath) {
60
+ const specsDir = path.join(currentDir, "specs");
61
+ const latest = await SpecUpdater.findLatestSpec(specsDir);
62
+ if (!latest) {
63
+ console.error(chalk.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
64
+ process.exit(1);
65
+ }
66
+ specPath = latest.filePath;
67
+ console.log(chalk.gray(` Using spec: ${path.relative(currentDir, specPath)} (v${latest.version})`));
68
+ }
69
+
70
+ console.log(chalk.gray(" Loading project context..."));
71
+ const loader = new ContextLoader(currentDir);
72
+ const context = await loader.loadProjectContext();
73
+ if (context.constitution && context.constitution.length > 6000) {
74
+ console.log(chalk.yellow(` ⚠ Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
75
+ }
76
+
77
+ const { type: repoType } = await detectRepoType(currentDir);
78
+
79
+ const updater = new SpecUpdater(provider);
80
+ let result;
81
+ try {
82
+ result = await updater.update(change!, specPath, currentDir, context, {
83
+ skipAffectedFiles: opts.skipAffected,
84
+ repoType,
85
+ });
86
+ } catch (err) {
87
+ console.error(chalk.red(` Update failed: ${(err as Error).message}`));
88
+ process.exit(1);
89
+ }
90
+
91
+ console.log(chalk.green(`\n ✔ Spec updated → v${result.newVersion}: ${path.relative(currentDir, result.newSpecPath)}`));
92
+ if (result.newDslPath) {
93
+ console.log(chalk.green(` ✔ DSL updated: ${path.relative(currentDir, result.newDslPath)}`));
94
+ }
95
+
96
+ if (result.affectedFiles.length > 0) {
97
+ console.log(chalk.cyan("\n Affected files:"));
98
+ for (const f of result.affectedFiles) {
99
+ const icon = f.action === "create" ? chalk.green("+") : chalk.yellow("~");
100
+ console.log(` ${icon} ${f.file}: ${chalk.gray(f.description)}`);
101
+ }
102
+ }
103
+
104
+ if (opts.codegen && result.affectedFiles.length > 0) {
105
+ const codegenProviderName = opts.codegenProvider || config.codegenProvider || providerName;
106
+ const codegenModelName = opts.codegenModel || config.codegenModel || DEFAULT_MODELS[codegenProviderName];
107
+ const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
108
+ const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
109
+
110
+ console.log(chalk.blue("\n Regenerating affected files..."));
111
+ new CodeGenerator(codegenProvider, "api");
112
+
113
+ const specContent = await fs.readFile(result.newSpecPath, "utf-8");
114
+ const constitutionSection = context.constitution
115
+ ? `\n=== Project Constitution (MUST follow) ===\n${context.constitution}\n`
116
+ : "";
117
+ const dslSection = result.updatedDsl
118
+ ? `\n=== DSL Context ===\n${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3000)}\n`
119
+ : "";
120
+
121
+ updateLogger.stageStart("update_codegen");
122
+ for (const affected of result.affectedFiles) {
123
+ const fullPath = path.join(currentDir, affected.file);
124
+ let existing = "";
125
+ try { existing = await fs.readFile(fullPath, "utf-8"); } catch { /* new file */ }
126
+
127
+ const codePrompt = `Apply this change to the file.
128
+
129
+ Change: ${change}
130
+ File: ${affected.file}
131
+ Purpose: ${affected.description}
132
+
133
+ === Feature Spec (updated) ===
134
+ ${specContent}
135
+ ${constitutionSection}${dslSection}
136
+ === ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
137
+ ${existing || "Create from scratch."}`;
138
+
139
+ process.stdout.write(` ${existing ? chalk.yellow("~") : chalk.green("+")} ${affected.file}... `);
140
+ try {
141
+ const raw = await codegenProvider.generate(codePrompt, getCodeGenSystemPrompt(repoType));
142
+ const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
143
+ await fs.ensureDir(path.dirname(fullPath));
144
+ await updateSnapshot.snapshotFile(fullPath);
145
+ await fs.writeFile(fullPath, content, "utf-8");
146
+ updateLogger.fileWritten(affected.file);
147
+ console.log(chalk.green("✔"));
148
+ } catch (err) {
149
+ updateLogger.stageFail("update_codegen", `${affected.file}: ${(err as Error).message}`);
150
+ console.log(chalk.red(`✘ ${(err as Error).message}`));
151
+ }
152
+ }
153
+ updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
154
+
155
+ const updatedSpecContent = await fs.readFile(result.newSpecPath, "utf-8").catch(() => "");
156
+ if (updatedSpecContent) {
157
+ const updateReviewer = new CodeReviewer(provider, currentDir);
158
+ const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
159
+ if (reviewResult && reviewResult !== "No changes") {
160
+ await accumulateReviewKnowledge(provider, currentDir, reviewResult);
161
+ }
162
+ }
163
+ }
164
+
165
+ updateLogger.finish();
166
+ updateLogger.printSummary();
167
+ if (updateSnapshot.fileCount > 0) {
168
+ console.log(chalk.gray(` To undo changes: ai-spec restore ${updateRunId}`));
169
+ }
170
+
171
+ if (!opts.codegen && result.affectedFiles.length > 0) {
172
+ console.log(chalk.blue("\n Next steps:"));
173
+ console.log(chalk.gray(` • Re-run with --codegen to regenerate affected files automatically`));
174
+ console.log(chalk.gray(` • Or update files manually based on the affected files list above`));
175
+ console.log(chalk.gray(` • Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
176
+ }
177
+ });
178
+ }