ai-spec-dev 0.1.0 → 0.14.1

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