ai-spec-dev 0.41.0 → 0.42.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.
Files changed (37) hide show
  1. package/.ai-spec-workspace.json +17 -0
  2. package/.ai-spec.json +7 -0
  3. package/cli/pipeline/single-repo.ts +19 -10
  4. package/core/cli-ui.ts +136 -0
  5. package/core/code-generator.ts +4 -2
  6. package/core/error-feedback.ts +4 -2
  7. package/core/provider-utils.ts +8 -7
  8. package/demo-backend/.ai-spec-constitution.md +65 -0
  9. package/demo-backend/package.json +21 -0
  10. package/demo-backend/prisma/schema.prisma +22 -0
  11. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +186 -0
  12. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +211 -0
  13. package/demo-backend/src/controllers/bookmark.controller.test.ts +255 -0
  14. package/demo-backend/src/controllers/bookmark.controller.ts +187 -0
  15. package/demo-backend/src/index.ts +17 -0
  16. package/demo-backend/src/routes/bookmark.routes.test.ts +264 -0
  17. package/demo-backend/src/routes/bookmark.routes.ts +11 -0
  18. package/demo-backend/src/routes/index.ts +8 -0
  19. package/demo-backend/src/services/bookmark.service.test.ts +433 -0
  20. package/demo-backend/src/services/bookmark.service.ts +261 -0
  21. package/demo-backend/tsconfig.json +12 -0
  22. package/demo-frontend/.ai-spec-constitution.md +95 -0
  23. package/demo-frontend/package.json +23 -0
  24. package/demo-frontend/src/App.tsx +12 -0
  25. package/demo-frontend/src/main.tsx +9 -0
  26. package/demo-frontend/tsconfig.json +13 -0
  27. package/dist/cli/index.js +130 -21
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cli/index.mjs +130 -21
  30. package/dist/cli/index.mjs.map +1 -1
  31. package/dist/index.js +80 -8
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +80 -8
  34. package/dist/index.mjs.map +1 -1
  35. package/package.json +1 -1
  36. package/RELEASE_LOG.md +0 -2962
  37. package/purpose.md +0 -1434
@@ -0,0 +1,95 @@
1
+ # Project Constitution
2
+
3
+ ## 1. 架构规则 (Architecture Rules)
4
+ - **组件分层架构**:页面/组件 → 自定义hooks → 服务层 → API客户端
5
+ - **组件组织**:页面组件位于 `src/pages/`,可复用组件位于 `src/components/`
6
+ - **业务逻辑分离**:所有API调用、数据转换和业务逻辑必须封装在自定义hooks或服务层中
7
+ - **状态管理**:使用React Context或本地状态,禁止直接修改props
8
+ - **类型安全**:所有组件props、API响应和状态必须明确定义TypeScript类型
9
+
10
+ ## 2. 命名规范 (Naming Conventions)
11
+ - **文件命名**:
12
+ - 组件文件:PascalCase (`UserProfile.tsx`)
13
+ - Hook文件:camelCase,以`use`开头 (`useAuth.ts`)
14
+ - 工具/服务文件:camelCase (`apiService.ts`)
15
+ - 样式文件:与组件同名,使用CSS模块 (`UserProfile.module.css`)
16
+ - **变量/函数**:
17
+ - 变量/函数:camelCase (`userData`, `fetchUserProfile`)
18
+ - 接口/类型:PascalCase (`UserData`, `ApiResponse`)
19
+ - 常量:UPPER_SNAKE_CASE (`API_BASE_URL`)
20
+ - **组件命名**:PascalCase (`<UserProfile />`)
21
+ - **路由路径**:kebab-case (`/user-profile`, `/admin/dashboard`)
22
+
23
+ ## 3. API 规范 (API Patterns)
24
+ - **API客户端**:使用统一的axios实例配置,位于 `src/services/apiClient.ts`
25
+ - **请求模式**:所有API调用必须封装在自定义hooks (`useApi`, `useFetch`) 或服务函数中
26
+ - **响应结构**:
27
+ ```typescript
28
+ interface ApiResponse<T> {
29
+ success: boolean;
30
+ data: T;
31
+ message?: string;
32
+ error?: string;
33
+ code?: number;
34
+ }
35
+ ```
36
+ - **错误处理**:统一使用try-catch包装,错误通过状态返回而非抛出异常
37
+ - **认证**:Token存储在localStorage,通过axios拦截器自动添加Authorization头
38
+ - **路由组织**:
39
+ - 公开路由:无需认证 (`/login`, `/register`)
40
+ - 受保护路由:需要认证 (`/dashboard`, `/profile`)
41
+ - 管理员路由:需要特定角色 (`/admin/*`)
42
+
43
+ ## 4. 数据层规范 (Data Layer Rules)
44
+ - **状态管理**:
45
+ - 全局状态:React Context + useReducer
46
+ - 本地组件状态:useState
47
+ - 服务端状态:React Query或自定义缓存hooks
48
+ - **数据获取**:所有数据必须通过服务层获取,禁止组件直接调用fetch/axios
49
+ - **类型定义**:所有API请求/响应必须定义在 `src/types/` 目录下
50
+ - **数据转换**:在服务层进行API响应到前端模型的转换
51
+
52
+ ## 5. 错误处理规范 (Error Handling Patterns)
53
+ - **全局错误边界**:App组件包裹ErrorBoundary
54
+ - **API错误**:统一在服务层处理,返回标准化错误对象
55
+ - **用户反馈**:通过Toast/Snackbar组件显示错误消息
56
+ - **错误日志**:使用console.error记录,生产环境集成错误监控
57
+ - **表单验证**:使用Formik或React Hook Form进行客户端验证
58
+
59
+ ## 6. 禁区 (Red Lines — Never Violate)
60
+ - [ ] **禁止**在组件中直接进行API调用(必须通过hooks/服务层)
61
+ - [ ] **禁止**使用`any`类型(必须明确定义或使用`unknown`)
62
+ - [ ] **禁止**修改传入的props(遵循单向数据流)
63
+ - [ ] **禁止**在组件中编写复杂业务逻辑(必须提取到hooks/服务层)
64
+ - [ ] **禁止**使用内联样式(必须使用CSS模块或styled-components)
65
+ - [ ] **禁止**提交未定义类型的代码
66
+ - [ ] **禁止**直接访问process.env(必须通过配置模块)
67
+
68
+ ## 7. 测试规范 (Testing Rules)
69
+ - **测试框架**:Vitest + React Testing Library
70
+ - **测试文件位置**:与源文件同目录,使用`.test.tsx`或`.spec.tsx`后缀
71
+ - **必须测试**:
72
+ - 所有自定义hooks
73
+ - 复杂业务逻辑函数
74
+ - 关键用户交互流程
75
+ - 组件渲染和状态变化
76
+ - **测试结构**:Arrange-Act-Assert模式
77
+ - **Mock策略**:API调用必须mock,使用MSW或手动mock
78
+ - **覆盖率要求**:核心业务逻辑必须>80%
79
+
80
+ ## 8. 共享配置文件清单 (Shared Config Files — Append-Only)
81
+
82
+ CRITICAL: The following files are **singleton config files** that already exist in the project.
83
+ When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
84
+ appended/merged into these existing files. **NEVER create a new parallel file.**
85
+
86
+ - `src/constants/apiEndpoints.ts` — API端点配置 — **MODIFY ONLY, never recreate**
87
+ - `src/types/api.d.ts` — API类型定义 — **MODIFY ONLY, never recreate**
88
+ - `src/config/env.ts` — 环境变量配置 — **MODIFY ONLY, never recreate**
89
+ - `src/utils/errorCodes.ts` — 错误码常量 — **MODIFY ONLY, never recreate**
90
+ - `src/services/apiClient.ts` — Axios实例配置 — **MODIFY ONLY, never recreate**
91
+ - `src/routes/index.tsx` — 路由配置 — **MODIFY ONLY, never recreate**
92
+ - `src/hooks/index.ts` — 自定义hooks导出 — **MODIFY ONLY, never recreate**
93
+ - `src/context/index.ts` — Context导出 — **MODIFY ONLY, never recreate**
94
+
95
+ (No i18n/locale files detected — will be populated on first run)
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "demo-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc && vite build",
8
+ "test": "vitest run"
9
+ },
10
+ "dependencies": {
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1",
13
+ "axios": "^1.7.0"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.7.0",
17
+ "@types/react": "^18.3.0",
18
+ "@types/react-dom": "^18.3.0",
19
+ "vite": "^5.4.0",
20
+ "@vitejs/plugin-react": "^4.3.0",
21
+ "vitest": "^2.1.0"
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ function App() {
4
+ return (
5
+ <div>
6
+ <h1>Bookmark Manager</h1>
7
+ <p>Coming soon...</p>
8
+ </div>
9
+ );
10
+ }
11
+
12
+ export default App;
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+
5
+ ReactDOM.createRoot(document.getElementById("root")!).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>
9
+ );
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src"
11
+ },
12
+ "include": ["src"]
13
+ }
package/dist/cli/index.js CHANGED
@@ -147,6 +147,100 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
147
147
  }
148
148
  });
149
149
 
150
+ // core/cli-ui.ts
151
+ function startSpinner(text) {
152
+ const isTTY = process.stderr.isTTY;
153
+ let frame = 0;
154
+ let currentText = text;
155
+ let stopped = false;
156
+ function render() {
157
+ if (stopped) return;
158
+ const symbol = import_chalk.default.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
159
+ if (isTTY) {
160
+ process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
161
+ }
162
+ frame++;
163
+ }
164
+ if (!isTTY) {
165
+ process.stderr.write(` \u2026 ${currentText}
166
+ `);
167
+ }
168
+ const timer = setInterval(render, 80);
169
+ render();
170
+ return {
171
+ update(newText) {
172
+ currentText = newText;
173
+ },
174
+ stop(finalText) {
175
+ if (stopped) return;
176
+ stopped = true;
177
+ clearInterval(timer);
178
+ if (isTTY) {
179
+ process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
180
+ }
181
+ if (finalText) {
182
+ process.stderr.write(` ${finalText}
183
+ `);
184
+ }
185
+ },
186
+ succeed(successText) {
187
+ this.stop(import_chalk.default.green(`\u2714 ${successText}`));
188
+ },
189
+ fail(failText) {
190
+ this.stop(import_chalk.default.red(`\u2718 ${failText}`));
191
+ }
192
+ };
193
+ }
194
+ async function retryCountdown(opts) {
195
+ const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
196
+ const isTTY = process.stderr.isTTY;
197
+ const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
198
+ process.stderr.write("\n");
199
+ process.stderr.write(import_chalk.default.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + import_chalk.default.gray(`[${label}]`) + import_chalk.default.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
200
+ `));
201
+ process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.white(shortErr) + "\n");
202
+ process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.gray(`Waiting before retry...`) + "\n");
203
+ const totalSeconds = Math.ceil(waitMs / 1e3);
204
+ for (let s = totalSeconds; s > 0; s--) {
205
+ const bar = import_chalk.default.green("\u2588".repeat(totalSeconds - s)) + import_chalk.default.gray("\u2591".repeat(s));
206
+ const line = import_chalk.default.yellow(` \u2502 `) + `${bar} ${import_chalk.default.bold.white(`${s}s`)}`;
207
+ if (isTTY) {
208
+ process.stderr.write(`\r${line}${" ".repeat(10)}`);
209
+ }
210
+ await new Promise((r) => setTimeout(r, 1e3));
211
+ }
212
+ if (isTTY) {
213
+ process.stderr.write(`\r${" ".repeat(70)}\r`);
214
+ }
215
+ process.stderr.write(import_chalk.default.yellow(` \u2514\u2500 `) + import_chalk.default.cyan(`Retrying now...`) + "\n\n");
216
+ }
217
+ function startStage(stageKey, label) {
218
+ const icon = STAGE_ICONS[stageKey] ?? "\u25B8";
219
+ return startSpinner(`${icon} ${label}`);
220
+ }
221
+ var import_chalk, SPINNER_FRAMES, STAGE_ICONS;
222
+ var init_cli_ui = __esm({
223
+ "core/cli-ui.ts"() {
224
+ "use strict";
225
+ import_chalk = __toESM(require("chalk"));
226
+ SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
227
+ STAGE_ICONS = {
228
+ context_load: "\u{1F4C2}",
229
+ design_dialogue: "\u{1F4AC}",
230
+ spec_gen: "\u{1F4DD}",
231
+ spec_refine: "\u270F\uFE0F ",
232
+ spec_assess: "\u{1F4CA}",
233
+ dsl_extract: "\u{1F517}",
234
+ dsl_gap_feedback: "\u{1F50D}",
235
+ codegen: "\u2699\uFE0F ",
236
+ test_gen: "\u{1F9EA}",
237
+ error_feedback: "\u{1F527}",
238
+ review: "\u{1F50E}",
239
+ self_eval: "\u{1F4C8}"
240
+ };
241
+ }
242
+ });
243
+
150
244
  // core/provider-utils.ts
151
245
  function classifyError(err, label) {
152
246
  const e = err;
@@ -223,21 +317,23 @@ async function withReliability(fn, opts) {
223
317
  throw classifyError(err, label);
224
318
  }
225
319
  const waitMs = attempt === 0 ? 2e3 : 6e3;
226
- console.warn(
227
- import_chalk.default.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + import_chalk.default.gray(` \u2014 ${err.message}`)
228
- );
229
320
  onRetry?.(attempt + 1, err);
230
- await sleep(waitMs);
321
+ await retryCountdown({
322
+ attempt: attempt + 1,
323
+ maxAttempts: retries + 1,
324
+ waitMs,
325
+ errorMessage: err.message ?? String(err),
326
+ label
327
+ });
231
328
  }
232
329
  }
233
330
  throw new Error("unreachable");
234
331
  }
235
- var import_chalk, sleep, ProviderError;
332
+ var ProviderError;
236
333
  var init_provider_utils = __esm({
237
334
  "core/provider-utils.ts"() {
238
335
  "use strict";
239
- import_chalk = __toESM(require("chalk"));
240
- sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
336
+ init_cli_ui();
241
337
  ProviderError = class extends Error {
242
338
  constructor(message, kind, originalError) {
243
339
  super(message);
@@ -6711,6 +6807,7 @@ function printTaskProgress(completed, total, task, mode) {
6711
6807
  }
6712
6808
 
6713
6809
  // core/code-generator.ts
6810
+ init_cli_ui();
6714
6811
  var CodeGenerator = class {
6715
6812
  constructor(provider, mode = "claude-code") {
6716
6813
  this.provider = provider;
@@ -7159,6 +7256,7 @@ ${spec}
7159
7256
  ${constitutionSection}
7160
7257
  === ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
7161
7258
  ${existingContent || "Output only the complete file content."}`;
7259
+ const fileSpinner = startSpinner(`${prefix}Generating ${import_chalk9.default.bold(item.file)}...`);
7162
7260
  try {
7163
7261
  const raw = await this.provider.generate(codePrompt, systemPrompt);
7164
7262
  const fileContent = stripCodeFences(raw);
@@ -7166,11 +7264,11 @@ ${existingContent || "Output only the complete file content."}`;
7166
7264
  await fs12.ensureDir(path11.dirname(fullPath));
7167
7265
  await fs12.writeFile(fullPath, fileContent, "utf-8");
7168
7266
  getActiveLogger()?.fileWritten(item.file);
7169
- console.log(`${prefix}${existingContent ? import_chalk9.default.yellow("~") : import_chalk9.default.green("+")} ${import_chalk9.default.bold(item.file)} ${import_chalk9.default.green("\u2714")}`);
7267
+ fileSpinner.succeed(`${existingContent ? import_chalk9.default.yellow("~") : import_chalk9.default.green("+")} ${import_chalk9.default.bold(item.file)}`);
7170
7268
  successCount++;
7171
7269
  writtenFiles.push(item.file);
7172
7270
  } catch (err) {
7173
- console.log(`${prefix}${import_chalk9.default.red("\u2718")} ${import_chalk9.default.bold(item.file)} \u2014 ${import_chalk9.default.red(err.message)}`);
7271
+ fileSpinner.fail(`${import_chalk9.default.bold(item.file)} \u2014 ${err.message}`);
7174
7272
  }
7175
7273
  }
7176
7274
  if (!taskLabel) {
@@ -8196,6 +8294,7 @@ var import_chalk15 = __toESM(require("chalk"));
8196
8294
  var import_child_process5 = require("child_process");
8197
8295
  var fs18 = __toESM(require("fs-extra"));
8198
8296
  var path17 = __toESM(require("path"));
8297
+ init_cli_ui();
8199
8298
  var MAX_COMMAND_OUTPUT_CHARS = 5e4;
8200
8299
  var MAX_FIX_FILE_CHARS = 6e4;
8201
8300
  function runCommand(cmd, cwd) {
@@ -8387,16 +8486,17 @@ ${errorSummary}
8387
8486
  ${fileContent}
8388
8487
 
8389
8488
  Output ONLY the complete fixed file content. No markdown fences, no explanations.`;
8489
+ const fixSpinner = startSpinner(`Fixing ${import_chalk15.default.bold(file)} (${fileErrors.length} error(s))...`);
8390
8490
  try {
8391
8491
  const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
8392
8492
  const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
8393
8493
  await getActiveSnapshot()?.snapshotFile(fullPath);
8394
8494
  await fs18.writeFile(fullPath, fixed, "utf-8");
8395
8495
  results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
8396
- console.log(import_chalk15.default.green(` \u2714 Auto-fixed: ${file}`));
8496
+ fixSpinner.succeed(`Auto-fixed: ${file}`);
8397
8497
  } catch (err) {
8398
8498
  results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
8399
- console.log(import_chalk15.default.yellow(` \u26A0 Could not auto-fix: ${file}`));
8499
+ fixSpinner.fail(`Could not auto-fix: ${file}`);
8400
8500
  }
8401
8501
  }
8402
8502
  return results;
@@ -11272,6 +11372,7 @@ async function listVcrRecordings(workingDir) {
11272
11372
  }
11273
11373
 
11274
11374
  // cli/pipeline/single-repo.ts
11375
+ init_cli_ui();
11275
11376
  async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11276
11377
  const specProviderName = opts.provider || config2.provider || "gemini";
11277
11378
  const specModelName = opts.model || config2.model || DEFAULT_MODELS[specProviderName];
@@ -11334,7 +11435,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11334
11435
  console.log(import_chalk25.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
11335
11436
  }
11336
11437
  } else {
11337
- console.log(import_chalk25.default.yellow(" Constitution : not found \u2014 auto-generating..."));
11438
+ const constitutionSpinner = startSpinner("Constitution not found \u2014 auto-generating...");
11338
11439
  try {
11339
11440
  const constitutionGen = new ConstitutionGenerator(
11340
11441
  createProvider(specProviderName, specApiKey, specModelName)
@@ -11342,9 +11443,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11342
11443
  const constitutionContent = await constitutionGen.generate(currentDir);
11343
11444
  await constitutionGen.saveConstitution(currentDir, constitutionContent);
11344
11445
  context.constitution = constitutionContent;
11345
- console.log(import_chalk25.default.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
11446
+ constitutionSpinner.succeed("Constitution generated and saved (.ai-spec-constitution.md)");
11346
11447
  } catch (err) {
11347
- console.log(import_chalk25.default.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
11448
+ constitutionSpinner.fail(`Constitution auto-generation failed (${err.message}), continuing without it.`);
11348
11449
  }
11349
11450
  }
11350
11451
  let architectureDecision;
@@ -11375,17 +11476,18 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11375
11476
  let initialSpec;
11376
11477
  let initialTasks = [];
11377
11478
  runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
11479
+ const specSpinner = startStage("spec_gen", `Generating spec with ${specProviderName}/${specModelName}...`);
11378
11480
  try {
11379
11481
  if (opts.skipTasks) {
11380
11482
  const { SpecGenerator: SpecGenerator2 } = await Promise.resolve().then(() => (init_spec_generator(), spec_generator_exports));
11381
11483
  const generator = new SpecGenerator2(specProvider);
11382
11484
  initialSpec = await generator.generateSpec(idea, context, architectureDecision);
11383
- console.log(import_chalk25.default.green(" \u2714 Spec generated."));
11485
+ specSpinner.succeed("Spec generated.");
11384
11486
  } else {
11385
11487
  const result = await generateSpecWithTasks(specProvider, idea, context, architectureDecision);
11386
11488
  initialSpec = result.spec;
11387
11489
  initialTasks = result.tasks;
11388
- console.log(import_chalk25.default.green(` \u2714 Spec generated.`));
11490
+ specSpinner.succeed("Spec generated.");
11389
11491
  if (initialTasks.length > 0) {
11390
11492
  console.log(import_chalk25.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
11391
11493
  } else {
@@ -11394,8 +11496,8 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11394
11496
  }
11395
11497
  runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
11396
11498
  } catch (err) {
11499
+ specSpinner.fail(`Spec generation failed: ${err.message}`);
11397
11500
  runLogger.stageFail("spec_gen", err.message);
11398
- console.error(import_chalk25.default.red(" \u2718 Spec generation failed:"), err);
11399
11501
  process.exit(1);
11400
11502
  }
11401
11503
  let finalSpec;
@@ -11417,7 +11519,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11417
11519
  console.log(import_chalk25.default.blue("\n[3.4/6] Spec quality assessment..."));
11418
11520
  }
11419
11521
  runLogger.stageStart("spec_assess");
11522
+ const assessSpinner = startStage("spec_assess", "Evaluating spec quality...");
11420
11523
  const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
11524
+ assessSpinner.stop();
11421
11525
  if (assessment) {
11422
11526
  runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
11423
11527
  if (!opts.auto) printSpecAssessment(assessment);
@@ -11509,21 +11613,24 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11509
11613
  console.log(import_chalk25.default.blue("\n[DSL] Extracting structured DSL from spec..."));
11510
11614
  console.log(import_chalk25.default.gray(` Provider: ${specProviderName}/${specModelName}`));
11511
11615
  runLogger.stageStart("dsl_extract");
11616
+ const dslSpinner = startStage("dsl_extract", "Extracting DSL from spec...");
11512
11617
  try {
11513
11618
  const isFrontend = isFrontendDeps(context.dependencies);
11514
- if (isFrontend) console.log(import_chalk25.default.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
11619
+ if (isFrontend) {
11620
+ dslSpinner.update("\u{1F517} Extracting DSL (frontend ComponentSpec mode)...");
11621
+ }
11515
11622
  const dslExtractor = new DslExtractor(specProvider);
11516
11623
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
11517
11624
  if (extractedDsl) {
11518
11625
  runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
11519
- console.log(import_chalk25.default.green(" \u2714 DSL extracted and validated."));
11626
+ dslSpinner.succeed("DSL extracted and validated.");
11520
11627
  } else {
11521
11628
  runLogger.stageEnd("dsl_extract", { skipped: true });
11522
- console.log(import_chalk25.default.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
11629
+ dslSpinner.fail("DSL skipped \u2014 codegen will use Spec + Tasks only.");
11523
11630
  }
11524
11631
  } catch (err) {
11525
11632
  runLogger.stageFail("dsl_extract", err.message);
11526
- console.log(import_chalk25.default.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
11633
+ dslSpinner.fail(`DSL extraction error: ${err.message} \u2014 continuing without DSL.`);
11527
11634
  }
11528
11635
  }
11529
11636
  if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
@@ -11698,6 +11805,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11698
11805
  if (!opts.skipReview) {
11699
11806
  console.log(import_chalk25.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
11700
11807
  runLogger.stageStart("review");
11808
+ const reviewSpinner = startStage("review", "Running 3-pass code review...");
11701
11809
  const reviewer = new CodeReviewer(specProvider, workingDir);
11702
11810
  const savedSpec = await fs25.readFile(specFile, "utf-8");
11703
11811
  if (codegenMode === "api" && generatedFiles.length > 0) {
@@ -11705,6 +11813,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11705
11813
  } else {
11706
11814
  reviewResult = await reviewer.reviewCode(savedSpec, specFile);
11707
11815
  }
11816
+ reviewSpinner.succeed("Code review complete.");
11708
11817
  runLogger.stageEnd("review");
11709
11818
  const complianceScore = extractComplianceScore(reviewResult);
11710
11819
  const missingCount = extractMissingCount(reviewResult);