ai-spec-dev 0.1.0 → 0.17.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 (60) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/README.md +1215 -146
  3. package/RELEASE_LOG.md +1489 -0
  4. package/cli/index.ts +1981 -0
  5. package/cli/welcome.ts +151 -0
  6. package/core/code-generator.ts +757 -0
  7. package/core/combined-generator.ts +63 -0
  8. package/core/constitution-consolidator.ts +141 -0
  9. package/core/constitution-generator.ts +89 -0
  10. package/core/context-loader.ts +453 -0
  11. package/core/contract-bridge.ts +217 -0
  12. package/core/dsl-extractor.ts +337 -0
  13. package/core/dsl-types.ts +166 -0
  14. package/core/dsl-validator.ts +450 -0
  15. package/core/error-feedback.ts +354 -0
  16. package/core/frontend-context-loader.ts +602 -0
  17. package/core/global-constitution.ts +88 -0
  18. package/core/key-store.ts +49 -0
  19. package/core/knowledge-memory.ts +171 -0
  20. package/core/mock-server-generator.ts +571 -0
  21. package/core/openapi-exporter.ts +361 -0
  22. package/core/requirement-decomposer.ts +198 -0
  23. package/core/reviewer.ts +259 -0
  24. package/core/spec-assessor.ts +99 -0
  25. package/core/spec-generator.ts +428 -0
  26. package/core/spec-refiner.ts +89 -0
  27. package/core/spec-updater.ts +227 -0
  28. package/core/spec-versioning.ts +213 -0
  29. package/core/task-generator.ts +174 -0
  30. package/core/test-generator.ts +273 -0
  31. package/core/workspace-loader.ts +256 -0
  32. package/dist/cli/index.js +6717 -672
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/index.mjs +6717 -670
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/index.d.mts +147 -27
  37. package/dist/index.d.ts +147 -27
  38. package/dist/index.js +2337 -286
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +2329 -285
  41. package/dist/index.mjs.map +1 -1
  42. package/git/worktree.ts +109 -0
  43. package/index.ts +9 -0
  44. package/package.json +4 -28
  45. package/prompts/codegen.prompt.ts +259 -0
  46. package/prompts/consolidate.prompt.ts +73 -0
  47. package/prompts/constitution.prompt.ts +63 -0
  48. package/prompts/decompose.prompt.ts +168 -0
  49. package/prompts/dsl.prompt.ts +203 -0
  50. package/prompts/frontend-spec.prompt.ts +191 -0
  51. package/prompts/global-constitution.prompt.ts +61 -0
  52. package/prompts/spec-assess.prompt.ts +53 -0
  53. package/prompts/spec.prompt.ts +102 -0
  54. package/prompts/tasks.prompt.ts +35 -0
  55. package/prompts/testgen.prompt.ts +84 -0
  56. package/prompts/update.prompt.ts +131 -0
  57. package/purpose.docx +0 -0
  58. package/purpose.md +444 -0
  59. package/tsconfig.json +14 -0
  60. package/tsup.config.ts +10 -0
package/dist/index.js CHANGED
@@ -38,19 +38,26 @@ __export(index_exports, {
38
38
  ContextLoader: () => ContextLoader,
39
39
  DEFAULT_MODELS: () => DEFAULT_MODELS,
40
40
  ENV_KEY_MAP: () => ENV_KEY_MAP,
41
+ FRONTEND_FRAMEWORKS: () => FRONTEND_FRAMEWORKS,
41
42
  GeminiProvider: () => GeminiProvider,
42
43
  GitWorktreeManager: () => GitWorktreeManager,
44
+ MiMoProvider: () => MiMoProvider,
43
45
  OpenAICompatibleProvider: () => OpenAICompatibleProvider,
44
46
  PROVIDER_CATALOG: () => PROVIDER_CATALOG,
45
47
  SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
46
48
  SpecGenerator: () => SpecGenerator,
47
49
  SpecRefiner: () => SpecRefiner,
48
50
  TaskGenerator: () => TaskGenerator,
51
+ buildTaskPrompt: () => buildTaskPrompt,
49
52
  createProvider: () => createProvider,
53
+ generateSpecWithTasks: () => generateSpecWithTasks,
54
+ isFrontendDeps: () => isFrontendDeps,
50
55
  loadConstitution: () => loadConstitution,
51
56
  loadTasksForSpec: () => loadTasksForSpec,
52
57
  printConstitutionHint: () => printConstitutionHint,
53
- printTasks: () => printTasks
58
+ printTaskProgress: () => printTaskProgress,
59
+ printTasks: () => printTasks,
60
+ updateTaskStatus: () => updateTaskStatus
54
61
  });
55
62
  module.exports = __toCommonJS(index_exports);
56
63
 
@@ -58,6 +65,7 @@ module.exports = __toCommonJS(index_exports);
58
65
  var import_generative_ai = require("@google/generative-ai");
59
66
  var import_sdk = __toESM(require("@anthropic-ai/sdk"));
60
67
  var import_openai = __toESM(require("openai"));
68
+ var import_axios = __toESM(require("axios"));
61
69
  var import_undici = require("undici");
62
70
 
63
71
  // prompts/spec.prompt.ts
@@ -172,12 +180,21 @@ function geminiRequestOptions() {
172
180
  }
173
181
  var PROVIDER_CATALOG = {
174
182
  // ── International ──────────────────────────────────────────────────────────
183
+ mimo: {
184
+ displayName: "MiMo (Xiaomi)",
185
+ description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
186
+ models: ["mimo-v2-pro"],
187
+ envKey: "MIMO_API_KEY"
188
+ // baseURL not used — MiMo has a dedicated provider class
189
+ },
175
190
  gemini: {
176
191
  displayName: "Google Gemini",
177
- description: "Google AI Studio \u2014 Gemini 2.5 / 1.5 series",
192
+ description: "Google AI Studio \u2014 Gemini 2.5 / 2.0 series",
178
193
  models: [
179
- "gemini-2.5-flash",
180
194
  "gemini-2.5-pro",
195
+ "gemini-2.5-flash",
196
+ "gemini-2.0-flash",
197
+ "gemini-2.0-flash-lite",
181
198
  "gemini-1.5-pro",
182
199
  "gemini-1.5-flash"
183
200
  ],
@@ -185,61 +202,103 @@ var PROVIDER_CATALOG = {
185
202
  },
186
203
  claude: {
187
204
  displayName: "Anthropic Claude",
188
- description: "Anthropic \u2014 Claude 4.x series",
205
+ description: "Anthropic \u2014 Claude 4.x / 3.7 series",
189
206
  models: [
190
- "claude-sonnet-4-6",
191
207
  "claude-opus-4-6",
192
- "claude-haiku-4-5"
208
+ "claude-sonnet-4-6",
209
+ "claude-haiku-4-5",
210
+ "claude-3-7-sonnet-20250219"
193
211
  ],
194
212
  envKey: "ANTHROPIC_API_KEY"
195
213
  },
196
214
  openai: {
197
215
  displayName: "OpenAI",
198
- description: "OpenAI \u2014 GPT-4o / o1 series",
199
- models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1-mini"],
216
+ description: "OpenAI \u2014 o3 / GPT-4o series",
217
+ models: [
218
+ "o3",
219
+ "o3-mini",
220
+ "o1",
221
+ "o1-mini",
222
+ "gpt-4o",
223
+ "gpt-4o-mini"
224
+ ],
200
225
  envKey: "OPENAI_API_KEY",
201
226
  baseURL: "https://api.openai.com/v1"
202
227
  },
228
+ deepseek: {
229
+ displayName: "DeepSeek",
230
+ description: "DeepSeek \u2014 V3 (chat) / R1 (reasoning)",
231
+ models: [
232
+ "deepseek-chat",
233
+ // DeepSeek-V3
234
+ "deepseek-reasoner"
235
+ // DeepSeek-R1
236
+ ],
237
+ envKey: "DEEPSEEK_API_KEY",
238
+ baseURL: "https://api.deepseek.com/v1"
239
+ },
203
240
  // ── Chinese Models (OpenAI-compatible) ────────────────────────────────────
204
241
  qwen: {
205
242
  displayName: "\u901A\u4E49\u5343\u95EE (Qwen)",
206
- description: "\u963F\u91CC\u4E91\u767E\u70BC DashScope \u2014 qwen-max / plus / turbo",
243
+ description: "\u963F\u91CC\u4E91\u767E\u70BC \u2014 Qwen3 / Qwen2.5 series",
207
244
  models: [
245
+ "qwen3-235b-a22b",
246
+ // Qwen3 MoE flagship (supports thinking mode)
247
+ "qwen3-72b",
248
+ "qwen3-32b",
249
+ "qwen3-8b",
208
250
  "qwen-max",
209
251
  "qwen-max-latest",
210
252
  "qwen-plus",
211
- "qwen-plus-latest",
212
- "qwen-turbo",
213
253
  "qwen-long"
214
254
  ],
215
255
  envKey: "DASHSCOPE_API_KEY",
216
- baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
256
+ baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
257
+ // Qwen3 models enable thinking (CoT) by default, which pollutes structured outputs.
258
+ // Disable it so JSON/Markdown responses stay clean.
259
+ extraBody: { enable_thinking: false }
260
+ },
261
+ glm: {
262
+ displayName: "\u667A\u8C31 GLM (Zhipu AI)",
263
+ description: "\u667A\u8C31 AI \u2014 GLM-5 / GLM-4 series + Z1 reasoning",
264
+ models: [
265
+ "glm-5",
266
+ // GLM-5 flagship (如不可用请确认最新 model ID)
267
+ "glm-5-flash",
268
+ "glm-z1",
269
+ // GLM-Z1 reasoning model
270
+ "glm-z1-flash",
271
+ "glm-4-plus",
272
+ "glm-4-flash",
273
+ "glm-4-long"
274
+ ],
275
+ envKey: "ZHIPU_API_KEY",
276
+ baseURL: "https://open.bigmodel.cn/api/paas/v4/"
217
277
  },
218
278
  minimax: {
219
279
  displayName: "MiniMax",
220
- description: "MiniMax AI \u2014 MiniMax-Text-01 / abab series",
280
+ description: "MiniMax AI \u2014 MiniMax-Text-2.7 / Text-01 series",
221
281
  models: [
282
+ "MiniMax-Text-2.7",
283
+ // MiniMax 最新旗舰 (如不可用请确认最新 model ID)
222
284
  "MiniMax-Text-01",
223
- "abab6.5s-chat",
224
- "abab6.5g-chat",
225
- "abab5.5-chat"
285
+ "abab6.5s-chat"
226
286
  ],
227
287
  envKey: "MINIMAX_API_KEY",
228
288
  baseURL: "https://api.minimax.chat/v1"
229
289
  },
230
- glm: {
231
- displayName: "\u667A\u8C31 GLM (Zhipu AI)",
232
- description: "\u667A\u8C31 AI \u2014 GLM-4 plus / flash / air series",
290
+ doubao: {
291
+ displayName: "\u8C46\u5305 Doubao (ByteDance)",
292
+ description: "\u706B\u5C71\u5F15\u64CE Ark \u2014 Doubao Pro/Lite series",
233
293
  models: [
234
- "glm-4-plus",
235
- "glm-4",
236
- "glm-4-flash",
237
- "glm-4-air",
238
- "glm-4-airx",
239
- "glm-4-long"
294
+ "doubao-pro-256k",
295
+ "doubao-pro-128k",
296
+ "doubao-pro-32k",
297
+ "doubao-lite-128k",
298
+ "doubao-lite-32k"
240
299
  ],
241
- envKey: "ZHIPU_API_KEY",
242
- baseURL: "https://open.bigmodel.cn/api/paas/v4/"
300
+ envKey: "ARK_API_KEY",
301
+ baseURL: "https://ark.cn-beijing.volces.com/api/v3"
243
302
  }
244
303
  };
245
304
  var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_CATALOG);
@@ -290,9 +349,13 @@ var OpenAICompatibleProvider = class {
290
349
  client;
291
350
  providerName;
292
351
  modelName;
293
- constructor(providerName, apiKey, modelName, baseURL) {
352
+ systemRole;
353
+ extraBody;
354
+ constructor(providerName, apiKey, modelName, baseURL, systemRole = "system", extraBody) {
294
355
  this.providerName = providerName;
295
356
  this.modelName = modelName;
357
+ this.systemRole = systemRole;
358
+ this.extraBody = extraBody;
296
359
  this.client = new import_openai.default({
297
360
  apiKey,
298
361
  ...baseURL ? { baseURL } : {}
@@ -301,16 +364,57 @@ var OpenAICompatibleProvider = class {
301
364
  async generate(prompt, systemInstruction) {
302
365
  const messages = [];
303
366
  if (systemInstruction) {
304
- messages.push({ role: "system", content: systemInstruction });
367
+ const isOSeries = /^o[13]/.test(this.modelName);
368
+ const role = isOSeries ? "developer" : this.systemRole;
369
+ messages.push({ role, content: systemInstruction });
305
370
  }
306
371
  messages.push({ role: "user", content: prompt });
307
372
  const completion = await this.client.chat.completions.create({
308
373
  model: this.modelName,
309
- messages
374
+ messages,
375
+ ...this.extraBody ? { extra_body: this.extraBody } : {}
310
376
  });
311
377
  return completion.choices[0].message.content ?? "";
312
378
  }
313
379
  };
380
+ var MiMoProvider = class {
381
+ providerName = "mimo";
382
+ modelName;
383
+ apiKey;
384
+ baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
385
+ constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
386
+ this.apiKey = apiKey;
387
+ this.modelName = modelName;
388
+ }
389
+ async generate(prompt, systemInstruction) {
390
+ const body = {
391
+ model: this.modelName,
392
+ max_tokens: 16384,
393
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
394
+ top_p: 0.95,
395
+ stream: false,
396
+ temperature: 1,
397
+ stop_sequences: null
398
+ };
399
+ if (systemInstruction) {
400
+ body.system = systemInstruction;
401
+ }
402
+ const response = await import_axios.default.post(this.baseUrl, body, {
403
+ headers: {
404
+ "api-key": this.apiKey,
405
+ "Content-Type": "application/json"
406
+ }
407
+ });
408
+ const data = response.data;
409
+ const blocks = data?.content ?? [];
410
+ const textBlock = blocks.find((b) => b.type === "text");
411
+ if (textBlock?.text) return textBlock.text;
412
+ if (data?.stop_reason === "max_tokens") {
413
+ throw new Error(`MiMo response truncated (max_tokens reached). The prompt may be too long. Try a shorter spec or switch to a model with larger context.`);
414
+ }
415
+ throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
416
+ }
417
+ };
314
418
  function createProvider(providerName, apiKey, modelName) {
315
419
  const meta = PROVIDER_CATALOG[providerName];
316
420
  if (!meta) {
@@ -324,9 +428,18 @@ function createProvider(providerName, apiKey, modelName) {
324
428
  return new GeminiProvider(apiKey, model);
325
429
  case "claude":
326
430
  return new ClaudeProvider(apiKey, model);
327
- // All OpenAI-compatible providers (openai, qwen, minimax, glm)
431
+ case "mimo":
432
+ return new MiMoProvider(apiKey, model);
433
+ // All OpenAI-compatible providers: openai, deepseek, qwen, glm, minimax, doubao
328
434
  default:
329
- return new OpenAICompatibleProvider(providerName, apiKey, model, meta.baseURL);
435
+ return new OpenAICompatibleProvider(
436
+ providerName,
437
+ apiKey,
438
+ model,
439
+ meta.baseURL,
440
+ meta.systemRole ?? "system",
441
+ meta.extraBody
442
+ );
330
443
  }
331
444
  }
332
445
  var SpecGenerator = class {
@@ -376,8 +489,8 @@ ${context.schema.slice(0, 3e3)}`);
376
489
  };
377
490
 
378
491
  // core/context-loader.ts
379
- var fs2 = __toESM(require("fs-extra"));
380
- var path = __toESM(require("path"));
492
+ var fs3 = __toESM(require("fs-extra"));
493
+ var path2 = __toESM(require("path"));
381
494
 
382
495
  // node_modules/glob/dist/esm/index.min.js
383
496
  var import_node_url = require("url");
@@ -3363,7 +3476,48 @@ var Ui = Object.assign(ts, { stream: Bt, iterate: Ut });
3363
3476
  var Ze = Object.assign(Je, { glob: Je, globSync: ts, sync: Ui, globStream: Qe, stream: Ii, globStreamSync: Bt, streamSync: ji, globIterate: es, iterate: Bi, globIterateSync: Ut, iterateSync: zi, Glob: I, hasMagic: le, escape: tt, unescape: W });
3364
3477
  Ze.glob = Ze;
3365
3478
 
3479
+ // core/global-constitution.ts
3480
+ var fs2 = __toESM(require("fs-extra"));
3481
+ var path = __toESM(require("path"));
3482
+ var os2 = __toESM(require("os"));
3483
+ var GLOBAL_CONSTITUTION_FILE = ".ai-spec-global-constitution.md";
3484
+ var SEARCH_ROOTS = [
3485
+ // Workspace root is injected at runtime — see loadGlobalConstitution()
3486
+ os2.homedir()
3487
+ ];
3488
+ async function loadGlobalConstitution(extraRoots = []) {
3489
+ const roots = [...extraRoots, ...SEARCH_ROOTS];
3490
+ for (const root of roots) {
3491
+ const filePath = path.join(root, GLOBAL_CONSTITUTION_FILE);
3492
+ if (await fs2.pathExists(filePath)) {
3493
+ const content = await fs2.readFile(filePath, "utf-8");
3494
+ return { content, source: filePath };
3495
+ }
3496
+ }
3497
+ return null;
3498
+ }
3499
+ function mergeConstitutions(globalContent, projectContent) {
3500
+ const parts = [
3501
+ "<!-- BEGIN GLOBAL CONSTITUTION (team baseline \u2014 lower priority) -->",
3502
+ globalContent.trim(),
3503
+ "<!-- END GLOBAL CONSTITUTION -->"
3504
+ ];
3505
+ if (projectContent && projectContent.trim()) {
3506
+ parts.push(
3507
+ "",
3508
+ "<!-- BEGIN PROJECT CONSTITUTION (project-specific \u2014 HIGHER priority, overrides global) -->",
3509
+ projectContent.trim(),
3510
+ "<!-- END PROJECT CONSTITUTION -->"
3511
+ );
3512
+ }
3513
+ return parts.join("\n");
3514
+ }
3515
+
3366
3516
  // core/context-loader.ts
3517
+ var FRONTEND_FRAMEWORKS = ["react", "vue", "next", "nuxt", "react-native", "expo", "svelte", "solid-js", "qwik"];
3518
+ function isFrontendDeps(deps) {
3519
+ return deps.some((d) => FRONTEND_FRAMEWORKS.includes(d));
3520
+ }
3367
3521
  var STACK_MAP = {
3368
3522
  react: "React",
3369
3523
  vue: "Vue",
@@ -3397,21 +3551,150 @@ var ContextLoader = class {
3397
3551
  apiStructure: []
3398
3552
  };
3399
3553
  try {
3400
- await this.loadPackageJson(context);
3401
- await this.loadPrismaSchema(context);
3554
+ const isPhp = await fs3.pathExists(path2.join(this.projectRoot, "composer.json"));
3555
+ const isJava = await fs3.pathExists(path2.join(this.projectRoot, "pom.xml")) || await fs3.pathExists(path2.join(this.projectRoot, "build.gradle")) || await fs3.pathExists(path2.join(this.projectRoot, "build.gradle.kts"));
3556
+ if (isPhp) {
3557
+ await this.loadComposerJson(context);
3558
+ await this.loadPhpRoutes(context);
3559
+ } else if (isJava) {
3560
+ await this.loadMavenOrGradle(context);
3561
+ await this.loadJavaApiStructure(context);
3562
+ } else {
3563
+ await this.loadPackageJson(context);
3564
+ await this.loadPrismaSchema(context);
3565
+ }
3402
3566
  await this.loadFileStructure(context);
3403
3567
  await this.loadApiStructure(context);
3404
3568
  await this.loadConstitution(context);
3405
3569
  await this.loadErrorPatterns(context);
3570
+ await this.loadSharedConfigFiles(context);
3406
3571
  } catch (e) {
3407
3572
  console.warn("Warning: Could not load full project context.", e);
3408
3573
  }
3409
3574
  return context;
3410
3575
  }
3576
+ /** Load PHP project context from composer.json */
3577
+ async loadComposerJson(context) {
3578
+ const composerPath = path2.join(this.projectRoot, "composer.json");
3579
+ let composer = {};
3580
+ try {
3581
+ composer = await fs3.readJson(composerPath);
3582
+ } catch {
3583
+ return;
3584
+ }
3585
+ const require2 = composer.require ?? {};
3586
+ const requireDev = composer["require-dev"] ?? {};
3587
+ context.dependencies = [...Object.keys(require2), ...Object.keys(requireDev)];
3588
+ const stack = /* @__PURE__ */ new Set();
3589
+ stack.add("PHP");
3590
+ if (require2["laravel/lumen-framework"]) stack.add("Lumen");
3591
+ if (require2["laravel/framework"]) stack.add("Laravel");
3592
+ if (require2["symfony/framework-bundle"]) stack.add("Symfony");
3593
+ if (require2["slim/slim"]) stack.add("Slim");
3594
+ if (require2["illuminate/database"] || require2["laravel/lumen-framework"]) stack.add("Eloquent ORM");
3595
+ if (require2["doctrine/orm"]) stack.add("Doctrine ORM");
3596
+ if (require2["tymon/jwt-auth"]) stack.add("JWT Auth");
3597
+ if (require2["league/fractal"] || require2["spatie/laravel-fractal"]) stack.add("Fractal (Transformers)");
3598
+ const phpVersion = require2["php"];
3599
+ if (phpVersion) stack.add(`PHP ${phpVersion}`);
3600
+ context.techStack = Array.from(stack);
3601
+ }
3602
+ /**
3603
+ * Load PHP route files (routes/api.php, routes/web.php) as routeSummary.
3604
+ * Lumen uses these files to register API endpoints.
3605
+ */
3606
+ async loadPhpRoutes(context) {
3607
+ const routeFiles = ["routes/api.php", "routes/web.php"];
3608
+ const parts = [];
3609
+ for (const rel of routeFiles) {
3610
+ const fullPath = path2.join(this.projectRoot, rel);
3611
+ if (!await fs3.pathExists(fullPath)) continue;
3612
+ try {
3613
+ const content = await fs3.readFile(fullPath, "utf-8");
3614
+ parts.push(`// ${rel}
3615
+ ${content.slice(0, 1500)}`);
3616
+ } catch {
3617
+ }
3618
+ }
3619
+ if (parts.length > 0) {
3620
+ context.routeSummary = parts.join("\n\n");
3621
+ }
3622
+ const controllerFiles = await Ze("app/Http/Controllers/**/*.php", {
3623
+ cwd: this.projectRoot,
3624
+ ignore: ["vendor/**"]
3625
+ });
3626
+ context.apiStructure = controllerFiles.slice(0, 20);
3627
+ }
3628
+ /** Load Java project context from pom.xml or build.gradle */
3629
+ async loadMavenOrGradle(context) {
3630
+ const pomPath = path2.join(this.projectRoot, "pom.xml");
3631
+ const gradlePath = path2.join(this.projectRoot, "build.gradle");
3632
+ const gradleKtsPath = path2.join(this.projectRoot, "build.gradle.kts");
3633
+ const stack = /* @__PURE__ */ new Set(["Java"]);
3634
+ const deps = [];
3635
+ if (await fs3.pathExists(pomPath)) {
3636
+ try {
3637
+ const xml = await fs3.readFile(pomPath, "utf-8");
3638
+ const artifactIds = [...xml.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)].map((m) => m[1].trim()).filter((id, i) => i > 0);
3639
+ deps.push(...artifactIds);
3640
+ const javaVerMatch = xml.match(/<maven\.compiler\.source>(\d+)<\/maven\.compiler\.source>/);
3641
+ if (javaVerMatch) stack.add(`Java ${javaVerMatch[1]}`);
3642
+ if (deps.some((d) => d.includes("spring-boot"))) stack.add("Spring Boot");
3643
+ if (deps.some((d) => d.includes("spring-web") || d.includes("spring-webmvc"))) stack.add("Spring MVC");
3644
+ if (deps.some((d) => d.includes("mybatis"))) stack.add("MyBatis");
3645
+ if (deps.some((d) => d.includes("hibernate") || d.includes("spring-data-jpa"))) stack.add("JPA/Hibernate");
3646
+ if (deps.some((d) => d.includes("dubbo"))) stack.add("Dubbo");
3647
+ if (deps.some((d) => d.includes("rocketmq"))) stack.add("RocketMQ");
3648
+ if (deps.some((d) => d.includes("kafka"))) stack.add("Kafka");
3649
+ if (deps.some((d) => d.includes("redis"))) stack.add("Redis");
3650
+ if (deps.some((d) => d.includes("lombok"))) stack.add("Lombok");
3651
+ if (deps.some((d) => d.includes("feign") || d.includes("openfeign"))) stack.add("OpenFeign");
3652
+ if (deps.some((d) => d.includes("nacos"))) stack.add("Nacos");
3653
+ if (deps.some((d) => d.includes("sentinel"))) stack.add("Sentinel");
3654
+ } catch {
3655
+ }
3656
+ } else {
3657
+ const gradleFile = await fs3.pathExists(gradleKtsPath) ? gradleKtsPath : gradlePath;
3658
+ try {
3659
+ const content = await fs3.readFile(gradleFile, "utf-8");
3660
+ const depMatches = [...content.matchAll(/['"]([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]+):[^'"]+['"]/g)];
3661
+ deps.push(...depMatches.map((m) => m[2]));
3662
+ if (deps.some((d) => d.includes("spring-boot"))) stack.add("Spring Boot");
3663
+ if (deps.some((d) => d.includes("mybatis"))) stack.add("MyBatis");
3664
+ } catch {
3665
+ }
3666
+ }
3667
+ context.techStack = Array.from(stack);
3668
+ context.dependencies = [...new Set(deps)];
3669
+ }
3670
+ /** Scan Java controller files for API structure */
3671
+ async loadJavaApiStructure(context) {
3672
+ const controllerFiles = await Ze("**/src/main/java/**/*Controller.java", {
3673
+ cwd: this.projectRoot,
3674
+ ignore: ["**/target/**"]
3675
+ });
3676
+ context.apiStructure = controllerFiles.slice(0, 30);
3677
+ const propFiles = await Ze("**/src/main/resources/application.{properties,yml,yaml}", {
3678
+ cwd: this.projectRoot,
3679
+ ignore: ["**/target/**"]
3680
+ });
3681
+ if (propFiles.length > 0 && !context.routeSummary) {
3682
+ const parts = [];
3683
+ for (const f of propFiles.slice(0, 2)) {
3684
+ try {
3685
+ const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
3686
+ parts.push(`// ${f}
3687
+ ${content.slice(0, 800)}`);
3688
+ } catch {
3689
+ }
3690
+ }
3691
+ if (parts.length > 0) context.routeSummary = parts.join("\n\n");
3692
+ }
3693
+ }
3411
3694
  async loadPackageJson(context) {
3412
- const pkgPath = path.join(this.projectRoot, "package.json");
3413
- if (!await fs2.pathExists(pkgPath)) return;
3414
- const pkg = await fs2.readJson(pkgPath);
3695
+ const pkgPath = path2.join(this.projectRoot, "package.json");
3696
+ if (!await fs3.pathExists(pkgPath)) return;
3697
+ const pkg = await fs3.readJson(pkgPath);
3415
3698
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
3416
3699
  context.dependencies = Object.keys(allDeps);
3417
3700
  const detectedStack = /* @__PURE__ */ new Set();
@@ -3423,9 +3706,9 @@ var ContextLoader = class {
3423
3706
  context.techStack = Array.from(detectedStack);
3424
3707
  }
3425
3708
  async loadPrismaSchema(context) {
3426
- const schemaPath = path.join(this.projectRoot, "prisma", "schema.prisma");
3427
- if (await fs2.pathExists(schemaPath)) {
3428
- context.schema = await fs2.readFile(schemaPath, "utf-8");
3709
+ const schemaPath = path2.join(this.projectRoot, "prisma", "schema.prisma");
3710
+ if (await fs3.pathExists(schemaPath)) {
3711
+ context.schema = await fs3.readFile(schemaPath, "utf-8");
3429
3712
  }
3430
3713
  }
3431
3714
  async loadFileStructure(context) {
@@ -3433,21 +3716,30 @@ var ContextLoader = class {
3433
3716
  cwd: this.projectRoot,
3434
3717
  ignore: [
3435
3718
  "node_modules/**",
3719
+ "vendor/**",
3436
3720
  "dist/**",
3721
+ "build/**",
3437
3722
  ".git/**",
3438
3723
  "coverage/**",
3439
3724
  "*.lock",
3440
- ".DS_Store"
3725
+ ".DS_Store",
3726
+ "**/*.min.js",
3727
+ "**/*.map"
3441
3728
  ],
3442
3729
  nodir: false,
3443
- maxDepth: 3
3730
+ maxDepth: 5
3444
3731
  });
3445
- context.fileStructure = files.slice(0, 60);
3732
+ context.fileStructure = files.slice(0, 120);
3446
3733
  }
3447
3734
  async loadConstitution(context) {
3448
- const filePath = path.join(this.projectRoot, ".ai-spec-constitution.md");
3449
- if (await fs2.pathExists(filePath)) {
3450
- context.constitution = await fs2.readFile(filePath, "utf-8");
3735
+ const projectFile = path2.join(this.projectRoot, ".ai-spec-constitution.md");
3736
+ const projectConstitution = await fs3.pathExists(projectFile) ? await fs3.readFile(projectFile, "utf-8") : void 0;
3737
+ const workspaceRoot = path2.dirname(this.projectRoot);
3738
+ const globalResult = await loadGlobalConstitution([workspaceRoot]);
3739
+ if (globalResult) {
3740
+ context.constitution = mergeConstitutions(globalResult.content, projectConstitution);
3741
+ } else if (projectConstitution) {
3742
+ context.constitution = projectConstitution;
3451
3743
  }
3452
3744
  }
3453
3745
  async loadErrorPatterns(context) {
@@ -3458,12 +3750,16 @@ var ContextLoader = class {
3458
3750
  const middlewareErrors = await Ze("src/**/middleware/**/{error,notFound}.{ts,js}", {
3459
3751
  cwd: this.projectRoot
3460
3752
  });
3461
- const allErrorFiles = [.../* @__PURE__ */ new Set([...errorFiles, ...middlewareErrors])].slice(0, 3);
3753
+ const phpErrorFiles = await Ze(
3754
+ "app/Exceptions/{Handler,ErrorHandler}.php",
3755
+ { cwd: this.projectRoot, ignore: ["vendor/**"] }
3756
+ );
3757
+ const allErrorFiles = [.../* @__PURE__ */ new Set([...errorFiles, ...middlewareErrors, ...phpErrorFiles])].slice(0, 3);
3462
3758
  if (allErrorFiles.length === 0) return;
3463
3759
  const parts = [];
3464
3760
  for (const f of allErrorFiles) {
3465
3761
  try {
3466
- const content = await fs2.readFile(path.join(this.projectRoot, f), "utf-8");
3762
+ const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
3467
3763
  parts.push(`// ${f}
3468
3764
  ${content.slice(0, 800)}`);
3469
3765
  } catch {
@@ -3473,6 +3769,70 @@ ${content.slice(0, 800)}`);
3473
3769
  context.errorPatterns = parts.join("\n\n");
3474
3770
  }
3475
3771
  }
3772
+ /**
3773
+ * Scan for "singleton" config files that should never be duplicated.
3774
+ * These are append-only files: i18n bundles, constants, enums, config indices.
3775
+ */
3776
+ async loadSharedConfigFiles(context) {
3777
+ const patterns = [
3778
+ // i18n / locales
3779
+ { glob: "src/locales/**/*.{json,ts,js}", category: "i18n" },
3780
+ { glob: "src/i18n/**/*.{json,ts,js}", category: "i18n" },
3781
+ { glob: "locales/**/*.{json,ts,js}", category: "i18n" },
3782
+ { glob: "public/locales/**/*.{json,ts,js}", category: "i18n" },
3783
+ // constants / enums
3784
+ { glob: "src/constants/**/*.{ts,js}", category: "constants" },
3785
+ { glob: "src/enums/**/*.{ts,js}", category: "enums" },
3786
+ { glob: "src/**/constants.{ts,js}", category: "constants" },
3787
+ { glob: "src/**/enums.{ts,js}", category: "enums" },
3788
+ // config
3789
+ { glob: "src/config/**/*.{ts,js}", category: "config" },
3790
+ // ── Route registration files ────────────────────────────────────────────
3791
+ // Node.js / Express
3792
+ { glob: "src/routes/**/index.{ts,js}", category: "route-index" },
3793
+ { glob: "src/routes/index.{ts,js}", category: "route-index" },
3794
+ // Vue Router — root index and modules pattern
3795
+ { glob: "src/router/index.{ts,js}", category: "route-index" },
3796
+ { glob: "src/router/routes.{ts,js}", category: "route-index" },
3797
+ { glob: "src/router/modules/**/*.{ts,js}", category: "route-index" },
3798
+ // React Router — standalone routes file or App entry
3799
+ { glob: "src/routes.{ts,tsx,js,jsx}", category: "route-index" },
3800
+ { glob: "src/router.{ts,tsx,js,jsx}", category: "route-index" },
3801
+ // PHP (Lumen / Laravel)
3802
+ { glob: "routes/api.php", category: "route-index" },
3803
+ { glob: "routes/web.php", category: "route-index" },
3804
+ // ── Store registration files ────────────────────────────────────────────
3805
+ // Pinia / Vuex index
3806
+ { glob: "src/stores/index.{ts,js}", category: "store-index" },
3807
+ { glob: "src/store/index.{ts,js}", category: "store-index" },
3808
+ { glob: "src/store/modules/index.{ts,js}", category: "store-index" },
3809
+ // Redux root reducer / store setup
3810
+ { glob: "src/store/rootReducer.{ts,js}", category: "store-index" },
3811
+ { glob: "src/store/store.{ts,js}", category: "store-index" },
3812
+ { glob: "src/app/store.{ts,js}", category: "store-index" }
3813
+ ];
3814
+ const seen = /* @__PURE__ */ new Set();
3815
+ const results = [];
3816
+ for (const { glob: pattern, category } of patterns) {
3817
+ const files = await Ze(pattern, {
3818
+ cwd: this.projectRoot,
3819
+ ignore: ["node_modules/**", "dist/**", "**/*.test.*", "**/*.spec.*"]
3820
+ });
3821
+ for (const filePath of files) {
3822
+ if (seen.has(filePath)) continue;
3823
+ seen.add(filePath);
3824
+ try {
3825
+ const content = await fs3.readFile(path2.join(this.projectRoot, filePath), "utf-8");
3826
+ const preview = content.split("\n").slice(0, 120).join("\n");
3827
+ results.push({ path: filePath, preview, category });
3828
+ } catch {
3829
+ }
3830
+ }
3831
+ }
3832
+ if (results.length > 0) {
3833
+ context.sharedConfigFiles = results;
3834
+ }
3835
+ }
3476
3836
  async loadApiStructure(context) {
3477
3837
  const apiFiles = await Ze(
3478
3838
  "src/**/{routes,controllers,api,router,middleware}/**/*.{ts,js}",
@@ -3488,9 +3848,9 @@ ${content.slice(0, 800)}`);
3488
3848
  if (context.apiStructure.length > 0) {
3489
3849
  const summaryParts = [];
3490
3850
  for (const filePath of context.apiStructure.slice(0, 8)) {
3491
- const fullPath = path.join(this.projectRoot, filePath);
3851
+ const fullPath = path2.join(this.projectRoot, filePath);
3492
3852
  try {
3493
- const content = await fs2.readFile(fullPath, "utf-8");
3853
+ const content = await fs3.readFile(fullPath, "utf-8");
3494
3854
  const preview = content.split("\n").slice(0, 60).join("\n");
3495
3855
  summaryParts.push(`\`\`\`
3496
3856
  // ${filePath}
@@ -3508,7 +3868,99 @@ ${preview}
3508
3868
 
3509
3869
  // core/spec-refiner.ts
3510
3870
  var import_prompts = require("@inquirer/prompts");
3871
+ var import_chalk2 = __toESM(require("chalk"));
3872
+
3873
+ // core/spec-versioning.ts
3511
3874
  var import_chalk = __toESM(require("chalk"));
3875
+ var fs4 = __toESM(require("fs-extra"));
3876
+ function computeDiff(oldText, newText) {
3877
+ const oldLines = oldText.split("\n");
3878
+ const newLines = newText.split("\n");
3879
+ const m = oldLines.length;
3880
+ const n7 = newLines.length;
3881
+ const MAX = 800;
3882
+ if (m > MAX || n7 > MAX) {
3883
+ return computeSimpleDiff(oldLines, newLines);
3884
+ }
3885
+ const dp = Array.from({ length: m + 1 }, () => new Array(n7 + 1).fill(0));
3886
+ for (let i2 = m - 1; i2 >= 0; i2--) {
3887
+ for (let j3 = n7 - 1; j3 >= 0; j3--) {
3888
+ if (oldLines[i2] === newLines[j3]) {
3889
+ dp[i2][j3] = dp[i2 + 1][j3 + 1] + 1;
3890
+ } else {
3891
+ dp[i2][j3] = Math.max(dp[i2 + 1][j3], dp[i2][j3 + 1]);
3892
+ }
3893
+ }
3894
+ }
3895
+ const lines = [];
3896
+ let i = 0, j2 = 0, lineNo = 1;
3897
+ while (i < m || j2 < n7) {
3898
+ if (i < m && j2 < n7 && oldLines[i] === newLines[j2]) {
3899
+ lines.push({ type: "unchanged", content: oldLines[i], lineNo: lineNo++ });
3900
+ i++;
3901
+ j2++;
3902
+ } else if (j2 < n7 && (i >= m || dp[i + 1][j2] <= dp[i][j2 + 1])) {
3903
+ lines.push({ type: "added", content: newLines[j2], lineNo: lineNo++ });
3904
+ j2++;
3905
+ } else {
3906
+ lines.push({ type: "removed", content: oldLines[i], lineNo });
3907
+ i++;
3908
+ }
3909
+ }
3910
+ const added = lines.filter((l) => l.type === "added").length;
3911
+ const removed = lines.filter((l) => l.type === "removed").length;
3912
+ const unchanged = lines.filter((l) => l.type === "unchanged").length;
3913
+ return { added, removed, unchanged, lines };
3914
+ }
3915
+ function computeSimpleDiff(oldLines, newLines) {
3916
+ const lines = [
3917
+ ...oldLines.map((c, i) => ({ type: "removed", content: c, lineNo: i + 1 })),
3918
+ ...newLines.map((c, i) => ({ type: "added", content: c, lineNo: i + 1 }))
3919
+ ];
3920
+ return { added: newLines.length, removed: oldLines.length, unchanged: 0, lines };
3921
+ }
3922
+ var CONTEXT_LINES = 3;
3923
+ function printDiff(diff) {
3924
+ if (diff.added === 0 && diff.removed === 0) {
3925
+ console.log(import_chalk.default.gray(" (no changes)"));
3926
+ return;
3927
+ }
3928
+ const { lines } = diff;
3929
+ const changedIdxs = new Set(
3930
+ lines.map((l, i) => l.type !== "unchanged" ? i : -1).filter((i) => i !== -1)
3931
+ );
3932
+ const toShow = /* @__PURE__ */ new Set();
3933
+ for (const idx of changedIdxs) {
3934
+ for (let k2 = Math.max(0, idx - CONTEXT_LINES); k2 <= Math.min(lines.length - 1, idx + CONTEXT_LINES); k2++) {
3935
+ toShow.add(k2);
3936
+ }
3937
+ }
3938
+ const sorted = [...toShow].sort((a, b) => a - b);
3939
+ let prevIdx = -2;
3940
+ for (const idx of sorted) {
3941
+ if (idx > prevIdx + 1 && prevIdx !== -2) {
3942
+ console.log(import_chalk.default.cyan(" @@"));
3943
+ }
3944
+ const l = lines[idx];
3945
+ if (l.type === "added") {
3946
+ console.log(import_chalk.default.green(` + ${l.content}`));
3947
+ } else if (l.type === "removed") {
3948
+ console.log(import_chalk.default.red(` - ${l.content}`));
3949
+ } else {
3950
+ console.log(import_chalk.default.gray(` ${l.content}`));
3951
+ }
3952
+ prevIdx = idx;
3953
+ }
3954
+ }
3955
+ function printDiffSummary(diff, label) {
3956
+ const parts = [];
3957
+ if (diff.added > 0) parts.push(import_chalk.default.green(`+${diff.added}`));
3958
+ if (diff.removed > 0) parts.push(import_chalk.default.red(`-${diff.removed}`));
3959
+ if (parts.length === 0) parts.push(import_chalk.default.gray("no change"));
3960
+ console.log(import_chalk.default.bold(` ${label}: `) + parts.join(" ") + import_chalk.default.gray(` lines`));
3961
+ }
3962
+
3963
+ // core/spec-refiner.ts
3512
3964
  var SpecRefiner = class {
3513
3965
  constructor(provider) {
3514
3966
  this.provider = provider;
@@ -3517,16 +3969,16 @@ var SpecRefiner = class {
3517
3969
  let currentSpec = initialSpec;
3518
3970
  let round = 1;
3519
3971
  while (true) {
3520
- console.log(import_chalk.default.cyan(`
3972
+ console.log(import_chalk2.default.cyan(`
3521
3973
  \u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
3522
- console.log(import_chalk.default.gray(" Opening spec in editor. Save and close to continue."));
3974
+ console.log(import_chalk2.default.gray(" Opening spec in editor. Save and close to continue."));
3523
3975
  currentSpec = await (0, import_prompts.editor)({
3524
3976
  message: "Review and edit the spec:",
3525
3977
  default: currentSpec,
3526
3978
  postfix: ".md",
3527
3979
  waitForUserInput: false
3528
3980
  });
3529
- console.log(import_chalk.default.green(" \u2714 Spec saved."));
3981
+ console.log(import_chalk2.default.green(" \u2714 Spec saved."));
3530
3982
  const action = await (0, import_prompts.select)({
3531
3983
  message: "What would you like to do?",
3532
3984
  choices: [
@@ -3539,7 +3991,7 @@ var SpecRefiner = class {
3539
3991
  break;
3540
3992
  }
3541
3993
  if (action === "ai") {
3542
- console.log(import_chalk.default.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
3994
+ console.log(import_chalk2.default.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
3543
3995
  try {
3544
3996
  const improved = await this.provider.generate(
3545
3997
  `Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
@@ -3549,25 +4001,30 @@ Output ONLY the improved markdown spec, nothing else.
3549
4001
  ${currentSpec}`,
3550
4002
  "You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
3551
4003
  );
3552
- console.log(import_chalk.default.yellow("\n AI has suggested improvements. Opening diff in editor..."));
4004
+ console.log(import_chalk2.default.yellow("\n AI has suggested improvements. Opening diff in editor..."));
3553
4005
  const acceptImproved = await (0, import_prompts.confirm)({
3554
4006
  message: "Accept AI improvements? (opens editor so you can review first)",
3555
4007
  default: true
3556
4008
  });
3557
4009
  if (acceptImproved) {
4010
+ const diff = computeDiff(currentSpec, improved);
4011
+ console.log(import_chalk2.default.cyan("\n \u2500\u2500 AI Changes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4012
+ printDiffSummary(diff, "AI edits");
4013
+ printDiff(diff);
4014
+ console.log(import_chalk2.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
3558
4015
  currentSpec = await (0, import_prompts.editor)({
3559
4016
  message: "Review AI-improved spec (edit if needed, then save):",
3560
4017
  default: improved,
3561
4018
  postfix: ".md",
3562
4019
  waitForUserInput: false
3563
4020
  });
3564
- console.log(import_chalk.default.green(" \u2714 AI-improved spec accepted."));
4021
+ console.log(import_chalk2.default.green(" \u2714 AI-improved spec accepted."));
3565
4022
  } else {
3566
- console.log(import_chalk.default.gray(" AI improvements discarded. Keeping your version."));
4023
+ console.log(import_chalk2.default.gray(" AI improvements discarded. Keeping your version."));
3567
4024
  }
3568
4025
  } catch (err) {
3569
- console.error(import_chalk.default.red(" AI improvement failed:"), err);
3570
- console.log(import_chalk.default.gray(" Continuing with current spec."));
4026
+ console.error(import_chalk2.default.red(" AI improvement failed:"), err);
4027
+ console.log(import_chalk2.default.gray(" Continuing with current spec."));
3571
4028
  }
3572
4029
  }
3573
4030
  round++;
@@ -3577,10 +4034,10 @@ ${currentSpec}`,
3577
4034
  };
3578
4035
 
3579
4036
  // core/code-generator.ts
3580
- var import_chalk3 = __toESM(require("chalk"));
4037
+ var import_chalk6 = __toESM(require("chalk"));
3581
4038
  var import_child_process = require("child_process");
3582
- var path3 = __toESM(require("path"));
3583
- var fs4 = __toESM(require("fs-extra"));
4039
+ var path6 = __toESM(require("path"));
4040
+ var fs8 = __toESM(require("fs-extra"));
3584
4041
 
3585
4042
  // prompts/codegen.prompt.ts
3586
4043
  var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
@@ -3591,30 +4048,217 @@ Rules:
3591
4048
  3. Include proper error handling, input validation, and logging
3592
4049
  4. Output ONLY raw code content \u2014 NO markdown fences, NO explanations, NO comments outside the code
3593
4050
  5. Match the imports, exports, and module patterns visible in the existing codebase
3594
- 6. Do not add unnecessary dependencies \u2014 reuse what is already in the project
3595
- 7. If modifying an existing file, preserve all unchanged code exactly`;
3596
- var reviewSystemPrompt = `You are a Senior Software Architect conducting a thorough code review. Your review should be structured, constructive, and specific.
4051
+ 6. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content with only the new additions merged in
4052
+
4053
+ CRITICAL \u2014 Dependency Hallucination Prevention (MUST follow):
4054
+ 7. You will be given an "=== Installed Packages ===" section listing ALL packages available in this project.
4055
+ NEVER import or use ANY package, library, or module that does not appear in that list.
4056
+ This includes (but is not limited to): i18n libraries, UI component libraries, utility libraries, state management libraries.
4057
+ If a feature would normally need a missing library, implement the equivalent functionality using only what IS installed.
4058
+ Violating this rule will break the project and is unacceptable.
4059
+
4060
+ CRITICAL \u2014 File Reuse Rules:
4061
+ 8. NEVER create a new file if an existing file serves the same purpose. Check the shared config file list before planning any new file.
4062
+ 9. i18n / locale files: ONLY add translation keys if an i18n file already exists in the project. If no i18n/locale files are listed in "Installed Packages" or shared config files, do NOT add any i18n code.
4063
+ 10. constants / enums files: ALWAYS add new values to the EXISTING constants or enums file. Never create a new parallel file.
4064
+ 11. When in doubt: prefer "modify existing" over "create new".
4065
+
4066
+ CRITICAL \u2014 Component Reuse (MUST follow):
4067
+ 12. Before writing any UI component or element: check the "Existing reusable components" list in the context.
4068
+ If a component serving the same purpose already exists in src/components/, import and use it \u2014 do NOT create a duplicate.
4069
+ 13. Check the "Existing page examples" for how the project's UI library components (e.g. antd, element-plus, arco-design) are actually used.
4070
+ Copy those exact component names and import patterns. Do NOT use generic HTML elements where the UI library already provides a component.
4071
+
4072
+ CRITICAL \u2014 Frontend Architecture Layer Separation (MUST follow):
4073
+ 14. State management stores (Pinia, Vuex, Redux, Zustand) MUST NOT make HTTP requests directly.
4074
+ Stores call functions from the API layer (src/api/ or src/apis/). The API layer makes HTTP requests.
4075
+ If the existing store patterns in the context show no HTTP calls, do not add any.
4076
+ 13. API files import the HTTP client using ONLY the exact import line shown in "HTTP client import" in the context.
4077
+ NEVER invent a different import path (e.g., '@/utils/request', '@/utils/http') unless that exact path appears in the provided context.
4078
+
4079
+ CRITICAL \u2014 Learn conventions from examples, do not invent them:
4080
+ 15. The "=== Existing Shared Config Files ===" section below shows real files from the project.
4081
+ Study them carefully and match their exact structure, naming conventions, and patterns.
4082
+ - Router files: replicate the exact same file structure, path naming, and registration approach you see.
4083
+ - Store files: replicate the exact module pattern shown.
4084
+ - Do NOT apply generic framework defaults (e.g., Vue Router docs examples) if the project shows a different convention.
4085
+ - If you see a modules/ directory pattern in the examples, follow it. If you see a flat file pattern, follow that instead.
4086
+ The examples are ground truth. Your prior knowledge about "typical" project layouts is secondary.
4087
+
4088
+ CRITICAL \u2014 Route/Store index registration (MUST follow):
4089
+ 16. When creating a new route module file (e.g., src/router/routes/taskManagement.ts), you MUST ALSO update
4090
+ the corresponding index file (src/router/routes/index.ts) to import the new module and add it to the export array.
4091
+ This is non-negotiable. A route module that is not registered in the index will never be loaded.
4092
+ Pattern: add "import taskManagement from './taskManagement'" at the top and "taskManagement" inside the "export default [...]".
4093
+
4094
+ CRITICAL \u2014 Cross-file function name consistency (MUST follow):
4095
+ 17. When you see an "=== Files Already Generated in This Run ===" section, those file contents are the AUTHORITATIVE source
4096
+ of truth for exported function/variable names.
4097
+ NEVER rename, guess, or substitute alternative names. If the file exports "getTaskList", import "getTaskList" \u2014 not "getTasks".
4098
+ If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.`;
4099
+ var codeGenGoSystemPrompt = `You are a Senior Go Developer implementing features based on provided specifications.
4100
+
4101
+ Rules:
4102
+ 1. Follow standard Go project layout (cmd/, internal/, pkg/, api/). Match whatever layout already exists in the project.
4103
+ 2. Write idiomatic Go \u2014 use named return errors, defer for cleanup, context propagation, structured logging (slog or zap if present).
4104
+ 3. Write complete, production-ready code \u2014 no placeholders, no TODOs, no stub implementations.
4105
+ 4. Output ONLY raw Go code \u2014 NO markdown fences, NO explanations.
4106
+ 5. Use Go modules (go.mod already exists). Never add a dependency without checking go.mod first.
4107
+ 6. Error handling: always return errors up the call stack. Never ignore errors with _.
4108
+ 7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
4109
+ 8. HTTP handlers: match the existing router pattern (net/http ServeMux, gorilla/mux, chi, gin, echo \u2014 use whatever is in go.mod).
4110
+ 9. Tests: use the standard testing package + testify/assert if already in go.mod.
4111
+
4112
+ CRITICAL \u2014 File Reuse Rules:
4113
+ 10. NEVER create a parallel package if an existing one serves the purpose.
4114
+ 11. Register routes/handlers in the EXISTING router setup file.
4115
+ 12. Add new model structs to the EXISTING models file if one exists.`;
4116
+ var codeGenPythonSystemPrompt = `You are a Senior Python Developer implementing features based on provided specifications.
4117
+
4118
+ Rules:
4119
+ 1. Follow PEP 8 and PEP 20. Match the code style visible in the existing codebase.
4120
+ 2. Detect and match the existing framework: FastAPI, Flask, Django, or plain scripts.
4121
+ 3. Write complete, production-ready code \u2014 no placeholders, no TODOs, no stub implementations.
4122
+ 4. Output ONLY raw Python code \u2014 NO markdown fences, NO explanations.
4123
+ 5. Use type annotations (Python 3.10+ style). Use Pydantic models if FastAPI is detected.
4124
+ 6. Error handling: raise HTTPException (FastAPI/Flask) or domain exceptions \u2014 never swallow errors.
4125
+ 7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
4126
+ 8. Dependency management: only use packages already in requirements.txt / pyproject.toml.
4127
+ 9. FastAPI: use APIRouter for new endpoints and include it in the main app router.
4128
+ 10. Django: follow MVT pattern, register URLs in urls.py.
4129
+
4130
+ CRITICAL \u2014 File Reuse Rules:
4131
+ 11. NEVER create a parallel module if an existing one serves the purpose.
4132
+ 12. Register new routes/views in the EXISTING urls.py / router.py.
4133
+ 13. Add new models to the EXISTING models.py if it exists \u2014 do not create a parallel models file.`;
4134
+ var codeGenJavaSystemPrompt = `You are a Senior Java Developer implementing features based on provided specifications.
4135
+
4136
+ Rules:
4137
+ 1. Detect and match the existing framework: Spring Boot, Micronaut, or Quarkus.
4138
+ 2. Follow standard layered architecture: Controller \u2192 Service \u2192 Repository. Match existing package names.
4139
+ 3. Write complete, production-ready code \u2014 no placeholders, no TODOs.
4140
+ 4. Output ONLY raw Java code \u2014 NO markdown fences, NO explanations.
4141
+ 5. Use constructor injection (@Autowired on constructor, not field injection).
4142
+ 6. Exception handling: use @ControllerAdvice / @ExceptionHandler if already present. Never swallow exceptions.
4143
+ 7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
4144
+ 8. Use Lombok if already in pom.xml / build.gradle (@Data, @Builder, etc.).
4145
+
4146
+ CRITICAL \u2014 File Reuse Rules:
4147
+ 9. Register new endpoints in the EXISTING Controller class if one covers the same resource.
4148
+ 10. Add new repository methods to the EXISTING Repository interface \u2014 never create a parallel one.`;
4149
+ var codeGenRustSystemPrompt = `You are a Senior Rust Developer implementing features based on provided specifications.
4150
+
4151
+ Rules:
4152
+ 1. Detect and match the existing web framework: Axum, Actix-web, Warp, or Rocket.
4153
+ 2. Write idiomatic Rust \u2014 use Result<T,E> everywhere, no unwrap() in production paths, use ? operator.
4154
+ 3. Write complete, production-ready code \u2014 no placeholders, no TODOs.
4155
+ 4. Output ONLY raw Rust code \u2014 NO markdown fences, NO explanations.
4156
+ 5. Follow existing module structure (mod declarations in lib.rs / main.rs).
4157
+ 6. Use existing crates from Cargo.toml only \u2014 do not add new dependencies.
4158
+ 7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
4159
+ 8. Async: use tokio if already in Cargo.toml. Match existing async patterns.
4160
+
4161
+ CRITICAL \u2014 File Reuse Rules:
4162
+ 9. Register new routes in the EXISTING router setup (match pattern in main.rs or router.rs).
4163
+ 10. Add new model structs to the EXISTING models.rs / types.rs \u2014 never create a parallel file.`;
4164
+ var codeGenPhpSystemPrompt = `You are a Senior PHP Developer implementing features based on provided specifications.
4165
+
4166
+ Rules:
4167
+ 1. Detect and match the existing framework: Lumen, Laravel, or plain PHP. Follow its directory conventions (app/Http/Controllers, app/Models, routes/web.php / routes/api.php).
4168
+ 2. Write clean, PSR-12 compliant PHP 8.x code. Use typed properties, constructor promotion, named arguments, and match expressions where appropriate.
4169
+ 3. Write complete, production-ready code \u2014 no placeholders, no TODOs, no stub implementations.
4170
+ 4. Output ONLY raw PHP code \u2014 NO markdown fences, NO explanations.
4171
+ 5. Use Eloquent ORM if already present. Never introduce raw SQL when Eloquent is available.
4172
+ 6. Lumen: register routes in routes/web.php or routes/api.php. Laravel: use resource controllers and Route::apiResource where suitable.
4173
+ 7. Error handling: throw proper HTTP exceptions (e.g. Illuminate\\Http\\Exceptions\\HttpResponseException) and return JSON responses consistently.
4174
+ 8. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
4175
+ 9. Only use Composer packages already listed in composer.json \u2014 never add new dependencies.
4176
+
4177
+ CRITICAL \u2014 File Reuse Rules:
4178
+ 10. Register new routes in the EXISTING routes/api.php (or routes/web.php) \u2014 never create a parallel routes file.
4179
+ 11. Add new Eloquent model methods to the EXISTING Model class \u2014 never create a parallel model file.
4180
+ 12. Add new service methods to the EXISTING service class if one already covers the same resource.`;
4181
+ function getCodeGenSystemPrompt(repoType) {
4182
+ switch (repoType) {
4183
+ case "go":
4184
+ return codeGenGoSystemPrompt;
4185
+ case "python":
4186
+ return codeGenPythonSystemPrompt;
4187
+ case "java":
4188
+ return codeGenJavaSystemPrompt;
4189
+ case "rust":
4190
+ return codeGenRustSystemPrompt;
4191
+ case "php":
4192
+ return codeGenPhpSystemPrompt;
4193
+ default:
4194
+ return codeGenSystemPrompt;
4195
+ }
4196
+ }
4197
+ var reviewArchitectureSystemPrompt = `You are a Senior Software Architect reviewing the HIGH-LEVEL design of a code change.
4198
+
4199
+ Focus ONLY on:
4200
+ 1. **Spec compliance** \u2014 Does the implementation match the spec? Are there missing or extra endpoints/components?
4201
+ 2. **Layer separation** \u2014 Does each layer have the right responsibilities? (e.g., no business logic in controllers, no HTTP in stores)
4202
+ 3. **API contract** \u2014 Are request/response shapes correct? Are all error codes from the spec implemented?
4203
+ 4. **Data model integrity** \u2014 Are constraints, unique fields, and relationships correct?
4204
+ 5. **Security posture** \u2014 Are auth checks applied to the right endpoints? Any obvious missing auth?
4205
+
4206
+ DO NOT comment on:
4207
+ - Code style, naming conventions, formatting
4208
+ - Minor implementation details (variable names, inline comments)
4209
+ - Performance micro-optimizations
3597
4210
 
3598
- Always format your review with these exact sections:
4211
+ Format:
4212
+
4213
+ ## \u{1F3D7} \u67B6\u6784\u5408\u89C4\u6027 (Spec Compliance)
4214
+ Does the implementation match the spec? List any missing or wrong endpoints/components.
4215
+
4216
+ ## \u{1F500} \u5C42\u804C\u8D23\u5206\u79BB (Layer Separation)
4217
+ Any layer boundary violations?
4218
+
4219
+ ## \u{1F512} \u5B89\u5168\u4E0E\u6743\u9650 (Security & Auth)
4220
+ Any missing auth checks, exposed data, or privilege issues?
4221
+
4222
+ ## \u{1F4CB} \u67B6\u6784\u8BC4\u5206 (Architecture Score)
4223
+ Score: X/10 \u2014 One short paragraph.
4224
+
4225
+ Be specific. Reference file names or endpoint paths.`;
4226
+ var reviewImplementationSystemPrompt = `You are a Senior Engineer reviewing the IMPLEMENTATION DETAILS of a code change.
4227
+
4228
+ An architecture review has already been completed (provided as context). Do NOT repeat its findings.
4229
+
4230
+ Focus ONLY on:
4231
+ 1. **Input validation** \u2014 Are all inputs validated before use? Missing length/format/type checks?
4232
+ 2. **Error handling** \u2014 Are all error paths handled? Any unhandled promise rejections or uncaught exceptions?
4233
+ 3. **Edge cases** \u2014 Null/undefined handling, empty arrays, boundary conditions?
4234
+ 4. **Code patterns** \u2014 DRY violations, overly complex logic that could be simplified, missing abstractions?
4235
+ 5. **Past issue recurrence** \u2014 Does the code repeat any known patterns flagged in previous reviews (provided as history context)?
4236
+
4237
+ DO NOT repeat architecture-level findings already covered in the architecture review.
4238
+
4239
+ Format:
3599
4240
 
3600
4241
  ## \u2705 \u4F18\u70B9 (What's Good)
3601
- List specific things done well.
4242
+ Specific implementation strengths.
3602
4243
 
3603
4244
  ## \u26A0\uFE0F \u95EE\u9898 (Issues Found)
3604
- List bugs, security issues, or spec deviations with line references.
4245
+ Bugs, missing validation, error handling gaps \u2014 with file:line references where possible.
4246
+
4247
+ ## \u{1F501} \u5386\u53F2\u95EE\u9898\u590D\u73B0 (Recurring Issues)
4248
+ Any issues that appeared in past reviews and are still present? (Only if history context is provided)
3605
4249
 
3606
4250
  ## \u{1F4A1} \u6539\u8FDB\u5EFA\u8BAE (Suggestions)
3607
- Actionable improvements that would elevate code quality.
4251
+ Actionable, concrete improvements.
3608
4252
 
3609
- ## \u{1F4CA} \u603B\u4F53\u8BC4\u4EF7 (Overall Assessment)
3610
- Score: X/10 \u2014 One-paragraph summary.
4253
+ ## \u{1F4CA} \u7EFC\u5408\u8BC4\u5206 (Final Score)
4254
+ Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
3611
4255
 
3612
4256
  Be specific. Reference actual code, not vague principles.`;
3613
4257
 
3614
4258
  // core/task-generator.ts
3615
- var import_chalk2 = __toESM(require("chalk"));
3616
- var fs3 = __toESM(require("fs-extra"));
3617
- var path2 = __toESM(require("path"));
4259
+ var import_chalk3 = __toESM(require("chalk"));
4260
+ var fs5 = __toESM(require("fs-extra"));
4261
+ var path3 = __toESM(require("path"));
3618
4262
 
3619
4263
  // prompts/tasks.prompt.ts
3620
4264
  var tasksSystemPrompt = `You are a Senior Software Architect. Decompose the provided Feature Spec into an ordered list of discrete implementation tasks.
@@ -3627,7 +4271,7 @@ Each task object must have these exact fields:
3627
4271
  "title": "...", // short action phrase, e.g. "Add UserFavorite Prisma model"
3628
4272
  "description": "...", // 1-2 sentences, specific and actionable
3629
4273
  "layer": "data|service|api|test|infra", // implementation layer
3630
- "filesToTouch": ["..."], // specific file paths relative to project root
4274
+ "filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
3631
4275
  "acceptanceCriteria": ["..."], // verifiable completion conditions
3632
4276
  "dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
3633
4277
  "priority": "high|medium|low"
@@ -3640,14 +4284,64 @@ Layer ordering guidance (implement in this order):
3640
4284
  4. "api" \u2014 controllers, routes, middleware, validators
3641
4285
  5. "test" \u2014 unit tests, integration tests
3642
4286
 
3643
- Rules:
3644
- - Be specific about filesToTouch \u2014 use exact paths from the project context
4287
+ CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
4288
+ - ONLY use paths that appear in the "Verified File Inventory" section of the prompt.
4289
+ - For NEW files that don't exist yet, derive the path by following the naming pattern of sibling files already in the inventory (same directory, same extension, same casing).
4290
+ - For EXISTING singleton files (i18n, constants, enums, route index), you MUST use the exact path from the inventory. NEVER invent a sub-path or nested variant.
4291
+ - If you are unsure of the exact path for a new file, leave it as "TBD:<description>" rather than guessing.
4292
+ - Cross-check: after writing all tasks, verify every path in filesToTouch exists in the inventory or is a logical new sibling. If it doesn't pass this check, fix it.
4293
+
4294
+ Other rules:
3645
4295
  - acceptanceCriteria must be verifiable (not vague like "works correctly")
3646
4296
  - dependencies must reflect real implementation order
3647
4297
  - Aim for 4-10 tasks total \u2014 not too granular, not too coarse
3648
4298
  - Each task should be completable in one focused coding session`;
3649
4299
 
3650
4300
  // core/task-generator.ts
4301
+ function buildVerifiedInventory(context) {
4302
+ const lines = ["=== Verified File Inventory (filesToTouch MUST use paths from here) ===\n"];
4303
+ if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
4304
+ lines.push("-- Shared Config Files (APPEND-ONLY \u2014 never create a parallel file) --");
4305
+ for (const f of context.sharedConfigFiles) {
4306
+ lines.push(` [${f.category}] ${f.path}`);
4307
+ }
4308
+ lines.push("");
4309
+ }
4310
+ if (context.apiStructure.length > 0) {
4311
+ lines.push("-- API / Route / Controller Files --");
4312
+ for (const f of context.apiStructure.slice(0, 20)) {
4313
+ lines.push(` ${f}`);
4314
+ }
4315
+ lines.push("");
4316
+ }
4317
+ if (context.fileStructure.length > 0) {
4318
+ lines.push("-- Project File Tree (first 60 entries) --");
4319
+ for (const f of context.fileStructure.slice(0, 60)) {
4320
+ lines.push(` ${f}`);
4321
+ }
4322
+ lines.push("");
4323
+ }
4324
+ lines.push(
4325
+ "REMINDER: If a needed file does not appear above and is NOT a new file, verify its path.\nFor i18n/locale files, constants, enums, or route indexes \u2014 use EXACTLY the path shown above.\n"
4326
+ );
4327
+ return lines.join("\n");
4328
+ }
4329
+ function buildTaskPrompt(spec, context) {
4330
+ if (!context) return spec;
4331
+ const parts = [spec];
4332
+ if (context.constitution) {
4333
+ parts.push(`
4334
+ === Project Constitution (rules to follow) ===
4335
+ ${context.constitution.slice(0, 1500)}`);
4336
+ }
4337
+ if (context.techStack.length > 0) {
4338
+ parts.push(`
4339
+ === Tech Stack ===
4340
+ ${context.techStack.join(", ")}`);
4341
+ }
4342
+ parts.push("\n" + buildVerifiedInventory(context));
4343
+ return parts.join("\n");
4344
+ }
3651
4345
  var LAYER_ORDER = {
3652
4346
  data: 0,
3653
4347
  infra: 1,
@@ -3660,20 +4354,15 @@ var TaskGenerator = class {
3660
4354
  this.provider = provider;
3661
4355
  }
3662
4356
  async generateTasks(spec, context) {
3663
- const contextSummary = context ? `
3664
- === Project Context ===
3665
- Tech: ${context.techStack.join(", ")}
3666
- Files: ${context.fileStructure.slice(0, 20).join(", ")}
3667
- ` : "";
3668
- const prompt = `${spec}${contextSummary}`;
4357
+ const prompt = buildTaskPrompt(spec, context);
3669
4358
  const raw = await this.provider.generate(prompt, tasksSystemPrompt);
3670
4359
  return parseTasks(raw);
3671
4360
  }
3672
4361
  async saveTasks(tasks, specFilePath) {
3673
- const dir = path2.dirname(specFilePath);
3674
- const base = path2.basename(specFilePath, ".md");
3675
- const tasksFile = path2.join(dir, `${base}-tasks.json`);
3676
- await fs3.writeJson(tasksFile, tasks, { spaces: 2 });
4362
+ const dir = path3.dirname(specFilePath);
4363
+ const base = path3.basename(specFilePath, ".md");
4364
+ const tasksFile = path3.join(dir, `${base}-tasks.json`);
4365
+ await fs5.writeJson(tasksFile, tasks, { spaces: 2 });
3677
4366
  return tasksFile;
3678
4367
  }
3679
4368
  sortByLayer(tasks) {
@@ -3696,103 +4385,986 @@ function parseTasks(raw) {
3696
4385
  }
3697
4386
  function printTasks(tasks) {
3698
4387
  const layerColors = {
3699
- data: import_chalk2.default.magenta,
3700
- infra: import_chalk2.default.gray,
3701
- service: import_chalk2.default.blue,
3702
- api: import_chalk2.default.cyan,
3703
- test: import_chalk2.default.green
4388
+ data: import_chalk3.default.magenta,
4389
+ infra: import_chalk3.default.gray,
4390
+ service: import_chalk3.default.blue,
4391
+ api: import_chalk3.default.cyan,
4392
+ test: import_chalk3.default.green
3704
4393
  };
3705
- console.log(import_chalk2.default.bold(`
4394
+ console.log(import_chalk3.default.bold(`
3706
4395
  Tasks (${tasks.length}):`));
3707
4396
  for (const task of tasks) {
3708
- const color = layerColors[task.layer] ?? import_chalk2.default.white;
4397
+ const color = layerColors[task.layer] ?? import_chalk3.default.white;
3709
4398
  const badge = color(`[${task.layer}]`);
3710
- const prio = task.priority === "high" ? import_chalk2.default.red("\u25CF") : task.priority === "medium" ? import_chalk2.default.yellow("\u25CF") : import_chalk2.default.gray("\u25CF");
3711
- console.log(` ${prio} ${import_chalk2.default.bold(task.id)} ${badge} ${task.title}`);
4399
+ const prio = task.priority === "high" ? import_chalk3.default.red("\u25CF") : task.priority === "medium" ? import_chalk3.default.yellow("\u25CF") : import_chalk3.default.gray("\u25CF");
4400
+ console.log(` ${prio} ${import_chalk3.default.bold(task.id)} ${badge} ${task.title}`);
3712
4401
  }
3713
4402
  }
3714
4403
  async function loadTasksForSpec(specFilePath) {
3715
- const base = path2.basename(specFilePath, ".md");
3716
- const dir = path2.dirname(specFilePath);
3717
- const tasksFile = path2.join(dir, `${base}-tasks.json`);
3718
- if (await fs3.pathExists(tasksFile)) {
3719
- return fs3.readJson(tasksFile);
4404
+ const base = path3.basename(specFilePath, ".md");
4405
+ const dir = path3.dirname(specFilePath);
4406
+ const tasksFile = path3.join(dir, `${base}-tasks.json`);
4407
+ if (await fs5.pathExists(tasksFile)) {
4408
+ return fs5.readJson(tasksFile);
3720
4409
  }
3721
4410
  return null;
3722
4411
  }
4412
+ async function updateTaskStatus(specFilePath, taskId, status) {
4413
+ const tasks = await loadTasksForSpec(specFilePath);
4414
+ if (!tasks) return;
4415
+ const task = tasks.find((t) => t.id === taskId);
4416
+ if (!task) return;
4417
+ task.status = status;
4418
+ const base = path3.basename(specFilePath, ".md");
4419
+ const dir = path3.dirname(specFilePath);
4420
+ await fs5.writeJson(path3.join(dir, `${base}-tasks.json`), tasks, { spaces: 2 });
4421
+ }
3723
4422
 
3724
- // core/code-generator.ts
3725
- function isRtkAvailable() {
3726
- try {
3727
- (0, import_child_process.execSync)("rtk --version", { stdio: "ignore" });
3728
- return true;
3729
- } catch {
3730
- return false;
4423
+ // core/dsl-extractor.ts
4424
+ var import_chalk5 = __toESM(require("chalk"));
4425
+ var fs6 = __toESM(require("fs-extra"));
4426
+ var path4 = __toESM(require("path"));
4427
+ var import_prompts2 = require("@inquirer/prompts");
4428
+
4429
+ // core/dsl-validator.ts
4430
+ var import_chalk4 = __toESM(require("chalk"));
4431
+ var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
4432
+ var MAX_MODELS = 50;
4433
+ var MAX_FIELDS_PER_MODEL = 100;
4434
+ var MAX_ENDPOINTS = 100;
4435
+ var MAX_BEHAVIORS = 50;
4436
+ var MAX_ERRORS_PER_ENDPOINT = 20;
4437
+ function validateDsl(raw) {
4438
+ const errors = [];
4439
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4440
+ return {
4441
+ valid: false,
4442
+ errors: [{ path: "root", message: "DSL must be a JSON object, got: " + typeLabel(raw) }]
4443
+ };
3731
4444
  }
3732
- }
3733
- function stripCodeFences(output) {
3734
- const fenced = output.match(/^```(?:\w+)?\n([\s\S]*?)```\s*$/m);
3735
- if (fenced) return fenced[1].trim();
3736
- const lines = output.split("\n");
3737
- if (lines[0].startsWith("```")) lines.shift();
3738
- if (lines[lines.length - 1].trim() === "```") lines.pop();
3739
- return lines.join("\n").trim();
3740
- }
3741
- function parseJsonArray(text) {
3742
- const fenced = text.match(/```(?:json)?\n(\[[\s\S]*?\])\n```/);
3743
- const raw = fenced ? fenced[1] : text.match(/\[[\s\S]*?\]/)?.[0] ?? "";
3744
- try {
3745
- const parsed = JSON.parse(raw);
3746
- if (Array.isArray(parsed)) return parsed;
3747
- } catch {
4445
+ const obj = raw;
4446
+ if (obj["version"] !== "1.0") {
4447
+ errors.push({
4448
+ path: "version",
4449
+ message: `Must be "1.0", got: ${JSON.stringify(obj["version"])}`
4450
+ });
3748
4451
  }
3749
- return [];
3750
- }
3751
- var CodeGenerator = class {
3752
- constructor(provider, mode = "claude-code") {
3753
- this.provider = provider;
3754
- this.mode = mode;
4452
+ validateFeature(obj["feature"], "feature", errors);
4453
+ if (!Array.isArray(obj["models"])) {
4454
+ errors.push({ path: "models", message: `Must be an array, got: ${typeLabel(obj["models"])}` });
4455
+ } else {
4456
+ const models = obj["models"];
4457
+ if (models.length > MAX_MODELS) {
4458
+ errors.push({ path: "models", message: `Too many models (${models.length} > ${MAX_MODELS})` });
4459
+ }
4460
+ for (let i = 0; i < Math.min(models.length, MAX_MODELS); i++) {
4461
+ validateModel(models[i], `models[${i}]`, errors);
4462
+ }
3755
4463
  }
3756
- async generateCode(specFilePath, workingDir, context, options = {}) {
3757
- let effectiveMode = this.mode;
3758
- if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
3759
- console.log(
3760
- import_chalk3.default.yellow(
3761
- `
3762
- \u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
3763
- )
3764
- );
3765
- console.log(import_chalk3.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
3766
- console.log(import_chalk3.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
3767
- `));
3768
- effectiveMode = "api";
4464
+ if (!Array.isArray(obj["endpoints"])) {
4465
+ errors.push({ path: "endpoints", message: `Must be an array, got: ${typeLabel(obj["endpoints"])}` });
4466
+ } else {
4467
+ const eps = obj["endpoints"];
4468
+ if (eps.length > MAX_ENDPOINTS) {
4469
+ errors.push({ path: "endpoints", message: `Too many endpoints (${eps.length} > ${MAX_ENDPOINTS})` });
3769
4470
  }
3770
- switch (effectiveMode) {
3771
- case "claude-code":
3772
- return this.runClaudeCode(specFilePath, workingDir, options);
3773
- case "api":
3774
- return this.runApiMode(specFilePath, workingDir, context);
3775
- case "plan":
3776
- return this.runPlanMode(specFilePath);
4471
+ for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
4472
+ validateEndpoint(eps[i], `endpoints[${i}]`, errors);
3777
4473
  }
3778
4474
  }
3779
- // ── Mode: claude-code ──────────────────────────────────────────────────────
3780
- isClaudeCLIAvailable() {
3781
- try {
3782
- (0, import_child_process.execSync)("claude --version", { stdio: "ignore" });
3783
- return true;
3784
- } catch {
3785
- return false;
4475
+ if (obj["behaviors"] !== void 0) {
4476
+ if (!Array.isArray(obj["behaviors"])) {
4477
+ errors.push({ path: "behaviors", message: `Must be an array if present, got: ${typeLabel(obj["behaviors"])}` });
4478
+ } else {
4479
+ const behaviors = obj["behaviors"];
4480
+ if (behaviors.length > MAX_BEHAVIORS) {
4481
+ errors.push({ path: "behaviors", message: `Too many behaviors (${behaviors.length} > ${MAX_BEHAVIORS})` });
4482
+ }
4483
+ for (let i = 0; i < Math.min(behaviors.length, MAX_BEHAVIORS); i++) {
4484
+ validateBehavior(behaviors[i], `behaviors[${i}]`, errors);
4485
+ }
3786
4486
  }
3787
4487
  }
3788
- async runClaudeCode(specFilePath, workingDir, options = {}) {
3789
- console.log(import_chalk3.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3790
- if (!this.isClaudeCLIAvailable()) {
3791
- console.log(import_chalk3.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
3792
- console.log(import_chalk3.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
4488
+ if (obj["components"] !== void 0) {
4489
+ if (!Array.isArray(obj["components"])) {
4490
+ errors.push({ path: "components", message: `Must be an array if present, got: ${typeLabel(obj["components"])}` });
4491
+ } else {
4492
+ const components = obj["components"];
4493
+ for (let i = 0; i < Math.min(components.length, 50); i++) {
4494
+ validateComponent(components[i], `components[${i}]`, errors);
4495
+ }
4496
+ }
4497
+ }
4498
+ if (errors.length > 0) {
4499
+ return { valid: false, errors };
4500
+ }
4501
+ return { valid: true, dsl: raw };
4502
+ }
4503
+ function validateFeature(raw, path10, errors) {
4504
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4505
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4506
+ return;
4507
+ }
4508
+ const f = raw;
4509
+ requireNonEmptyString(f["id"], `${path10}.id`, errors);
4510
+ requireNonEmptyString(f["title"], `${path10}.title`, errors);
4511
+ requireNonEmptyString(f["description"], `${path10}.description`, errors);
4512
+ }
4513
+ function validateModel(raw, path10, errors) {
4514
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4515
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4516
+ return;
4517
+ }
4518
+ const m = raw;
4519
+ requireNonEmptyString(m["name"], `${path10}.name`, errors);
4520
+ if (!Array.isArray(m["fields"])) {
4521
+ errors.push({ path: `${path10}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4522
+ } else {
4523
+ const fields = m["fields"];
4524
+ if (fields.length > MAX_FIELDS_PER_MODEL) {
4525
+ errors.push({ path: `${path10}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4526
+ }
4527
+ for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4528
+ validateModelField(fields[j2], `${path10}.fields[${j2}]`, errors);
4529
+ }
4530
+ }
4531
+ if (m["relations"] !== void 0) {
4532
+ if (!Array.isArray(m["relations"])) {
4533
+ errors.push({ path: `${path10}.relations`, message: "Must be an array of strings if present" });
4534
+ } else {
4535
+ const rels = m["relations"];
4536
+ for (let j2 = 0; j2 < rels.length; j2++) {
4537
+ if (typeof rels[j2] !== "string") {
4538
+ errors.push({ path: `${path10}.relations[${j2}]`, message: "Must be a string" });
4539
+ }
4540
+ }
4541
+ }
4542
+ }
4543
+ }
4544
+ function validateModelField(raw, path10, errors) {
4545
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4546
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4547
+ return;
4548
+ }
4549
+ const f = raw;
4550
+ requireNonEmptyString(f["name"], `${path10}.name`, errors);
4551
+ requireNonEmptyString(f["type"], `${path10}.type`, errors);
4552
+ if (typeof f["required"] !== "boolean") {
4553
+ errors.push({ path: `${path10}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4554
+ }
4555
+ }
4556
+ function validateEndpoint(raw, path10, errors) {
4557
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4558
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4559
+ return;
4560
+ }
4561
+ const e = raw;
4562
+ requireNonEmptyString(e["id"], `${path10}.id`, errors);
4563
+ requireNonEmptyString(e["description"], `${path10}.description`, errors);
4564
+ if (!VALID_METHODS.includes(e["method"])) {
4565
+ errors.push({
4566
+ path: `${path10}.method`,
4567
+ message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
4568
+ });
4569
+ }
4570
+ if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
4571
+ errors.push({
4572
+ path: `${path10}.path`,
4573
+ message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
4574
+ });
4575
+ }
4576
+ if (typeof e["auth"] !== "boolean") {
4577
+ errors.push({ path: `${path10}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4578
+ }
4579
+ if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
4580
+ errors.push({
4581
+ path: `${path10}.successStatus`,
4582
+ message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
4583
+ });
4584
+ }
4585
+ requireNonEmptyString(e["successDescription"], `${path10}.successDescription`, errors);
4586
+ if (e["request"] !== void 0) {
4587
+ validateRequestSchema(e["request"], `${path10}.request`, errors);
4588
+ }
4589
+ if (e["errors"] !== void 0) {
4590
+ if (!Array.isArray(e["errors"])) {
4591
+ errors.push({ path: `${path10}.errors`, message: "Must be an array if present" });
4592
+ } else {
4593
+ const errs = e["errors"];
4594
+ if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
4595
+ errors.push({ path: `${path10}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
4596
+ }
4597
+ for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
4598
+ validateResponseError(errs[j2], `${path10}.errors[${j2}]`, errors);
4599
+ }
4600
+ }
4601
+ }
4602
+ }
4603
+ function validateRequestSchema(raw, path10, errors) {
4604
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4605
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4606
+ return;
4607
+ }
4608
+ const r = raw;
4609
+ for (const key of ["body", "query", "params"]) {
4610
+ if (r[key] !== void 0) {
4611
+ validateFieldMap(r[key], `${path10}.${key}`, errors);
4612
+ }
4613
+ }
4614
+ }
4615
+ function validateFieldMap(raw, path10, errors) {
4616
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4617
+ errors.push({ path: path10, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
4618
+ return;
4619
+ }
4620
+ const map = raw;
4621
+ for (const [k2, v2] of Object.entries(map)) {
4622
+ if (typeof v2 !== "string") {
4623
+ errors.push({ path: `${path10}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
4624
+ }
4625
+ }
4626
+ }
4627
+ function validateResponseError(raw, path10, errors) {
4628
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4629
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4630
+ return;
4631
+ }
4632
+ const e = raw;
4633
+ if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
4634
+ errors.push({ path: `${path10}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
4635
+ }
4636
+ requireNonEmptyString(e["code"], `${path10}.code`, errors);
4637
+ requireNonEmptyString(e["description"], `${path10}.description`, errors);
4638
+ }
4639
+ function validateBehavior(raw, path10, errors) {
4640
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4641
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4642
+ return;
4643
+ }
4644
+ const b = raw;
4645
+ requireNonEmptyString(b["id"], `${path10}.id`, errors);
4646
+ requireNonEmptyString(b["description"], `${path10}.description`, errors);
4647
+ if (b["constraints"] !== void 0) {
4648
+ if (!Array.isArray(b["constraints"])) {
4649
+ errors.push({ path: `${path10}.constraints`, message: "Must be an array of strings if present" });
4650
+ } else {
4651
+ const cs2 = b["constraints"];
4652
+ for (let j2 = 0; j2 < cs2.length; j2++) {
4653
+ if (typeof cs2[j2] !== "string") {
4654
+ errors.push({ path: `${path10}.constraints[${j2}]`, message: "Must be a string" });
4655
+ }
4656
+ }
4657
+ }
4658
+ }
4659
+ }
4660
+ function validateComponent(raw, path10, errors) {
4661
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4662
+ errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4663
+ return;
4664
+ }
4665
+ const c = raw;
4666
+ requireNonEmptyString(c["id"], `${path10}.id`, errors);
4667
+ requireNonEmptyString(c["name"], `${path10}.name`, errors);
4668
+ requireNonEmptyString(c["description"], `${path10}.description`, errors);
4669
+ if (c["props"] !== void 0) {
4670
+ if (!Array.isArray(c["props"])) {
4671
+ errors.push({ path: `${path10}.props`, message: "Must be an array if present" });
4672
+ } else {
4673
+ const props = c["props"];
4674
+ for (let j2 = 0; j2 < props.length; j2++) {
4675
+ const p = props[j2];
4676
+ if (typeof p !== "object" || p === null) {
4677
+ errors.push({ path: `${path10}.props[${j2}]`, message: "Must be an object" });
4678
+ continue;
4679
+ }
4680
+ requireNonEmptyString(p["name"], `${path10}.props[${j2}].name`, errors);
4681
+ requireNonEmptyString(p["type"], `${path10}.props[${j2}].type`, errors);
4682
+ if (typeof p["required"] !== "boolean") {
4683
+ errors.push({ path: `${path10}.props[${j2}].required`, message: "Must be boolean" });
4684
+ }
4685
+ }
4686
+ }
4687
+ }
4688
+ if (c["events"] !== void 0) {
4689
+ if (!Array.isArray(c["events"])) {
4690
+ errors.push({ path: `${path10}.events`, message: "Must be an array if present" });
4691
+ } else {
4692
+ const events = c["events"];
4693
+ for (let j2 = 0; j2 < events.length; j2++) {
4694
+ const e = events[j2];
4695
+ if (typeof e !== "object" || e === null) {
4696
+ errors.push({ path: `${path10}.events[${j2}]`, message: "Must be an object" });
4697
+ continue;
4698
+ }
4699
+ requireNonEmptyString(e["name"], `${path10}.events[${j2}].name`, errors);
4700
+ }
4701
+ }
4702
+ }
4703
+ if (c["state"] !== void 0) {
4704
+ if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
4705
+ errors.push({ path: `${path10}.state`, message: "Must be a flat object (Record<string, string>) if present" });
4706
+ }
4707
+ }
4708
+ if (c["apiCalls"] !== void 0) {
4709
+ if (!Array.isArray(c["apiCalls"])) {
4710
+ errors.push({ path: `${path10}.apiCalls`, message: "Must be an array of strings if present" });
4711
+ }
4712
+ }
4713
+ }
4714
+ function requireNonEmptyString(v2, path10, errors) {
4715
+ if (typeof v2 !== "string" || v2.trim().length === 0) {
4716
+ errors.push({
4717
+ path: path10,
4718
+ message: `Must be a non-empty string, got: ${typeLabel(v2)}`
4719
+ });
4720
+ }
4721
+ }
4722
+ function typeLabel(v2) {
4723
+ if (v2 === null) return "null";
4724
+ if (Array.isArray(v2)) return "array";
4725
+ return typeof v2;
4726
+ }
4727
+
4728
+ // core/dsl-extractor.ts
4729
+ function dslFilePath(specFilePath) {
4730
+ const dir = path4.dirname(specFilePath);
4731
+ const base = path4.basename(specFilePath, ".md");
4732
+ return path4.join(dir, `${base}.dsl.json`);
4733
+ }
4734
+ function buildDslContextSection(dsl) {
4735
+ const lines = [
4736
+ "=== Feature DSL (structured summary \u2014 use for implementation guidance) ==="
4737
+ ];
4738
+ if (dsl.models.length > 0) {
4739
+ lines.push("\n-- Data Models --");
4740
+ for (const model of dsl.models) {
4741
+ lines.push(`${model.name}:`);
4742
+ for (const field of model.fields) {
4743
+ const flags = [];
4744
+ if (field.required) flags.push("required");
4745
+ if (field.unique) flags.push("unique");
4746
+ lines.push(` ${field.name}: ${field.type}${flags.length ? ` (${flags.join(", ")})` : ""}`);
4747
+ }
4748
+ if (model.relations && model.relations.length > 0) {
4749
+ lines.push(` relations: ${model.relations.join("; ")}`);
4750
+ }
4751
+ }
4752
+ }
4753
+ if (dsl.endpoints.length > 0) {
4754
+ lines.push("\n-- API Endpoints --");
4755
+ for (const ep of dsl.endpoints) {
4756
+ lines.push(`${ep.id}: ${ep.method} ${ep.path} [auth: ${ep.auth}] \u2192 ${ep.successStatus}`);
4757
+ lines.push(` ${ep.description}`);
4758
+ if (ep.request?.body) {
4759
+ const fields = Object.entries(ep.request.body).map(([k2, v2]) => `${k2}: ${v2}`).join(", ");
4760
+ lines.push(` body: { ${fields} }`);
4761
+ }
4762
+ if (ep.errors && ep.errors.length > 0) {
4763
+ lines.push(` errors: ${ep.errors.map((e) => `${e.status} ${e.code}`).join(", ")}`);
4764
+ }
4765
+ }
4766
+ }
4767
+ if (dsl.behaviors.length > 0) {
4768
+ lines.push("\n-- Business Behaviors --");
4769
+ for (const b of dsl.behaviors) {
4770
+ lines.push(`${b.id}: ${b.description}`);
4771
+ if (b.trigger) lines.push(` trigger: ${b.trigger}`);
4772
+ if (b.constraints && b.constraints.length > 0) {
4773
+ lines.push(` rules: ${b.constraints.join("; ")}`);
4774
+ }
4775
+ }
4776
+ }
4777
+ if (dsl.components && dsl.components.length > 0) {
4778
+ lines.push("\n-- UI Components --");
4779
+ for (const cmp of dsl.components) {
4780
+ lines.push(`${cmp.id}: ${cmp.name} \u2014 ${cmp.description}`);
4781
+ if (cmp.props.length > 0) {
4782
+ lines.push(` props: ${cmp.props.map((p) => `${p.name}${p.required ? "" : "?"}:${p.type}`).join(", ")}`);
4783
+ }
4784
+ if (cmp.events.length > 0) {
4785
+ lines.push(` events: ${cmp.events.map((e) => `${e.name}(${e.payload ?? ""})`).join(", ")}`);
4786
+ }
4787
+ if (Object.keys(cmp.state).length > 0) {
4788
+ lines.push(` state: ${Object.entries(cmp.state).map(([k2, v2]) => `${k2}:${v2}`).join(", ")}`);
4789
+ }
4790
+ if (cmp.apiCalls.length > 0) {
4791
+ lines.push(` calls: ${cmp.apiCalls.join(", ")}`);
4792
+ }
4793
+ }
4794
+ }
4795
+ lines.push("\n=== End of DSL ===");
4796
+ return lines.join("\n");
4797
+ }
4798
+ async function loadDslForSpec(specFilePath) {
4799
+ const dslPath = dslFilePath(specFilePath);
4800
+ if (!await fs6.pathExists(dslPath)) return null;
4801
+ try {
4802
+ const raw = await fs6.readJson(dslPath);
4803
+ const result = validateDsl(raw);
4804
+ return result.valid ? result.dsl : null;
4805
+ } catch {
4806
+ return null;
4807
+ }
4808
+ }
4809
+
4810
+ // core/frontend-context-loader.ts
4811
+ var fs7 = __toESM(require("fs-extra"));
4812
+ var path5 = __toESM(require("path"));
4813
+ var STATE_MANAGEMENT_LIBS = [
4814
+ "zustand",
4815
+ "redux",
4816
+ "@reduxjs/toolkit",
4817
+ "jotai",
4818
+ "recoil",
4819
+ "mobx",
4820
+ "mobx-react",
4821
+ "valtio",
4822
+ "pinia",
4823
+ "vuex"
4824
+ ];
4825
+ var HTTP_CLIENT_LIBS = [
4826
+ ["swr", "swr"],
4827
+ ["@tanstack/react-query", "react-query"],
4828
+ ["react-query", "react-query"],
4829
+ ["axios", "axios"],
4830
+ ["ky", "ky"]
4831
+ ];
4832
+ var UI_LIBRARY_LIBS = [
4833
+ ["antd", "antd"],
4834
+ ["@ant-design/pro-components", "antd-pro"],
4835
+ ["@mui/material", "mui"],
4836
+ ["@chakra-ui/react", "chakra-ui"],
4837
+ ["shadcn-ui", "shadcn"],
4838
+ ["@radix-ui/react-primitive", "radix-ui"],
4839
+ ["element-plus", "element-plus"],
4840
+ ["vant", "vant"],
4841
+ ["tailwindcss", "tailwind"],
4842
+ ["@tailwindcss/vite", "tailwind"],
4843
+ ["react-native-paper", "react-native-paper"]
4844
+ ];
4845
+ var ROUTING_LIBS = [
4846
+ ["react-router-dom", "react-router"],
4847
+ ["react-router", "react-router"],
4848
+ ["@tanstack/react-router", "tanstack-router"],
4849
+ ["react-navigation", "react-navigation"],
4850
+ ["expo-router", "expo-router"],
4851
+ ["vue-router", "vue-router"]
4852
+ ];
4853
+ async function loadFrontendContext(projectRoot) {
4854
+ const ctx = {
4855
+ framework: "unknown",
4856
+ stateManagement: [],
4857
+ httpClient: "fetch",
4858
+ uiLibrary: "unknown",
4859
+ routingPattern: "unknown",
4860
+ testFramework: "unknown",
4861
+ existingApiFiles: [],
4862
+ apiWrapperContent: [],
4863
+ hookFiles: [],
4864
+ hookPatterns: [],
4865
+ storeFiles: [],
4866
+ storePatterns: [],
4867
+ reusableComponents: [],
4868
+ pageExamples: [],
4869
+ componentPatterns: []
4870
+ };
4871
+ try {
4872
+ const pkgPath = path5.join(projectRoot, "package.json");
4873
+ if (!await fs7.pathExists(pkgPath)) return ctx;
4874
+ const pkg = await fs7.readJson(pkgPath);
4875
+ const allDeps = {
4876
+ ...pkg.dependencies ?? {},
4877
+ ...pkg.devDependencies ?? {}
4878
+ };
4879
+ const depKeys = Object.keys(allDeps);
4880
+ const has = (name) => depKeys.includes(name);
4881
+ if (has("react-native") || has("expo")) {
4882
+ ctx.framework = "react-native";
4883
+ } else if (has("next")) {
4884
+ ctx.framework = "next";
4885
+ } else if (has("react")) {
4886
+ ctx.framework = "react";
4887
+ } else if (has("vue")) {
4888
+ ctx.framework = "vue";
4889
+ }
4890
+ ctx.stateManagement = STATE_MANAGEMENT_LIBS.filter((lib) => has(lib));
4891
+ for (const [lib, label] of HTTP_CLIENT_LIBS) {
4892
+ if (has(lib)) {
4893
+ ctx.httpClient = label;
4894
+ break;
4895
+ }
4896
+ }
4897
+ for (const [lib, label] of UI_LIBRARY_LIBS) {
4898
+ if (has(lib)) {
4899
+ ctx.uiLibrary = label;
4900
+ break;
4901
+ }
4902
+ }
4903
+ if (ctx.uiLibrary === "unknown") {
4904
+ ctx.uiLibrary = "none";
4905
+ }
4906
+ if (ctx.framework === "next") {
4907
+ const hasAppDir = await fs7.pathExists(path5.join(projectRoot, "app"));
4908
+ ctx.routingPattern = hasAppDir ? "next-app-router" : "next-pages-router";
4909
+ } else {
4910
+ for (const [lib, label] of ROUTING_LIBS) {
4911
+ if (has(lib)) {
4912
+ ctx.routingPattern = label;
4913
+ break;
4914
+ }
4915
+ }
4916
+ }
4917
+ if (depKeys.includes("@testing-library/react") || depKeys.includes("@testing-library/vue")) {
4918
+ ctx.testFramework = "rtl";
4919
+ } else if (depKeys.includes("cypress")) {
4920
+ ctx.testFramework = "cypress";
4921
+ } else if (depKeys.includes("vitest")) {
4922
+ ctx.testFramework = "vitest";
4923
+ } else if (depKeys.includes("jest") || depKeys.includes("@jest/core")) {
4924
+ ctx.testFramework = "jest";
4925
+ }
4926
+ const apiFilePatterns = [
4927
+ "src/api/**/*.{ts,js}",
4928
+ "src/apis/**/*.{ts,js}",
4929
+ "src/services/**/*.{ts,js}",
4930
+ "src/lib/api/**/*.{ts,js}",
4931
+ "src/utils/api/**/*.{ts,js}",
4932
+ "api/**/*.{ts,js}",
4933
+ "services/**/*.{ts,js}"
4934
+ ];
4935
+ for (const pattern of apiFilePatterns) {
4936
+ const files = await Ze(pattern, {
4937
+ cwd: projectRoot,
4938
+ ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"]
4939
+ });
4940
+ ctx.existingApiFiles.push(...files);
4941
+ }
4942
+ ctx.existingApiFiles = [...new Set(ctx.existingApiFiles)].slice(0, 20);
4943
+ for (const relPath of ctx.existingApiFiles.slice(0, 2)) {
4944
+ try {
4945
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
4946
+ const preview = content.split("\n").slice(0, 60).join("\n");
4947
+ ctx.apiWrapperContent.push(`// ${relPath}
4948
+ ${preview}`);
4949
+ } catch {
4950
+ }
4951
+ }
4952
+ const hookPatterns = [
4953
+ "src/hooks/use*.{ts,tsx}",
4954
+ "src/**/hooks/use*.{ts,tsx}",
4955
+ "hooks/use*.{ts,tsx}"
4956
+ ];
4957
+ for (const pattern of hookPatterns) {
4958
+ const files = await Ze(pattern, {
4959
+ cwd: projectRoot,
4960
+ ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"]
4961
+ });
4962
+ ctx.hookFiles.push(...files);
4963
+ }
4964
+ ctx.hookFiles = [...new Set(ctx.hookFiles)].slice(0, 15);
4965
+ for (const relPath of ctx.hookFiles.slice(0, 2)) {
4966
+ try {
4967
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
4968
+ const preview = content.split("\n").slice(0, 30).join("\n");
4969
+ ctx.hookPatterns.push(`// ${relPath}
4970
+ ${preview}`);
4971
+ } catch {
4972
+ }
4973
+ }
4974
+ const storeFilePatterns = [
4975
+ "src/store/**/*.{ts,js}",
4976
+ "src/stores/**/*.{ts,js}",
4977
+ "src/**/slice*.{ts,js}",
4978
+ "src/**/*slice.{ts,js}",
4979
+ "src/**/*store.{ts,js}",
4980
+ "src/**/*Store.{ts,js}",
4981
+ "store/**/*.{ts,js}",
4982
+ "stores/**/*.{ts,js}"
4983
+ ];
4984
+ for (const pattern of storeFilePatterns) {
4985
+ const files = await Ze(pattern, {
4986
+ cwd: projectRoot,
4987
+ ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"]
4988
+ });
4989
+ ctx.storeFiles.push(...files);
4990
+ }
4991
+ ctx.storeFiles = [...new Set(ctx.storeFiles)].slice(0, 10);
4992
+ for (const relPath of ctx.storeFiles.slice(0, 2)) {
4993
+ try {
4994
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
4995
+ const preview = content.split("\n").slice(0, 60).join("\n");
4996
+ ctx.storePatterns.push(`// ${relPath}
4997
+ ${preview}`);
4998
+ } catch {
4999
+ }
5000
+ }
5001
+ const httpImportRegex = /^import\s+(?:\w+|\{[^}]+\})\s+from\s+['"](@\/[^'"]+|axios|ky)['"]/m;
5002
+ for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
5003
+ try {
5004
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
5005
+ const match = content.match(httpImportRegex);
5006
+ if (match) {
5007
+ ctx.httpClientImport = match[0].trim();
5008
+ break;
5009
+ }
5010
+ } catch {
5011
+ }
5012
+ }
5013
+ const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
5014
+ const paginationInterfaceRegex = new RegExp(
5015
+ `(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
5016
+ "s"
5017
+ );
5018
+ const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
5019
+ for (const relPath of ctx.existingApiFiles) {
5020
+ if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
5021
+ try {
5022
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
5023
+ if (!paginationFieldNames.some((f) => content.includes(f))) continue;
5024
+ const interfaceMatch = content.match(paginationInterfaceRegex);
5025
+ if (!interfaceMatch) continue;
5026
+ const interfaceName = interfaceMatch[1];
5027
+ const fnRegex = new RegExp(
5028
+ `export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${interfaceName}[^)]*\\)[\\s\\S]*?\\n\\}`,
5029
+ ""
5030
+ );
5031
+ const fnMatch = content.match(fnRegex);
5032
+ if (fnMatch) {
5033
+ ctx.paginationExample = `// From ${relPath}
5034
+ ${interfaceMatch[0]}
5035
+
5036
+ ${fnMatch[0]}`;
5037
+ break;
5038
+ }
5039
+ } catch {
5040
+ }
5041
+ }
5042
+ const sharedComponentDirs = ["src/components", "components"];
5043
+ for (const dir of sharedComponentDirs) {
5044
+ const absDir = path5.join(projectRoot, dir);
5045
+ if (!await fs7.pathExists(absDir)) continue;
5046
+ const files = await Ze("**/*.{vue,tsx,jsx}", {
5047
+ cwd: absDir,
5048
+ ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"],
5049
+ maxDepth: 4
5050
+ });
5051
+ ctx.reusableComponents.push(...files.map((f) => path5.join(dir, f)));
5052
+ }
5053
+ ctx.reusableComponents = [...new Set(ctx.reusableComponents)].slice(0, 40);
5054
+ const viewDirs = ["src/views", "src/pages", "views", "pages"];
5055
+ const viewFiles = [];
5056
+ for (const dir of viewDirs) {
5057
+ const absDir = path5.join(projectRoot, dir);
5058
+ if (!await fs7.pathExists(absDir)) continue;
5059
+ const files = await Ze("**/*.{vue,tsx,jsx}", {
5060
+ cwd: absDir,
5061
+ ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"],
5062
+ maxDepth: 3
5063
+ });
5064
+ viewFiles.push(...files.map((f) => path5.join(dir, f)));
5065
+ if (viewFiles.length >= 6) break;
5066
+ }
5067
+ for (const relPath of viewFiles.slice(0, 2)) {
5068
+ try {
5069
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
5070
+ const preview = content.split("\n").slice(0, 80).join("\n");
5071
+ ctx.pageExamples.push(`// ${relPath}
5072
+ ${preview}`);
5073
+ } catch {
5074
+ }
5075
+ }
5076
+ const componentFiles = [];
5077
+ for (const dir of sharedComponentDirs) {
5078
+ const absDir = path5.join(projectRoot, dir);
5079
+ if (!await fs7.pathExists(absDir)) continue;
5080
+ const files = await Ze("**/*.{tsx,vue,jsx}", {
5081
+ cwd: absDir,
5082
+ ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"],
5083
+ maxDepth: 2
5084
+ });
5085
+ componentFiles.push(...files.map((f) => path5.join(dir, f)));
5086
+ if (componentFiles.length >= 5) break;
5087
+ }
5088
+ for (const relPath of componentFiles.slice(0, 3)) {
5089
+ try {
5090
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
5091
+ const preview = content.split("\n").slice(0, 40).join("\n");
5092
+ ctx.componentPatterns.push(`// ${relPath}
5093
+ ${preview}`);
5094
+ } catch {
5095
+ }
5096
+ }
5097
+ await extractRouteModuleContext(projectRoot, ctx);
5098
+ } catch {
5099
+ }
5100
+ return ctx;
5101
+ }
5102
+ async function extractRouteModuleContext(projectRoot, ctx) {
5103
+ const modulePatterns = [
5104
+ "src/router/modules/**/*.{ts,js}",
5105
+ "src/routes/modules/**/*.{ts,js}",
5106
+ "src/router/**/*.{ts,js}"
5107
+ ];
5108
+ const moduleFiles = [];
5109
+ for (const pattern of modulePatterns) {
5110
+ const files = await Ze(pattern, {
5111
+ cwd: projectRoot,
5112
+ ignore: ["**/index.{ts,js}", "node_modules/**", "**/*.test.*"]
5113
+ });
5114
+ moduleFiles.push(...files);
5115
+ }
5116
+ if (moduleFiles.length === 0) return;
5117
+ const layoutImportRegex = /^(?:const\s+Layout\s*=.*import\(['"][^'"]+['"]\)|import\s+Layout\s+from\s+['"][^'"]+['"])/m;
5118
+ for (const relPath of moduleFiles) {
5119
+ try {
5120
+ const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
5121
+ const match = content.match(layoutImportRegex);
5122
+ if (match) {
5123
+ ctx.layoutImport = match[0].trim();
5124
+ const preview = content.split("\n").slice(0, 100).join("\n");
5125
+ ctx.routeModuleExample = { path: relPath, content: preview };
5126
+ break;
5127
+ }
5128
+ } catch {
5129
+ }
5130
+ }
5131
+ }
5132
+ function buildFrontendContextSection(ctx) {
5133
+ const lines = [
5134
+ "=== Frontend Project Context ===",
5135
+ `Framework : ${ctx.framework}`,
5136
+ `State Management : ${ctx.stateManagement.join(", ") || "none detected"}`,
5137
+ `HTTP Client : ${ctx.httpClient}`,
5138
+ `UI Library : ${ctx.uiLibrary}`,
5139
+ `Routing : ${ctx.routingPattern}`,
5140
+ `Test Framework : ${ctx.testFramework}`
5141
+ ];
5142
+ if (ctx.layoutImport) {
5143
+ lines.push(
5144
+ `
5145
+ Layout component import (COPY THIS EXACTLY in every new route module \u2014 do NOT invent a different path):`,
5146
+ ` ${ctx.layoutImport}`
5147
+ );
5148
+ }
5149
+ if (ctx.routeModuleExample) {
5150
+ lines.push(
5151
+ `
5152
+ Existing route module template (${ctx.routeModuleExample.path}) \u2014 use this as the structural template for new route modules:`,
5153
+ "```",
5154
+ ctx.routeModuleExample.content,
5155
+ "```"
5156
+ );
5157
+ }
5158
+ if (ctx.existingApiFiles.length > 0) {
5159
+ lines.push(`
5160
+ Existing API/service files (${ctx.existingApiFiles.length}):`);
5161
+ ctx.existingApiFiles.slice(0, 10).forEach((f) => lines.push(` - ${f}`));
5162
+ }
5163
+ if (ctx.httpClientImport) {
5164
+ lines.push(
5165
+ `
5166
+ HTTP client import (COPY THIS EXACTLY in every new API file \u2014 do NOT import from any other path):`,
5167
+ ` ${ctx.httpClientImport}`
5168
+ );
5169
+ }
5170
+ if (ctx.paginationExample) {
5171
+ lines.push(
5172
+ `
5173
+ Pagination pattern (COPY THIS EXACTLY for all paginated list APIs \u2014 use IDENTICAL parameter names, HTTP method, and call style):`,
5174
+ "```typescript",
5175
+ ctx.paginationExample,
5176
+ "```"
5177
+ );
5178
+ }
5179
+ if (ctx.apiWrapperContent.length > 0) {
5180
+ lines.push(`
5181
+ API file patterns (new API functions must follow this exact structure):`);
5182
+ ctx.apiWrapperContent.forEach((p) => {
5183
+ lines.push("```");
5184
+ lines.push(p);
5185
+ lines.push("```");
5186
+ });
5187
+ }
5188
+ if (ctx.hookFiles.length > 0) {
5189
+ lines.push(`
5190
+ Existing custom hooks (${ctx.hookFiles.length}):`);
5191
+ ctx.hookFiles.slice(0, 8).forEach((f) => lines.push(` - ${f}`));
5192
+ }
5193
+ if (ctx.hookPatterns.length > 0) {
5194
+ lines.push(`
5195
+ Hook patterns (follow same structure):`);
5196
+ ctx.hookPatterns.forEach((p) => {
5197
+ lines.push("```");
5198
+ lines.push(p);
5199
+ lines.push("```");
5200
+ });
5201
+ }
5202
+ if (ctx.storeFiles.length > 0) {
5203
+ lines.push(`
5204
+ State store files (${ctx.storeFiles.length}):`);
5205
+ ctx.storeFiles.slice(0, 8).forEach((f) => lines.push(` - ${f}`));
5206
+ }
5207
+ if (ctx.storePatterns.length > 0) {
5208
+ lines.push(
5209
+ `
5210
+ Existing store patterns (CRITICAL \u2014 stores in this project call API layer functions, they do NOT make HTTP requests directly):`,
5211
+ `Follow this exact structure for new stores:`
5212
+ );
5213
+ ctx.storePatterns.forEach((p) => {
5214
+ lines.push("```");
5215
+ lines.push(p);
5216
+ lines.push("```");
5217
+ });
5218
+ }
5219
+ if (ctx.reusableComponents.length > 0) {
5220
+ lines.push(
5221
+ `
5222
+ Existing reusable components in src/components/ (${ctx.reusableComponents.length} files):`,
5223
+ `ALWAYS check this list before creating a new component. Import and reuse existing ones instead of reinventing.`
5224
+ );
5225
+ ctx.reusableComponents.forEach((f) => lines.push(` - ${f}`));
5226
+ }
5227
+ if (ctx.pageExamples.length > 0) {
5228
+ lines.push(
5229
+ `
5230
+ Existing page examples (shows which UI library components and shared components are used \u2014 follow the same import and usage patterns):`
5231
+ );
5232
+ ctx.pageExamples.forEach((p) => {
5233
+ lines.push("```");
5234
+ lines.push(p);
5235
+ lines.push("```");
5236
+ });
5237
+ }
5238
+ if (ctx.componentPatterns.length > 0) {
5239
+ lines.push(`
5240
+ Shared component structure patterns:`);
5241
+ ctx.componentPatterns.forEach((p) => {
5242
+ lines.push("```");
5243
+ lines.push(p.slice(0, 500));
5244
+ lines.push("```");
5245
+ });
5246
+ }
5247
+ lines.push("=== End of Frontend Context ===");
5248
+ return lines.join("\n");
5249
+ }
5250
+
5251
+ // core/code-generator.ts
5252
+ function buildSharedConfigSection(context) {
5253
+ if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
5254
+ const lines = [
5255
+ "\n=== Existing Shared Config Files (study these to learn project conventions) ===",
5256
+ "These are real files from the project. Use them as ground truth for naming, structure, and registration patterns.",
5257
+ "Modify them in-place when adding new entries. Do NOT create parallel files for the same purpose.\n"
5258
+ ];
5259
+ for (const f of context.sharedConfigFiles) {
5260
+ lines.push(`--- File: ${f.path} [${f.category}] ---`);
5261
+ lines.push(f.preview);
5262
+ lines.push("");
5263
+ }
5264
+ return lines.join("\n") + "\n";
5265
+ }
5266
+ function buildInstalledPackagesSection(context) {
5267
+ if (!context?.dependencies || context.dependencies.length === 0) return "";
5268
+ return `
5269
+ === Installed Packages (ONLY use packages from this list \u2014 NEVER import anything not listed here) ===
5270
+ ${context.dependencies.join(", ")}
5271
+ `;
5272
+ }
5273
+ function buildGeneratedFilesSection(cache) {
5274
+ if (cache.size === 0) return "";
5275
+ const lines = [
5276
+ "\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ==="
5277
+ ];
5278
+ for (const [filePath, content] of cache) {
5279
+ lines.push(`
5280
+ --- ${filePath} ---`);
5281
+ lines.push(content.slice(0, 800));
5282
+ if (content.length > 800) lines.push("... (truncated)");
5283
+ }
5284
+ return lines.join("\n") + "\n";
5285
+ }
5286
+ function isRtkAvailable() {
5287
+ try {
5288
+ (0, import_child_process.execSync)("rtk --version", { stdio: "ignore" });
5289
+ return true;
5290
+ } catch {
5291
+ return false;
5292
+ }
5293
+ }
5294
+ function stripCodeFences(output) {
5295
+ const fenced = output.match(/^```(?:\w+)?\n([\s\S]*?)```\s*$/m);
5296
+ if (fenced) return fenced[1].trim();
5297
+ const lines = output.split("\n");
5298
+ if (lines[0].startsWith("```")) lines.shift();
5299
+ if (lines[lines.length - 1].trim() === "```") lines.pop();
5300
+ return lines.join("\n").trim();
5301
+ }
5302
+ function parseJsonArray(text) {
5303
+ const fenced = text.match(/```(?:json)?\n(\[[\s\S]*?\])\n```/);
5304
+ const raw = fenced ? fenced[1] : text.match(/\[[\s\S]*?\]/)?.[0] ?? "";
5305
+ try {
5306
+ const parsed = JSON.parse(raw);
5307
+ if (Array.isArray(parsed)) return parsed;
5308
+ } catch {
5309
+ }
5310
+ return [];
5311
+ }
5312
+ var CodeGenerator = class {
5313
+ constructor(provider, mode = "claude-code") {
5314
+ this.provider = provider;
5315
+ this.mode = mode;
5316
+ }
5317
+ /** Returns the list of file paths written to disk (useful for api-mode review). */
5318
+ async generateCode(specFilePath, workingDir, context, options = {}) {
5319
+ let effectiveMode = this.mode;
5320
+ if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
5321
+ console.log(
5322
+ import_chalk6.default.yellow(
5323
+ `
5324
+ \u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
5325
+ )
5326
+ );
5327
+ console.log(import_chalk6.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
5328
+ console.log(import_chalk6.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5329
+ `));
5330
+ effectiveMode = "api";
5331
+ }
5332
+ switch (effectiveMode) {
5333
+ case "claude-code":
5334
+ await this.runClaudeCode(specFilePath, workingDir, options);
5335
+ return [];
5336
+ case "api":
5337
+ return this.runApiMode(specFilePath, workingDir, context, options);
5338
+ case "plan":
5339
+ await this.runPlanMode(specFilePath);
5340
+ return [];
5341
+ }
5342
+ }
5343
+ // ── Mode: claude-code ──────────────────────────────────────────────────────
5344
+ isClaudeCLIAvailable() {
5345
+ try {
5346
+ (0, import_child_process.execSync)("claude --version", { stdio: "ignore" });
5347
+ return true;
5348
+ } catch {
5349
+ return false;
5350
+ }
5351
+ }
5352
+ async runClaudeCode(specFilePath, workingDir, options = {}) {
5353
+ console.log(import_chalk6.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5354
+ if (!this.isClaudeCLIAvailable()) {
5355
+ console.log(import_chalk6.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5356
+ console.log(import_chalk6.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
3793
5357
  return this.runPlanMode(specFilePath);
3794
5358
  }
5359
+ const rtkAvailable = isRtkAvailable();
5360
+ const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
5361
+ if (rtkAvailable) {
5362
+ console.log(import_chalk6.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
5363
+ }
3795
5364
  const tasks = await loadTasksForSpec(specFilePath);
5365
+ if (options.auto && tasks && tasks.length > 0) {
5366
+ return this.runClaudeCodeIncremental(tasks, specFilePath, workingDir, claudeCmd, options);
5367
+ }
3796
5368
  const taskSection = tasks && tasks.length > 0 ? `
3797
5369
 
3798
5370
  == Implementation Tasks (implement in order) ==
@@ -3800,64 +5372,139 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
3800
5372
  Files: ${t.filesToTouch.join(", ")}
3801
5373
  Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
3802
5374
  const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
3803
- const promptFile = path3.join(workingDir, ".claude-prompt.txt");
3804
- await fs4.writeFile(promptFile, promptContent, "utf-8");
3805
- const rtkAvailable = isRtkAvailable();
3806
- const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
3807
- if (rtkAvailable) {
3808
- console.log(import_chalk3.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
3809
- }
5375
+ const promptFile = path6.join(workingDir, ".claude-prompt.txt");
5376
+ await fs8.writeFile(promptFile, promptContent, "utf-8");
3810
5377
  if (options.auto) {
3811
- console.log(import_chalk3.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
3812
- console.log(import_chalk3.default.gray(` Spec: ${specFilePath}`));
3813
- if (tasks) console.log(import_chalk3.default.gray(` Tasks: ${tasks.length} tasks loaded`));
5378
+ console.log(import_chalk6.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5379
+ console.log(import_chalk6.default.gray(` Spec: ${specFilePath}`));
3814
5380
  try {
3815
5381
  (0, import_child_process.execSync)(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
3816
5382
  cwd: workingDir,
3817
5383
  stdio: "inherit"
3818
5384
  });
3819
- console.log(import_chalk3.default.green("\n \u2714 Claude Code completed."));
5385
+ console.log(import_chalk6.default.green("\n \u2714 Claude Code completed."));
3820
5386
  } catch {
3821
- console.log(import_chalk3.default.yellow("\n Claude Code exited. Check output above."));
5387
+ console.log(import_chalk6.default.yellow("\n Claude Code exited. Check output above."));
3822
5388
  }
3823
5389
  } else {
3824
- console.log(import_chalk3.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
3825
- console.log(import_chalk3.default.gray(` Spec: ${specFilePath}`));
3826
- if (tasks) console.log(import_chalk3.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
3827
- console.log(import_chalk3.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
5390
+ console.log(import_chalk6.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
5391
+ console.log(import_chalk6.default.gray(` Spec: ${specFilePath}`));
5392
+ if (tasks) console.log(import_chalk6.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
5393
+ console.log(import_chalk6.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
3828
5394
  try {
3829
5395
  (0, import_child_process.execSync)(claudeCmd, { cwd: workingDir, stdio: "inherit" });
3830
- console.log(import_chalk3.default.green("\n \u2714 Claude Code session completed."));
5396
+ console.log(import_chalk6.default.green("\n \u2714 Claude Code session completed."));
3831
5397
  } catch {
3832
- console.log(import_chalk3.default.yellow("\n Claude Code session ended. Continuing workflow."));
5398
+ console.log(import_chalk6.default.yellow("\n Claude Code session ended. Continuing workflow."));
3833
5399
  }
3834
5400
  }
3835
5401
  }
5402
+ /**
5403
+ * Incremental claude-code execution: one `claude -p` call per task.
5404
+ * Tasks marked as "done" are skipped (resume support).
5405
+ * Progress is shown as a percentage bar.
5406
+ */
5407
+ async runClaudeCodeIncremental(tasks, specFilePath, workingDir, claudeCmd, options) {
5408
+ const pending = tasks.filter((t) => t.status !== "done");
5409
+ const doneCount = tasks.length - pending.length;
5410
+ if (options.resume && doneCount > 0) {
5411
+ console.log(import_chalk6.default.cyan(`
5412
+ Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
5413
+ } else {
5414
+ console.log(import_chalk6.default.cyan(`
5415
+ Incremental mode: ${tasks.length} tasks`));
5416
+ }
5417
+ let completed = doneCount;
5418
+ for (const task of tasks) {
5419
+ if (task.status === "done") {
5420
+ printTaskProgress(completed, tasks.length, task, "skip");
5421
+ continue;
5422
+ }
5423
+ printTaskProgress(completed, tasks.length, task, "run");
5424
+ const taskPrompt = `Task: ${task.id} \u2014 ${task.title}
5425
+ Layer: ${task.layer}
5426
+ Description: ${task.description}
5427
+ Files to touch: ${task.filesToTouch.join(", ") || "as needed"}
5428
+ Acceptance criteria:
5429
+ ${task.acceptanceCriteria.map((c) => ` - ${c}`).join("\n")}
5430
+
5431
+ Full spec is at: ${specFilePath}
5432
+ Implement ONLY this task. Do not implement other tasks.`;
5433
+ let taskStatus = "done";
5434
+ try {
5435
+ (0, import_child_process.execSync)(`${claudeCmd} -p "${taskPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`, {
5436
+ cwd: workingDir,
5437
+ stdio: "inherit"
5438
+ });
5439
+ completed++;
5440
+ } catch {
5441
+ taskStatus = "failed";
5442
+ console.log(import_chalk6.default.yellow(`
5443
+ \u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
5444
+ }
5445
+ await updateTaskStatus(specFilePath, task.id, taskStatus);
5446
+ }
5447
+ const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
5448
+ console.log(
5449
+ import_chalk6.default.bold(
5450
+ `
5451
+ ${successCount === tasks.length ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
5452
+ )
5453
+ );
5454
+ }
3836
5455
  // ── Mode: api ─────────────────────────────────────────────────────────────
3837
- async runApiMode(specFilePath, workingDir, context) {
5456
+ async runApiMode(specFilePath, workingDir, context, options = {}) {
3838
5457
  console.log(
3839
- import_chalk3.default.blue(
5458
+ import_chalk6.default.blue(
3840
5459
  `
3841
5460
  \u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
3842
5461
  )
3843
5462
  );
3844
- const spec = await fs4.readFile(specFilePath, "utf-8");
5463
+ const systemPrompt = getCodeGenSystemPrompt(options.repoType);
5464
+ if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
5465
+ console.log(import_chalk6.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
5466
+ }
5467
+ const spec = await fs8.readFile(specFilePath, "utf-8");
3845
5468
  const constitutionSection = context?.constitution ? `
3846
5469
  === Project Constitution (MUST follow) ===
3847
5470
  ${context.constitution.slice(0, 2e3)}
3848
5471
  ` : "";
3849
5472
  const contextSummary = context ? `Tech Stack: ${context.techStack.join(", ")}
3850
5473
  Existing files: ${context.fileStructure.slice(0, 20).join(", ")}` : "";
5474
+ const installedPackagesSection = buildInstalledPackagesSection(context);
5475
+ const sharedConfigSection = buildSharedConfigSection(context);
5476
+ const dsl = await loadDslForSpec(specFilePath);
5477
+ const dslSection = dsl ? `
5478
+ ${buildDslContextSection(dsl)}
5479
+ ` : "";
5480
+ if (dsl) {
5481
+ const cmpCount = dsl.components?.length ?? 0;
5482
+ const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
5483
+ console.log(import_chalk6.default.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
5484
+ }
5485
+ const isFrontend = isFrontendDeps(context?.dependencies ?? []);
5486
+ let frontendSection = "";
5487
+ if (isFrontend) {
5488
+ const fctx = await loadFrontendContext(workingDir);
5489
+ frontendSection = `
5490
+ ${buildFrontendContextSection(fctx)}
5491
+ `;
5492
+ console.log(import_chalk6.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
5493
+ }
3851
5494
  const tasks = await loadTasksForSpec(specFilePath);
3852
5495
  if (tasks && tasks.length > 0) {
3853
- return this.runApiModeWithTasks(spec, tasks, workingDir, constitutionSection);
5496
+ return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
3854
5497
  }
3855
- console.log(import_chalk3.default.gray(" [1/2] Planning implementation files..."));
5498
+ console.log(import_chalk6.default.gray(" [1/2] Planning implementation files..."));
3856
5499
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
3857
5500
 
5501
+ IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
5502
+ use action "modify" (never "create") even if you are only adding new entries.
5503
+ IMPORTANT: Check the "Frontend Project Context" section below. Extend existing hooks/services/stores \u2014 do NOT create new parallel utilities.
5504
+
3858
5505
  === Feature Spec ===
3859
5506
  ${spec}
3860
- ${constitutionSection}
5507
+ ${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
3861
5508
  === Project Context ===
3862
5509
  ${contextSummary}
3863
5510
 
@@ -3868,71 +5515,195 @@ Output ONLY a valid JSON array:
3868
5515
  ]`;
3869
5516
  let filePlan = [];
3870
5517
  try {
3871
- const planResponse = await this.provider.generate(planPrompt, codeGenSystemPrompt);
5518
+ const planResponse = await this.provider.generate(planPrompt, systemPrompt);
3872
5519
  filePlan = parseJsonArray(planResponse);
3873
5520
  } catch (err) {
3874
- console.error(import_chalk3.default.red(" Failed to generate file plan:"), err);
5521
+ console.error(import_chalk6.default.red(" Failed to generate file plan:"), err);
3875
5522
  }
3876
5523
  if (filePlan.length === 0) {
3877
- console.log(import_chalk3.default.yellow(" Could not determine file plan. Falling back to plan mode."));
3878
- return this.runPlanMode(specFilePath);
5524
+ console.log(import_chalk6.default.yellow(" Could not determine file plan. Falling back to plan mode."));
5525
+ await this.runPlanMode(specFilePath);
5526
+ return [];
3879
5527
  }
3880
- console.log(import_chalk3.default.cyan(`
5528
+ console.log(import_chalk6.default.cyan(`
3881
5529
  Plan: ${filePlan.length} file(s) to process`));
3882
5530
  filePlan.forEach((item) => {
3883
- const icon = item.action === "create" ? import_chalk3.default.green("+") : import_chalk3.default.yellow("~");
3884
- console.log(` ${icon} ${item.file}: ${import_chalk3.default.gray(item.description)}`);
5531
+ const icon = item.action === "create" ? import_chalk6.default.green("+") : import_chalk6.default.yellow("~");
5532
+ console.log(` ${icon} ${item.file}: ${import_chalk6.default.gray(item.description)}`);
3885
5533
  });
3886
- await this.generateFiles(filePlan, spec, workingDir, constitutionSection);
3887
- }
3888
- async runApiModeWithTasks(spec, tasks, workingDir, constitutionSection) {
3889
- console.log(import_chalk3.default.cyan(`
5534
+ const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
5535
+ return files;
5536
+ }
5537
+ async runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection, frontendSection = "", sharedConfigSection = "", options = {}, systemPrompt = getCodeGenSystemPrompt(), context) {
5538
+ const pendingTasks = tasks.filter((t) => t.status !== "done");
5539
+ const doneCount = tasks.length - pendingTasks.length;
5540
+ if (options.resume && doneCount > 0) {
5541
+ console.log(import_chalk6.default.cyan(`
5542
+ Task-based generation (resume): ${tasks.length} tasks (${import_chalk6.default.green(doneCount + " already done")}, skipping)`));
5543
+ } else if (doneCount > 0) {
5544
+ console.log(import_chalk6.default.cyan(`
5545
+ Task-based generation: ${tasks.length} tasks (${import_chalk6.default.green(doneCount + " already done")}, resuming from checkpoint)`));
5546
+ } else {
5547
+ console.log(import_chalk6.default.cyan(`
3890
5548
  Task-based generation: ${tasks.length} tasks`));
5549
+ }
5550
+ const sharedConfigPaths = new Set(
5551
+ (context?.sharedConfigFiles ?? []).map((f) => f.path)
5552
+ );
5553
+ const processedSharedConfigs = /* @__PURE__ */ new Set();
5554
+ const generatedFileCache = /* @__PURE__ */ new Map();
3891
5555
  let totalSuccess = 0;
3892
5556
  let totalFiles = 0;
5557
+ let completedTasks = doneCount;
5558
+ const allGeneratedFiles = [];
3893
5559
  for (const task of tasks) {
3894
- console.log(import_chalk3.default.bold(`
3895
- \u2192 ${task.id} [${task.layer}] ${task.title}`));
3896
- if (task.filesToTouch.length === 0) {
3897
- console.log(import_chalk3.default.gray(" No files specified, skipping."));
3898
- continue;
5560
+ if (task.status === "done") {
5561
+ printTaskProgress(completedTasks++, tasks.length, task, "skip");
5562
+ }
5563
+ }
5564
+ const LAYER_ORDER2 = ["data", "infra", "service", "api", "test"];
5565
+ const layerGroups = [];
5566
+ for (const layer of LAYER_ORDER2) {
5567
+ const group = pendingTasks.filter((t) => t.layer === layer);
5568
+ if (group.length > 0) layerGroups.push({ layer, tasks: group });
5569
+ }
5570
+ const unknownTasks = pendingTasks.filter((t) => !LAYER_ORDER2.includes(t.layer));
5571
+ if (unknownTasks.length > 0) layerGroups.push({ layer: "other", tasks: unknownTasks });
5572
+ for (const { layer, tasks: layerTasks } of layerGroups) {
5573
+ const isParallel = layerTasks.length > 1;
5574
+ const layerIcon = LAYER_ICONS[layer] ?? " ";
5575
+ if (isParallel) {
5576
+ const pct = Math.round(completedTasks / tasks.length * 100);
5577
+ const barWidth = 20;
5578
+ const filled = Math.round(pct / 100 * barWidth);
5579
+ const bar = import_chalk6.default.green("\u2588".repeat(filled)) + import_chalk6.default.gray("\u2591".repeat(barWidth - filled));
5580
+ console.log(
5581
+ import_chalk6.default.bold(`
5582
+ [${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
5583
+ );
5584
+ } else {
5585
+ printTaskProgress(completedTasks, tasks.length, layerTasks[0], "run");
3899
5586
  }
3900
- const filePlan = task.filesToTouch.map((f) => ({
3901
- file: f,
3902
- action: "create",
3903
- description: task.description
3904
- }));
3905
- const taskContext = `Task: ${task.id} \u2014 ${task.title}
5587
+ const generatedFilesSection = buildGeneratedFilesSection(generatedFileCache);
5588
+ const taskResultPromises = layerTasks.map(async (task) => {
5589
+ if (task.filesToTouch.length === 0) {
5590
+ if (!isParallel) console.log(import_chalk6.default.gray(" No files specified, skipping."));
5591
+ return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
5592
+ }
5593
+ const filePlan = await Promise.all(
5594
+ task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
5595
+ const exists = await fs8.pathExists(path6.join(workingDir, f));
5596
+ return {
5597
+ file: f,
5598
+ action: exists ? "modify" : "create",
5599
+ description: task.description
5600
+ };
5601
+ })
5602
+ );
5603
+ const createsNewFiles = filePlan.some((f) => f.action === "create");
5604
+ const taskText = `${task.title} ${task.description}`.toLowerCase();
5605
+ const impliesRegistration = createsNewFiles && (taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
5606
+ if (filePlan.length === 0) {
5607
+ return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration };
5608
+ }
5609
+ const taskContext = `Task: ${task.id} \u2014 ${task.title}
3906
5610
  ${task.description}
3907
5611
  Acceptance: ${task.acceptanceCriteria.join("; ")}`;
3908
- const { success, total } = await this.generateFiles(
3909
- filePlan,
3910
- `${spec}
5612
+ const { success, total, files } = await this.generateFiles(
5613
+ filePlan,
5614
+ `${spec}
3911
5615
 
3912
5616
  === Current Task ===
3913
5617
  ${taskContext}`,
3914
- workingDir,
3915
- constitutionSection
3916
- );
3917
- totalSuccess += success;
3918
- totalFiles += total;
5618
+ workingDir,
5619
+ constitutionSection + frontendSection + sharedConfigSection + generatedFilesSection,
5620
+ systemPrompt,
5621
+ isParallel ? task.id : void 0
5622
+ // prefix output lines with task ID in parallel mode
5623
+ );
5624
+ const createdFiles = filePlan.filter((fp) => fp.action === "create").map((fp) => fp.file);
5625
+ return { task, files, createdFiles, success, total, impliesRegistration };
5626
+ });
5627
+ const layerResults = await Promise.all(taskResultPromises);
5628
+ if (isParallel) {
5629
+ console.log("");
5630
+ }
5631
+ for (const result of layerResults) {
5632
+ totalSuccess += result.success;
5633
+ totalFiles += result.total;
5634
+ allGeneratedFiles.push(...result.files);
5635
+ if (isParallel) {
5636
+ const icon = result.success === result.total ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!");
5637
+ const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
5638
+ console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
5639
+ }
5640
+ const taskStatus = result.success === result.total ? "done" : "failed";
5641
+ await updateTaskStatus(specFilePath, result.task.id, taskStatus);
5642
+ if (taskStatus === "failed") {
5643
+ console.log(import_chalk6.default.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
5644
+ }
5645
+ }
5646
+ completedTasks += layerTasks.length;
5647
+ for (const result of layerResults) {
5648
+ for (const writtenFile of result.files) {
5649
+ if (/src[\\/](api[s]?|services?|stores?|composables?)[\\/]/.test(writtenFile)) {
5650
+ try {
5651
+ const content = await fs8.readFile(path6.join(workingDir, writtenFile), "utf-8");
5652
+ generatedFileCache.set(writtenFile, content);
5653
+ } catch {
5654
+ }
5655
+ }
5656
+ }
5657
+ }
5658
+ const anyImpliesRegistration = layerResults.some((r) => r.impliesRegistration);
5659
+ if (anyImpliesRegistration && sharedConfigPaths.size > 0 && context?.sharedConfigFiles) {
5660
+ const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
5661
+ for (const sharedFile of context.sharedConfigFiles) {
5662
+ if (processedSharedConfigs.has(sharedFile.path)) continue;
5663
+ const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path6.basename(f).replace(/\.[jt]sx?$/, ""));
5664
+ if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
5665
+ let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
5666
+ if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
5667
+ purpose = `Add to this file: import ${newModuleNames.join(", ")} from their respective paths and register them in the export/default array. Do NOT remove any existing imports.`;
5668
+ }
5669
+ console.log(import_chalk6.default.gray(`
5670
+ + updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
5671
+ const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
5672
+ await this.generateFiles(
5673
+ [{ file: sharedFile.path, action: "modify", description: purpose }],
5674
+ `${spec}
5675
+
5676
+ === Context ===
5677
+ Updating shared registration after layer [${layer}] completed. New modules: ${newModuleNames.join(", ")}.`,
5678
+ workingDir,
5679
+ constitutionSection + frontendSection + sharedConfigSection + updatedGeneratedFilesSection,
5680
+ systemPrompt
5681
+ );
5682
+ processedSharedConfigs.add(sharedFile.path);
5683
+ }
5684
+ }
3919
5685
  }
3920
5686
  console.log(
3921
- import_chalk3.default.bold(
5687
+ import_chalk6.default.bold(
3922
5688
  `
3923
- ${totalSuccess === totalFiles ? import_chalk3.default.green("\u2714") : import_chalk3.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${tasks.length} tasks.`
5689
+ ${totalSuccess === totalFiles ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
3924
5690
  )
3925
5691
  );
5692
+ return allGeneratedFiles;
3926
5693
  }
3927
- async generateFiles(filePlan, spec, workingDir, constitutionSection) {
3928
- console.log(import_chalk3.default.gray(`
5694
+ async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
5695
+ const prefix = taskLabel ? ` [${import_chalk6.default.cyan(taskLabel)}] ` : " ";
5696
+ if (!taskLabel) {
5697
+ console.log(import_chalk6.default.gray(`
3929
5698
  Generating ${filePlan.length} file(s)...`));
5699
+ }
3930
5700
  let successCount = 0;
5701
+ const writtenFiles = [];
3931
5702
  for (const item of filePlan) {
3932
- const fullPath = path3.join(workingDir, item.file);
5703
+ const fullPath = path6.join(workingDir, item.file);
3933
5704
  let existingContent = "";
3934
- if (await fs4.pathExists(fullPath)) {
3935
- existingContent = await fs4.readFile(fullPath, "utf-8");
5705
+ if (await fs8.pathExists(fullPath)) {
5706
+ existingContent = await fs8.readFile(fullPath, "utf-8");
3936
5707
  }
3937
5708
  const codePrompt = `Implement this file.
3938
5709
 
@@ -3944,30 +5715,31 @@ ${spec}
3944
5715
  ${constitutionSection}
3945
5716
  === ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
3946
5717
  ${existingContent || "Output only the complete file content."}`;
3947
- process.stdout.write(` ${existingContent ? import_chalk3.default.yellow("~") : import_chalk3.default.green("+")} ${import_chalk3.default.bold(item.file)}... `);
3948
5718
  try {
3949
- const raw = await this.provider.generate(codePrompt, codeGenSystemPrompt);
5719
+ const raw = await this.provider.generate(codePrompt, systemPrompt);
3950
5720
  const fileContent = stripCodeFences(raw);
3951
- await fs4.ensureDir(path3.dirname(fullPath));
3952
- await fs4.writeFile(fullPath, fileContent, "utf-8");
3953
- console.log(import_chalk3.default.green("\u2714"));
5721
+ await fs8.ensureDir(path6.dirname(fullPath));
5722
+ await fs8.writeFile(fullPath, fileContent, "utf-8");
5723
+ console.log(`${prefix}${existingContent ? import_chalk6.default.yellow("~") : import_chalk6.default.green("+")} ${import_chalk6.default.bold(item.file)} ${import_chalk6.default.green("\u2714")}`);
3954
5724
  successCount++;
5725
+ writtenFiles.push(item.file);
3955
5726
  } catch (err) {
3956
- console.log(import_chalk3.default.red("\u2718"));
3957
- console.error(import_chalk3.default.red(` Error: ${err.message}`));
5727
+ console.log(`${prefix}${import_chalk6.default.red("\u2718")} ${import_chalk6.default.bold(item.file)} \u2014 ${import_chalk6.default.red(err.message)}`);
3958
5728
  }
3959
5729
  }
3960
- console.log(
3961
- import_chalk3.default.bold(
3962
- ` ${successCount === filePlan.length ? import_chalk3.default.green("\u2714") : import_chalk3.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
3963
- )
3964
- );
3965
- return { success: successCount, total: filePlan.length };
5730
+ if (!taskLabel) {
5731
+ console.log(
5732
+ import_chalk6.default.bold(
5733
+ ` ${successCount === filePlan.length ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
5734
+ )
5735
+ );
5736
+ }
5737
+ return { success: successCount, total: filePlan.length, files: writtenFiles };
3966
5738
  }
3967
5739
  // ── Mode: plan ─────────────────────────────────────────────────────────────
3968
5740
  async runPlanMode(specFilePath) {
3969
- console.log(import_chalk3.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3970
- const spec = await fs4.readFile(specFilePath, "utf-8");
5741
+ console.log(import_chalk6.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5742
+ const spec = await fs8.readFile(specFilePath, "utf-8");
3971
5743
  const plan = await this.provider.generate(
3972
5744
  `Create a detailed, step-by-step implementation plan for the following feature spec.
3973
5745
  Be specific about:
@@ -3979,22 +5751,95 @@ Be specific about:
3979
5751
  ${spec}`,
3980
5752
  "You are a senior developer creating an actionable implementation guide."
3981
5753
  );
3982
- console.log(import_chalk3.default.cyan("\n") + plan);
5754
+ console.log(import_chalk6.default.cyan("\n") + plan);
3983
5755
  }
3984
5756
  };
5757
+ var LAYER_ICONS = {
5758
+ data: "\u{1F4BE}",
5759
+ infra: "\u2699\uFE0F ",
5760
+ service: "\u{1F527}",
5761
+ api: "\u{1F310}",
5762
+ test: "\u{1F9EA}"
5763
+ };
5764
+ function printTaskProgress(completed, total, task, mode) {
5765
+ const pct = total > 0 ? Math.round(completed / total * 100) : 0;
5766
+ const barWidth = 20;
5767
+ const filled = Math.round(pct / 100 * barWidth);
5768
+ const bar = import_chalk6.default.green("\u2588".repeat(filled)) + import_chalk6.default.gray("\u2591".repeat(barWidth - filled));
5769
+ const icon = LAYER_ICONS[task.layer] ?? " ";
5770
+ if (mode === "skip") {
5771
+ console.log(
5772
+ import_chalk6.default.gray(`
5773
+ [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
5774
+ );
5775
+ } else {
5776
+ console.log(
5777
+ import_chalk6.default.bold(`
5778
+ [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
5779
+ );
5780
+ }
5781
+ }
3985
5782
 
3986
5783
  // core/reviewer.ts
3987
- var import_chalk4 = __toESM(require("chalk"));
5784
+ var import_chalk7 = __toESM(require("chalk"));
3988
5785
  var import_child_process2 = require("child_process");
5786
+ var path7 = __toESM(require("path"));
5787
+ var fs9 = __toESM(require("fs-extra"));
5788
+ var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5789
+ async function loadReviewHistory(projectRoot) {
5790
+ const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
5791
+ try {
5792
+ if (await fs9.pathExists(historyPath)) {
5793
+ return await fs9.readJson(historyPath);
5794
+ }
5795
+ } catch {
5796
+ }
5797
+ return [];
5798
+ }
5799
+ async function appendReviewHistory(projectRoot, entry) {
5800
+ const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
5801
+ const existing = await loadReviewHistory(projectRoot);
5802
+ const updated = [...existing, entry].slice(-20);
5803
+ try {
5804
+ await fs9.writeJson(historyPath, updated, { spaces: 2 });
5805
+ } catch {
5806
+ }
5807
+ }
5808
+ function extractScore(reviewText) {
5809
+ const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
5810
+ return match ? parseFloat(match[1]) : 0;
5811
+ }
5812
+ function extractTopIssues(reviewText) {
5813
+ const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
5814
+ return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
5815
+ }
5816
+ function buildHistoryContext(history) {
5817
+ if (history.length === 0) return "";
5818
+ const recent = history.slice(-5);
5819
+ const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
5820
+ for (const entry of recent) {
5821
+ lines.push(`
5822
+ [${entry.date}] ${path7.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
5823
+ entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
5824
+ }
5825
+ return lines.join("\n") + "\n";
5826
+ }
3989
5827
  var CodeReviewer = class {
3990
- constructor(provider) {
5828
+ constructor(provider, projectRoot = process.cwd()) {
3991
5829
  this.provider = provider;
5830
+ this.projectRoot = projectRoot;
3992
5831
  }
3993
5832
  getGitDiff() {
5833
+ const silent = { encoding: "utf-8", stdio: "pipe" };
5834
+ try {
5835
+ (0, import_child_process2.execSync)("git rev-parse --is-inside-work-tree", silent);
5836
+ } catch {
5837
+ return "";
5838
+ }
3994
5839
  try {
3995
- let diff = (0, import_child_process2.execSync)("git diff --cached", { encoding: "utf-8" });
3996
- if (!diff.trim()) diff = (0, import_child_process2.execSync)("git diff HEAD", { encoding: "utf-8" });
3997
- if (!diff.trim()) diff = (0, import_child_process2.execSync)("git diff", { encoding: "utf-8" });
5840
+ let diff = (0, import_child_process2.execSync)("git diff --cached", silent);
5841
+ if (!diff.trim()) diff = (0, import_child_process2.execSync)("git diff HEAD", silent);
5842
+ if (!diff.trim()) diff = (0, import_child_process2.execSync)("git diff", silent);
3998
5843
  return diff;
3999
5844
  } catch {
4000
5845
  return "";
@@ -4008,42 +5853,135 @@ var CodeReviewer = class {
4008
5853
  removed: lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length
4009
5854
  };
4010
5855
  }
4011
- async reviewCode(specContent) {
4012
- console.log(import_chalk4.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5856
+ /**
5857
+ * Two-pass review:
5858
+ * Pass 1 — architecture (spec compliance, layer separation, auth)
5859
+ * Pass 2 — implementation details (validation, error handling, edge cases)
5860
+ * + historical issue recurrence check
5861
+ *
5862
+ * Falls back to single-pass if the two-pass flag is not set.
5863
+ */
5864
+ async runTwoPassReview(specContent, codeContext, specFile) {
5865
+ console.log(import_chalk7.default.gray(" Pass 1/2: Architecture review..."));
5866
+ const archPrompt = `Review the architecture of this change.
5867
+
5868
+ === Feature Spec ===
5869
+ ${specContent || "(No spec \u2014 review for general code quality)"}
5870
+
5871
+ === Code ===
5872
+ ${codeContext}`;
5873
+ const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
5874
+ console.log(import_chalk7.default.gray(" Pass 2/2: Implementation review..."));
5875
+ const history = await loadReviewHistory(this.projectRoot);
5876
+ const historyContext = buildHistoryContext(history);
5877
+ const implPrompt = `Review the implementation details of this change.
5878
+
5879
+ === Feature Spec ===
5880
+ ${specContent || "(No spec \u2014 review for general code quality)"}
5881
+
5882
+ === Code ===
5883
+ ${codeContext}
5884
+
5885
+ === Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
5886
+ ${archReview}
5887
+ ${historyContext}`;
5888
+ const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
5889
+ const combined = `${archReview}
5890
+
5891
+ ${"\u2500".repeat(52)}
5892
+
5893
+ ${implReview}`;
5894
+ const score = extractScore(implReview) || extractScore(archReview);
5895
+ const topIssues = extractTopIssues(implReview);
5896
+ if (score > 0 && specFile) {
5897
+ await appendReviewHistory(this.projectRoot, {
5898
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
5899
+ specFile: path7.relative(this.projectRoot, specFile),
5900
+ score,
5901
+ topIssues
5902
+ });
5903
+ }
5904
+ return combined;
5905
+ }
5906
+ async reviewCode(specContent, specFile) {
5907
+ console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4013
5908
  const diff = this.getGitDiff();
4014
5909
  if (!diff.trim()) {
4015
5910
  console.log(
4016
- import_chalk4.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
5911
+ import_chalk7.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
4017
5912
  );
4018
- console.log(import_chalk4.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
5913
+ console.log(import_chalk7.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
4019
5914
  return "No changes";
4020
5915
  }
4021
5916
  const { files, added, removed } = this.getDiffStats(diff);
4022
5917
  console.log(
4023
- import_chalk4.default.gray(` Diff: ${files} file(s), ${import_chalk4.default.green("+" + added)} ${import_chalk4.default.red("-" + removed)}`)
5918
+ import_chalk7.default.gray(` Diff: ${files} file(s), ${import_chalk7.default.green("+" + added)} ${import_chalk7.default.red("-" + removed)}`)
5919
+ );
5920
+ console.log(
5921
+ import_chalk7.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
4024
5922
  );
5923
+ const codeContext = diff.slice(0, 1e4);
5924
+ const reviewResult = await this.runTwoPassReview(specContent, codeContext, specFile);
5925
+ console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5926
+ console.log(reviewResult);
5927
+ console.log(import_chalk7.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
5928
+ return reviewResult;
5929
+ }
5930
+ /**
5931
+ * Review directly from generated file contents (for api mode where git diff is empty).
5932
+ */
5933
+ async reviewFiles(specContent, filePaths, workingDir, specFile) {
5934
+ console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5935
+ console.log(import_chalk7.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
4025
5936
  console.log(
4026
- import_chalk4.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
5937
+ import_chalk7.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
4027
5938
  );
4028
- const prompt = `Conduct a code review for the following change.
5939
+ let filesSection = "";
5940
+ for (const filePath of filePaths) {
5941
+ const fullPath = path7.join(workingDir, filePath);
5942
+ try {
5943
+ const content = await fs9.readFile(fullPath, "utf-8");
5944
+ filesSection += `
4029
5945
 
4030
- === Feature Spec (what should be implemented) ===
4031
- ${specContent || "(No spec provided \u2014 review for general code quality, patterns, and correctness)"}
5946
+ === ${filePath} ===
5947
+ ${content.slice(0, 3e3)}`;
5948
+ if (content.length > 3e3) filesSection += `
5949
+ ... (truncated, ${content.length} chars total)`;
5950
+ } catch {
5951
+ filesSection += `
4032
5952
 
4033
- === Git Diff (what was actually implemented) ===
4034
- ${diff.slice(0, 1e4)}`;
4035
- const reviewResult = await this.provider.generate(prompt, reviewSystemPrompt);
4036
- console.log(import_chalk4.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5953
+ === ${filePath} ===
5954
+ (file not found)`;
5955
+ }
5956
+ }
5957
+ const reviewResult = await this.runTwoPassReview(specContent, filesSection, specFile);
5958
+ console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4037
5959
  console.log(reviewResult);
4038
- console.log(import_chalk4.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
5960
+ console.log(import_chalk7.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
4039
5961
  return reviewResult;
4040
5962
  }
5963
+ /** Print score trend from history (last N reviews) */
5964
+ async printScoreTrend(limit = 5) {
5965
+ const history = await loadReviewHistory(this.projectRoot);
5966
+ if (history.length === 0) {
5967
+ console.log(import_chalk7.default.gray(" No review history yet."));
5968
+ return;
5969
+ }
5970
+ const recent = history.slice(-limit);
5971
+ console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5972
+ for (const entry of recent) {
5973
+ const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
5974
+ const color = entry.score >= 8 ? import_chalk7.default.green : entry.score >= 6 ? import_chalk7.default.yellow : import_chalk7.default.red;
5975
+ console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path7.basename(entry.specFile)}`);
5976
+ }
5977
+ console.log(import_chalk7.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5978
+ }
4041
5979
  };
4042
5980
 
4043
5981
  // core/constitution-generator.ts
4044
- var import_chalk5 = __toESM(require("chalk"));
4045
- var fs5 = __toESM(require("fs-extra"));
4046
- var path4 = __toESM(require("path"));
5982
+ var import_chalk8 = __toESM(require("chalk"));
5983
+ var fs10 = __toESM(require("fs-extra"));
5984
+ var path8 = __toESM(require("path"));
4047
5985
 
4048
5986
  // prompts/constitution.prompt.ts
4049
5987
  var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
@@ -4091,9 +6029,24 @@ Output a Markdown document with EXACTLY these sections. Be specific and derive r
4091
6029
  - \u5FC5\u987B\u8986\u76D6\u7684\u6D4B\u8BD5\u573A\u666F\u7C7B\u578B
4092
6030
  - \u6D4B\u8BD5\u6846\u67B6\u548C\u5DE5\u5177
4093
6031
 
6032
+ ## 8. \u5171\u4EAB\u914D\u7F6E\u6587\u4EF6\u6E05\u5355 (Shared Config Files \u2014 Append-Only)
6033
+
6034
+ CRITICAL: The following files are **singleton config files** that already exist in the project.
6035
+ When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
6036
+ appended/merged into these existing files. **NEVER create a new parallel file.**
6037
+
6038
+ For each discovered file, list it as:
6039
+ - \`<relative-path>\` \u2014 <category> \u2014 **MODIFY ONLY, never recreate**
6040
+
6041
+ If the project context includes i18n/locale files: list ALL of them with their paths.
6042
+ If the project context includes constants/enums files: list ALL of them.
6043
+ If the project context includes route index files: list ALL of them.
6044
+ If none are provided in the context, write: "(No shared config files detected \u2014 will be populated on first run)"
6045
+
4094
6046
  ---
4095
6047
 
4096
- Be concise. Each rule must be specific enough to enforce, not a vague principle.`;
6048
+ Be concise. Each rule must be specific enough to enforce, not a vague principle.
6049
+ **Section 8 is the most important section for preventing file duplication bugs.**`;
4097
6050
 
4098
6051
  // core/constitution-generator.ts
4099
6052
  var CONSTITUTION_FILE = ".ai-spec-constitution.md";
@@ -4108,8 +6061,8 @@ var ConstitutionGenerator = class {
4108
6061
  return this.provider.generate(prompt, constitutionSystemPrompt);
4109
6062
  }
4110
6063
  async saveConstitution(projectRoot, content) {
4111
- const filePath = path4.join(projectRoot, CONSTITUTION_FILE);
4112
- await fs5.writeFile(filePath, content, "utf-8");
6064
+ const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6065
+ await fs10.writeFile(filePath, content, "utf-8");
4113
6066
  return filePath;
4114
6067
  }
4115
6068
  };
@@ -4141,32 +6094,88 @@ ${context.schema.slice(0, 4e3)}
4141
6094
  if (context.errorPatterns) {
4142
6095
  parts.push(`=== Error Handling Patterns ===
4143
6096
  ${context.errorPatterns}
6097
+ `);
6098
+ }
6099
+ if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
6100
+ const grouped = context.sharedConfigFiles.reduce(
6101
+ (acc, f) => {
6102
+ (acc[f.category] ??= []).push(f);
6103
+ return acc;
6104
+ },
6105
+ {}
6106
+ );
6107
+ const sections = [];
6108
+ for (const [category, files] of Object.entries(grouped)) {
6109
+ sections.push(`--- ${category} ---`);
6110
+ for (const f of files) {
6111
+ sections.push(`File: ${f.path}
6112
+ ${f.preview.slice(0, 600)}
6113
+ `);
6114
+ }
6115
+ }
6116
+ parts.push(`=== Existing Shared Config Files (Append-Only \u2014 NEVER Recreate) ===
6117
+ ${sections.join("\n")}
4144
6118
  `);
4145
6119
  }
4146
6120
  return parts.join("\n");
4147
6121
  }
4148
6122
  async function loadConstitution(projectRoot) {
4149
- const filePath = path4.join(projectRoot, CONSTITUTION_FILE);
4150
- if (await fs5.pathExists(filePath)) {
4151
- return fs5.readFile(filePath, "utf-8");
6123
+ const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6124
+ if (await fs10.pathExists(filePath)) {
6125
+ return fs10.readFile(filePath, "utf-8");
4152
6126
  }
4153
6127
  return void 0;
4154
6128
  }
4155
6129
  function printConstitutionHint(exists) {
4156
6130
  if (!exists) {
4157
6131
  console.log(
4158
- import_chalk5.default.yellow(
6132
+ import_chalk8.default.yellow(
4159
6133
  " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
4160
6134
  )
4161
6135
  );
4162
6136
  }
4163
6137
  }
4164
6138
 
6139
+ // core/combined-generator.ts
6140
+ var TASKS_SEPARATOR = "---TASKS_JSON---";
6141
+ var tasksInstruction = `
6142
+
6143
+ ---
6144
+ After outputting the complete spec above, append EXACTLY this line on its own (no extra text before or after it):
6145
+ ${TASKS_SEPARATOR}
6146
+ Then output a valid JSON array of implementation tasks. Each element must have these exact fields:
6147
+ {"id":"TASK-001","title":"...","description":"1-2 sentences, specific","layer":"data|infra|service|api|test","filesToTouch":["src/..."],"acceptanceCriteria":["verifiable condition"],"dependencies":[],"priority":"high|medium|low"}
6148
+ Layer order: data \u2192 infra \u2192 service \u2192 api \u2192 test. 4-10 tasks total. filesToTouch must use real paths from the project context.`;
6149
+ async function generateSpecWithTasks(provider, idea, context) {
6150
+ const contextBlock = buildTaskPrompt("", context).trim();
6151
+ const fullPrompt = [idea, contextBlock].filter(Boolean).join("\n\n");
6152
+ const combinedSystemPrompt = specPrompt + tasksInstruction;
6153
+ const raw = await provider.generate(fullPrompt, combinedSystemPrompt);
6154
+ return parseSpecAndTasks(raw);
6155
+ }
6156
+ function parseSpecAndTasks(raw) {
6157
+ const sepIdx = raw.indexOf(TASKS_SEPARATOR);
6158
+ if (sepIdx === -1) {
6159
+ return { spec: raw.trim(), tasks: [] };
6160
+ }
6161
+ const spec = raw.slice(0, sepIdx).trim();
6162
+ const tasksRaw = raw.slice(sepIdx + TASKS_SEPARATOR.length).trim();
6163
+ let tasks = [];
6164
+ try {
6165
+ const jsonMatch = tasksRaw.match(/\[[\s\S]*\]/);
6166
+ if (jsonMatch) {
6167
+ tasks = JSON.parse(jsonMatch[0]);
6168
+ }
6169
+ } catch {
6170
+ }
6171
+ return { spec, tasks };
6172
+ }
6173
+
4165
6174
  // git/worktree.ts
4166
6175
  var import_child_process3 = require("child_process");
4167
- var path5 = __toESM(require("path"));
4168
- var fs6 = __toESM(require("fs-extra"));
4169
- var import_chalk6 = __toESM(require("chalk"));
6176
+ var path9 = __toESM(require("path"));
6177
+ var fs11 = __toESM(require("fs-extra"));
6178
+ var import_chalk9 = __toESM(require("chalk"));
4170
6179
  var GitWorktreeManager = class {
4171
6180
  constructor(baseDir) {
4172
6181
  this.baseDir = baseDir;
@@ -4182,38 +6191,73 @@ var GitWorktreeManager = class {
4182
6191
  sanitizeFeatureName(idea) {
4183
6192
  return idea.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 30) || `feature-${Date.now()}`;
4184
6193
  }
6194
+ /**
6195
+ * Symlink dependency directories from the base repo into the worktree so that
6196
+ * tools like `vite`, `tsc`, etc. are available without re-installing.
6197
+ *
6198
+ * Handles: node_modules (npm/yarn/pnpm), vendor (PHP Composer)
6199
+ */
6200
+ async linkDependencies(worktreePath) {
6201
+ const candidates = ["node_modules", "vendor"];
6202
+ for (const dir of candidates) {
6203
+ const src = path9.join(this.baseDir, dir);
6204
+ const dest = path9.join(worktreePath, dir);
6205
+ if (!await fs11.pathExists(src)) continue;
6206
+ if (await fs11.pathExists(dest)) continue;
6207
+ try {
6208
+ await fs11.ensureSymlink(src, dest, "dir");
6209
+ console.log(import_chalk9.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6210
+ } catch (err) {
6211
+ console.log(import_chalk9.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6212
+ console.log(import_chalk9.default.yellow(` Run \`npm install\` inside the worktree manually.`));
6213
+ }
6214
+ }
6215
+ }
4185
6216
  async createWorktree(idea) {
4186
6217
  if (!this.isGitRepo()) {
4187
- console.log(import_chalk6.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6218
+ console.log(import_chalk9.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
4188
6219
  return null;
4189
6220
  }
4190
6221
  const featureName = this.sanitizeFeatureName(idea);
4191
6222
  const branchName = `feature/${featureName}`;
4192
- const repoName = path5.basename(this.baseDir);
4193
- const worktreePath = path5.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
4194
- console.log(import_chalk6.default.cyan(`
6223
+ const repoName = path9.basename(this.baseDir);
6224
+ const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
6225
+ console.log(import_chalk9.default.cyan(`
4195
6226
  --- Setting up Git Worktree ---`));
4196
- if (await fs6.pathExists(worktreePath)) {
4197
- console.log(import_chalk6.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6227
+ if (await fs11.pathExists(worktreePath)) {
6228
+ console.log(import_chalk9.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6229
+ await this.linkDependencies(worktreePath);
4198
6230
  return worktreePath;
4199
6231
  }
4200
6232
  try {
4201
6233
  let branchExists = false;
4202
6234
  try {
4203
- (0, import_child_process3.execSync)(`git show-ref --verify refs/heads/${branchName}`, { cwd: this.baseDir, stdio: "ignore" });
6235
+ (0, import_child_process3.execSync)(`git show-ref --verify refs/heads/${branchName}`, {
6236
+ cwd: this.baseDir,
6237
+ stdio: "ignore"
6238
+ });
4204
6239
  branchExists = true;
4205
6240
  } catch {
4206
6241
  }
4207
- console.log(import_chalk6.default.gray(`Creating worktree at: ${worktreePath}`));
6242
+ console.log(import_chalk9.default.gray(`Creating worktree at: ${worktreePath}`));
4208
6243
  if (branchExists) {
4209
- (0, import_child_process3.execSync)(`git worktree add "${worktreePath}" ${branchName}`, { cwd: this.baseDir, stdio: "inherit" });
6244
+ (0, import_child_process3.execSync)(`git worktree add "${worktreePath}" ${branchName}`, {
6245
+ cwd: this.baseDir,
6246
+ stdio: "inherit"
6247
+ });
4210
6248
  } else {
4211
- (0, import_child_process3.execSync)(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd: this.baseDir, stdio: "inherit" });
6249
+ (0, import_child_process3.execSync)(`git worktree add -b ${branchName} "${worktreePath}"`, {
6250
+ cwd: this.baseDir,
6251
+ stdio: "inherit"
6252
+ });
4212
6253
  }
4213
- console.log(import_chalk6.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`));
6254
+ console.log(
6255
+ import_chalk9.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6256
+ );
6257
+ await this.linkDependencies(worktreePath);
4214
6258
  return worktreePath;
4215
6259
  } catch (error) {
4216
- console.error(import_chalk6.default.red("Failed to create git worktree:"), error);
6260
+ console.error(import_chalk9.default.red("Failed to create git worktree:"), error);
4217
6261
  return null;
4218
6262
  }
4219
6263
  }
@@ -4228,18 +6272,25 @@ var GitWorktreeManager = class {
4228
6272
  ContextLoader,
4229
6273
  DEFAULT_MODELS,
4230
6274
  ENV_KEY_MAP,
6275
+ FRONTEND_FRAMEWORKS,
4231
6276
  GeminiProvider,
4232
6277
  GitWorktreeManager,
6278
+ MiMoProvider,
4233
6279
  OpenAICompatibleProvider,
4234
6280
  PROVIDER_CATALOG,
4235
6281
  SUPPORTED_PROVIDERS,
4236
6282
  SpecGenerator,
4237
6283
  SpecRefiner,
4238
6284
  TaskGenerator,
6285
+ buildTaskPrompt,
4239
6286
  createProvider,
6287
+ generateSpecWithTasks,
6288
+ isFrontendDeps,
4240
6289
  loadConstitution,
4241
6290
  loadTasksForSpec,
4242
6291
  printConstitutionHint,
4243
- printTasks
6292
+ printTaskProgress,
6293
+ printTasks,
6294
+ updateTaskStatus
4244
6295
  });
4245
6296
  //# sourceMappingURL=index.js.map