ai-spec-dev 0.25.0 → 0.28.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.
package/dist/index.js CHANGED
@@ -172,6 +172,67 @@ model ExampleModel {
172
172
 
173
173
  \u6839\u636E\u7528\u6237\u7684\u60F3\u6CD5\u548C\u9879\u76EE\u4E0A\u4E0B\u6587\u751F\u6210\u4E0A\u8FF0\u5B8C\u6574 Spec\u3002\u786E\u4FDD API \u8BBE\u8BA1\u4E0E\u73B0\u6709\u9879\u76EE\u7684\u8DEF\u7531\u98CE\u683C\u3001\u9519\u8BEF\u7801\u89C4\u8303\u4FDD\u6301\u4E00\u81F4\uFF0C\u6570\u636E\u6A21\u578B\u4E0E\u73B0\u6709 Prisma Schema \u534F\u8C03\u3002`;
174
174
 
175
+ // core/provider-utils.ts
176
+ var import_chalk = __toESM(require("chalk"));
177
+ var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
178
+ var ProviderError = class extends Error {
179
+ constructor(message, kind, originalError) {
180
+ super(message);
181
+ this.kind = kind;
182
+ this.originalError = originalError;
183
+ this.name = "ProviderError";
184
+ }
185
+ };
186
+ function classifyError(err, label) {
187
+ const e = err;
188
+ const status = e.status ?? e.response?.status;
189
+ if (status === 401 || status === 403)
190
+ return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
191
+ if (status === 429)
192
+ return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
193
+ if (e._timeout || e.message?.toLowerCase().includes("timed out"))
194
+ return new ProviderError(`Request timed out (${label})`, "timeout", err);
195
+ if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
196
+ return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
197
+ return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
198
+ }
199
+ function isRetryable(err) {
200
+ const e = err;
201
+ const status = e.status ?? e.response?.status;
202
+ if (status === 401 || status === 403) return false;
203
+ if (status === 429 || status !== void 0 && status >= 500) return true;
204
+ if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED") return true;
205
+ if (e.message?.toLowerCase().includes("timed out")) return true;
206
+ return true;
207
+ }
208
+ async function withReliability(fn, opts) {
209
+ const { retries = 2, timeoutMs = 9e4, label = "AI call", onRetry } = opts ?? {};
210
+ for (let attempt = 0; attempt <= retries; attempt++) {
211
+ try {
212
+ return await Promise.race([
213
+ fn(),
214
+ new Promise(
215
+ (_2, reject) => setTimeout(
216
+ () => reject(Object.assign(new Error(`timed out after ${timeoutMs / 1e3}s`), { _timeout: true })),
217
+ timeoutMs
218
+ )
219
+ )
220
+ ]);
221
+ } catch (err) {
222
+ if (!isRetryable(err) || attempt === retries) {
223
+ throw classifyError(err, label);
224
+ }
225
+ const waitMs = attempt === 0 ? 2e3 : 6e3;
226
+ console.warn(
227
+ import_chalk.default.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + import_chalk.default.gray(` \u2014 ${err.message}`)
228
+ );
229
+ onRetry?.(attempt + 1, err);
230
+ await sleep(waitMs);
231
+ }
232
+ }
233
+ throw new Error("unreachable");
234
+ }
235
+
175
236
  // core/spec-generator.ts
176
237
  function geminiRequestOptions() {
177
238
  const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
@@ -317,12 +378,17 @@ var GeminiProvider = class {
317
378
  this.modelName = modelName;
318
379
  }
319
380
  async generate(prompt, systemInstruction) {
320
- const model = this.genAI.getGenerativeModel(
321
- { model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
322
- geminiRequestOptions()
381
+ return withReliability(
382
+ async () => {
383
+ const model = this.genAI.getGenerativeModel(
384
+ { model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
385
+ geminiRequestOptions()
386
+ );
387
+ const result = await model.generateContent(prompt);
388
+ return result.response.text();
389
+ },
390
+ { label: `${this.providerName}/${this.modelName}` }
323
391
  );
324
- const result = await model.generateContent(prompt);
325
- return result.response.text();
326
392
  }
327
393
  };
328
394
  var ClaudeProvider = class {
@@ -334,15 +400,20 @@ var ClaudeProvider = class {
334
400
  this.modelName = modelName;
335
401
  }
336
402
  async generate(prompt, systemInstruction) {
337
- const message = await this.client.messages.create({
338
- model: this.modelName,
339
- max_tokens: 8192,
340
- ...systemInstruction ? { system: systemInstruction } : {},
341
- messages: [{ role: "user", content: prompt }]
342
- });
343
- const block = message.content[0];
344
- if (block.type === "text") return block.text;
345
- throw new Error("Unexpected response type from Claude API");
403
+ return withReliability(
404
+ async () => {
405
+ const message = await this.client.messages.create({
406
+ model: this.modelName,
407
+ max_tokens: 8192,
408
+ ...systemInstruction ? { system: systemInstruction } : {},
409
+ messages: [{ role: "user", content: prompt }]
410
+ });
411
+ const block = message.content[0];
412
+ if (block.type === "text") return block.text;
413
+ throw new Error("Unexpected response type from Claude API");
414
+ },
415
+ { label: `${this.providerName}/${this.modelName}` }
416
+ );
346
417
  }
347
418
  };
348
419
  var OpenAICompatibleProvider = class {
@@ -362,19 +433,24 @@ var OpenAICompatibleProvider = class {
362
433
  });
363
434
  }
364
435
  async generate(prompt, systemInstruction) {
365
- const messages = [];
366
- if (systemInstruction) {
367
- const isOSeries = /^o[13]/.test(this.modelName);
368
- const role = isOSeries ? "developer" : this.systemRole;
369
- messages.push({ role, content: systemInstruction });
370
- }
371
- messages.push({ role: "user", content: prompt });
372
- const completion = await this.client.chat.completions.create({
373
- model: this.modelName,
374
- messages,
375
- ...this.extraBody ? { extra_body: this.extraBody } : {}
376
- });
377
- return completion.choices[0].message.content ?? "";
436
+ return withReliability(
437
+ async () => {
438
+ const messages = [];
439
+ if (systemInstruction) {
440
+ const isOSeries = /^o[13]/.test(this.modelName);
441
+ const role = isOSeries ? "developer" : this.systemRole;
442
+ messages.push({ role, content: systemInstruction });
443
+ }
444
+ messages.push({ role: "user", content: prompt });
445
+ const completion = await this.client.chat.completions.create({
446
+ model: this.modelName,
447
+ messages,
448
+ ...this.extraBody ? { extra_body: this.extraBody } : {}
449
+ });
450
+ return completion.choices[0].message.content ?? "";
451
+ },
452
+ { label: `${this.providerName}/${this.modelName}` }
453
+ );
378
454
  }
379
455
  };
380
456
  var MiMoProvider = class {
@@ -387,32 +463,37 @@ var MiMoProvider = class {
387
463
  this.modelName = modelName;
388
464
  }
389
465
  async generate(prompt, systemInstruction) {
390
- const body = {
391
- model: this.modelName,
392
- max_tokens: 16384,
393
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
394
- top_p: 0.95,
395
- stream: false,
396
- temperature: 1,
397
- stop_sequences: null
398
- };
399
- if (systemInstruction) {
400
- body.system = systemInstruction;
401
- }
402
- const response = await import_axios.default.post(this.baseUrl, body, {
403
- headers: {
404
- "api-key": this.apiKey,
405
- "Content-Type": "application/json"
406
- }
407
- });
408
- const data = response.data;
409
- const blocks = data?.content ?? [];
410
- const textBlock = blocks.find((b) => b.type === "text");
411
- if (textBlock?.text) return textBlock.text;
412
- if (data?.stop_reason === "max_tokens") {
413
- throw new Error(`MiMo response truncated (max_tokens reached). The prompt may be too long. Try a shorter spec or switch to a model with larger context.`);
414
- }
415
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
466
+ return withReliability(
467
+ async () => {
468
+ const body = {
469
+ model: this.modelName,
470
+ max_tokens: 16384,
471
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
472
+ top_p: 0.95,
473
+ stream: false,
474
+ temperature: 1,
475
+ stop_sequences: null
476
+ };
477
+ if (systemInstruction) {
478
+ body.system = systemInstruction;
479
+ }
480
+ const response = await import_axios.default.post(this.baseUrl, body, {
481
+ headers: {
482
+ "api-key": this.apiKey,
483
+ "Content-Type": "application/json"
484
+ }
485
+ });
486
+ const data = response.data;
487
+ const blocks = data?.content ?? [];
488
+ const textBlock = blocks.find((b) => b.type === "text");
489
+ if (textBlock?.text) return textBlock.text;
490
+ if (data?.stop_reason === "max_tokens") {
491
+ 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.`);
492
+ }
493
+ throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
494
+ },
495
+ { label: `${this.providerName}/${this.modelName}` }
496
+ );
416
497
  }
417
498
  };
418
499
  function createProvider(providerName, apiKey, modelName) {
@@ -3868,10 +3949,10 @@ ${preview}
3868
3949
 
3869
3950
  // core/spec-refiner.ts
3870
3951
  var import_prompts = require("@inquirer/prompts");
3871
- var import_chalk2 = __toESM(require("chalk"));
3952
+ var import_chalk3 = __toESM(require("chalk"));
3872
3953
 
3873
3954
  // core/spec-versioning.ts
3874
- var import_chalk = __toESM(require("chalk"));
3955
+ var import_chalk2 = __toESM(require("chalk"));
3875
3956
  var fs4 = __toESM(require("fs-extra"));
3876
3957
  function computeDiff(oldText, newText) {
3877
3958
  const oldLines = oldText.split("\n");
@@ -3922,7 +4003,7 @@ function computeSimpleDiff(oldLines, newLines) {
3922
4003
  var CONTEXT_LINES = 3;
3923
4004
  function printDiff(diff) {
3924
4005
  if (diff.added === 0 && diff.removed === 0) {
3925
- console.log(import_chalk.default.gray(" (no changes)"));
4006
+ console.log(import_chalk2.default.gray(" (no changes)"));
3926
4007
  return;
3927
4008
  }
3928
4009
  const { lines } = diff;
@@ -3939,25 +4020,25 @@ function printDiff(diff) {
3939
4020
  let prevIdx = -2;
3940
4021
  for (const idx of sorted) {
3941
4022
  if (idx > prevIdx + 1 && prevIdx !== -2) {
3942
- console.log(import_chalk.default.cyan(" @@"));
4023
+ console.log(import_chalk2.default.cyan(" @@"));
3943
4024
  }
3944
4025
  const l = lines[idx];
3945
4026
  if (l.type === "added") {
3946
- console.log(import_chalk.default.green(` + ${l.content}`));
4027
+ console.log(import_chalk2.default.green(` + ${l.content}`));
3947
4028
  } else if (l.type === "removed") {
3948
- console.log(import_chalk.default.red(` - ${l.content}`));
4029
+ console.log(import_chalk2.default.red(` - ${l.content}`));
3949
4030
  } else {
3950
- console.log(import_chalk.default.gray(` ${l.content}`));
4031
+ console.log(import_chalk2.default.gray(` ${l.content}`));
3951
4032
  }
3952
4033
  prevIdx = idx;
3953
4034
  }
3954
4035
  }
3955
4036
  function printDiffSummary(diff, label) {
3956
4037
  const parts = [];
3957
- if (diff.added > 0) parts.push(import_chalk.default.green(`+${diff.added}`));
3958
- if (diff.removed > 0) parts.push(import_chalk.default.red(`-${diff.removed}`));
3959
- if (parts.length === 0) parts.push(import_chalk.default.gray("no change"));
3960
- console.log(import_chalk.default.bold(` ${label}: `) + parts.join(" ") + import_chalk.default.gray(` lines`));
4038
+ if (diff.added > 0) parts.push(import_chalk2.default.green(`+${diff.added}`));
4039
+ if (diff.removed > 0) parts.push(import_chalk2.default.red(`-${diff.removed}`));
4040
+ if (parts.length === 0) parts.push(import_chalk2.default.gray("no change"));
4041
+ console.log(import_chalk2.default.bold(` ${label}: `) + parts.join(" ") + import_chalk2.default.gray(` lines`));
3961
4042
  }
3962
4043
 
3963
4044
  // core/spec-refiner.ts
@@ -3969,16 +4050,16 @@ var SpecRefiner = class {
3969
4050
  let currentSpec = initialSpec;
3970
4051
  let round = 1;
3971
4052
  while (true) {
3972
- console.log(import_chalk2.default.cyan(`
4053
+ console.log(import_chalk3.default.cyan(`
3973
4054
  \u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
3974
- console.log(import_chalk2.default.gray(" Opening spec in editor. Save and close to continue."));
4055
+ console.log(import_chalk3.default.gray(" Opening spec in editor. Save and close to continue."));
3975
4056
  currentSpec = await (0, import_prompts.editor)({
3976
4057
  message: "Review and edit the spec:",
3977
4058
  default: currentSpec,
3978
4059
  postfix: ".md",
3979
4060
  waitForUserInput: false
3980
4061
  });
3981
- console.log(import_chalk2.default.green(" \u2714 Spec saved."));
4062
+ console.log(import_chalk3.default.green(" \u2714 Spec saved."));
3982
4063
  const action = await (0, import_prompts.select)({
3983
4064
  message: "What would you like to do?",
3984
4065
  choices: [
@@ -3991,7 +4072,7 @@ var SpecRefiner = class {
3991
4072
  break;
3992
4073
  }
3993
4074
  if (action === "ai") {
3994
- console.log(import_chalk2.default.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
4075
+ console.log(import_chalk3.default.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
3995
4076
  try {
3996
4077
  const improved = await this.provider.generate(
3997
4078
  `Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
@@ -4001,30 +4082,30 @@ Output ONLY the improved markdown spec, nothing else.
4001
4082
  ${currentSpec}`,
4002
4083
  "You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
4003
4084
  );
4004
- console.log(import_chalk2.default.yellow("\n AI has suggested improvements. Opening diff in editor..."));
4085
+ console.log(import_chalk3.default.yellow("\n AI has suggested improvements. Opening diff in editor..."));
4005
4086
  const acceptImproved = await (0, import_prompts.confirm)({
4006
4087
  message: "Accept AI improvements? (opens editor so you can review first)",
4007
4088
  default: true
4008
4089
  });
4009
4090
  if (acceptImproved) {
4010
4091
  const diff = computeDiff(currentSpec, improved);
4011
- console.log(import_chalk2.default.cyan("\n \u2500\u2500 AI Changes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4092
+ console.log(import_chalk3.default.cyan("\n \u2500\u2500 AI Changes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4012
4093
  printDiffSummary(diff, "AI edits");
4013
4094
  printDiff(diff);
4014
- console.log(import_chalk2.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
4095
+ console.log(import_chalk3.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
4015
4096
  currentSpec = await (0, import_prompts.editor)({
4016
4097
  message: "Review AI-improved spec (edit if needed, then save):",
4017
4098
  default: improved,
4018
4099
  postfix: ".md",
4019
4100
  waitForUserInput: false
4020
4101
  });
4021
- console.log(import_chalk2.default.green(" \u2714 AI-improved spec accepted."));
4102
+ console.log(import_chalk3.default.green(" \u2714 AI-improved spec accepted."));
4022
4103
  } else {
4023
- console.log(import_chalk2.default.gray(" AI improvements discarded. Keeping your version."));
4104
+ console.log(import_chalk3.default.gray(" AI improvements discarded. Keeping your version."));
4024
4105
  }
4025
4106
  } catch (err) {
4026
- console.error(import_chalk2.default.red(" AI improvement failed:"), err);
4027
- console.log(import_chalk2.default.gray(" Continuing with current spec."));
4107
+ console.error(import_chalk3.default.red(" AI improvement failed:"), err);
4108
+ console.log(import_chalk3.default.gray(" Continuing with current spec."));
4028
4109
  }
4029
4110
  }
4030
4111
  round++;
@@ -4034,10 +4115,10 @@ ${currentSpec}`,
4034
4115
  };
4035
4116
 
4036
4117
  // core/code-generator.ts
4037
- var import_chalk6 = __toESM(require("chalk"));
4118
+ var import_chalk8 = __toESM(require("chalk"));
4038
4119
  var import_child_process = require("child_process");
4039
4120
  var path6 = __toESM(require("path"));
4040
- var fs8 = __toESM(require("fs-extra"));
4121
+ var fs10 = __toESM(require("fs-extra"));
4041
4122
 
4042
4123
  // prompts/codegen.prompt.ts
4043
4124
  var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
@@ -4262,9 +4343,51 @@ Actionable, concrete improvements.
4262
4343
  Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
4263
4344
 
4264
4345
  Be specific. Reference actual code, not vague principles.`;
4346
+ var reviewImpactComplexitySystemPrompt = `You are a Senior Staff Engineer assessing the RISK and MAINTAINABILITY of a code change.
4347
+
4348
+ Two previous review passes have already covered architecture compliance and implementation correctness. Do NOT repeat their findings.
4349
+
4350
+ Your job is to answer exactly two questions:
4351
+
4352
+ ---
4353
+
4354
+ ## \u{1F30A} \u5F71\u54CD\u9762\u8BC4\u4F30 (Impact Assessment)
4355
+
4356
+ Evaluate what this change touches beyond its own files:
4357
+
4358
+ 1. **\u76F4\u63A5\u5F71\u54CD\u6587\u4EF6** \u2014 List the files created or modified.
4359
+ 2. **\u95F4\u63A5\u5F71\u54CD\u8303\u56F4** \u2014 Which existing modules, API consumers, or downstream services will be affected? (e.g., shared utilities modified, public interfaces changed, database schema altered)
4360
+ 3. **\u7834\u574F\u6027\u53D8\u66F4 (Breaking Changes)** \u2014 Are there changes requiring coordinated updates elsewhere?
4361
+ - API endpoint signature changes (path, method, required params)
4362
+ - Database schema changes (renamed columns, dropped fields, new NOT NULL constraints)
4363
+ - Config / env variable changes
4364
+ - Exported function/type renames
4365
+ - If none: state "\u65E0\u7834\u574F\u6027\u53D8\u66F4"
4366
+ 4. **\u5F71\u54CD\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
4367
+ - \u4F4E: isolated to new files, no shared interface changes
4368
+ - \u4E2D: modifies shared utilities or existing public interfaces, backwards compatible
4369
+ - \u9AD8: breaking changes, or touches core auth / payment / data integrity paths
4370
+
4371
+ ---
4372
+
4373
+ ## \u{1F9EE} \u4EE3\u7801\u590D\u6742\u5EA6\u8BC4\u4F30 (Complexity Assessment)
4374
+
4375
+ Evaluate how hard the new code will be to understand and change in the future:
4376
+
4377
+ 1. **\u8BA4\u77E5\u590D\u6742\u5EA6\u70ED\u70B9** \u2014 Identify the 1-3 most complex functions/methods. For each, explain WHY (deep nesting, multiple early returns, implicit state mutation, mixed abstraction levels).
4378
+ 2. **\u8026\u5408\u5EA6\u5206\u6790** \u2014 How tightly is the new code coupled to existing modules? Are dependencies injected or hardcoded? Any circular dependency risks?
4379
+ 3. **\u53EF\u7EF4\u62A4\u6027\u98CE\u9669** \u2014 Flag patterns that will cause pain when requirements change:
4380
+ - Magic numbers / hardcoded strings that should be constants
4381
+ - Business logic buried in framework lifecycle hooks or constructors
4382
+ - Implicit temporal coupling (functions that must be called in a specific order)
4383
+ 4. **\u590D\u6742\u5EA6\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
4384
+
4385
+ ---
4386
+
4387
+ Format using ONLY these two sections. Be specific. Reference file names and line ranges where relevant.`;
4265
4388
 
4266
4389
  // core/task-generator.ts
4267
- var import_chalk3 = __toESM(require("chalk"));
4390
+ var import_chalk4 = __toESM(require("chalk"));
4268
4391
  var fs5 = __toESM(require("fs-extra"));
4269
4392
  var path3 = __toESM(require("path"));
4270
4393
 
@@ -4418,31 +4541,36 @@ function parseTasks(raw) {
4418
4541
  }
4419
4542
  function printTasks(tasks) {
4420
4543
  const layerColors = {
4421
- data: import_chalk3.default.magenta,
4422
- infra: import_chalk3.default.gray,
4423
- service: import_chalk3.default.blue,
4424
- api: import_chalk3.default.cyan,
4425
- view: import_chalk3.default.yellow,
4426
- route: import_chalk3.default.white,
4427
- test: import_chalk3.default.green
4544
+ data: import_chalk4.default.magenta,
4545
+ infra: import_chalk4.default.gray,
4546
+ service: import_chalk4.default.blue,
4547
+ api: import_chalk4.default.cyan,
4548
+ view: import_chalk4.default.yellow,
4549
+ route: import_chalk4.default.white,
4550
+ test: import_chalk4.default.green
4428
4551
  };
4429
- console.log(import_chalk3.default.bold(`
4552
+ console.log(import_chalk4.default.bold(`
4430
4553
  Tasks (${tasks.length}):`));
4431
4554
  for (const task of tasks) {
4432
- const color = layerColors[task.layer] ?? import_chalk3.default.white;
4555
+ const color = layerColors[task.layer] ?? import_chalk4.default.white;
4433
4556
  const badge = color(`[${task.layer}]`);
4434
- const prio = task.priority === "high" ? import_chalk3.default.red("\u25CF") : task.priority === "medium" ? import_chalk3.default.yellow("\u25CF") : import_chalk3.default.gray("\u25CF");
4435
- console.log(` ${prio} ${import_chalk3.default.bold(task.id)} ${badge} ${task.title}`);
4557
+ const prio = task.priority === "high" ? import_chalk4.default.red("\u25CF") : task.priority === "medium" ? import_chalk4.default.yellow("\u25CF") : import_chalk4.default.gray("\u25CF");
4558
+ console.log(` ${prio} ${import_chalk4.default.bold(task.id)} ${badge} ${task.title}`);
4436
4559
  }
4437
4560
  }
4438
4561
  async function loadTasksForSpec(specFilePath) {
4439
4562
  const base = path3.basename(specFilePath, ".md");
4440
4563
  const dir = path3.dirname(specFilePath);
4441
4564
  const tasksFile = path3.join(dir, `${base}-tasks.json`);
4442
- if (await fs5.pathExists(tasksFile)) {
4443
- return fs5.readJson(tasksFile);
4565
+ if (!await fs5.pathExists(tasksFile)) return null;
4566
+ try {
4567
+ return await fs5.readJson(tasksFile);
4568
+ } catch {
4569
+ console.warn(
4570
+ import_chalk4.default.yellow(` \u26A0 Tasks file is corrupt or unreadable (${path3.basename(tasksFile)}).`) + import_chalk4.default.gray(` Re-run \`ai-spec tasks <spec>\` to regenerate.`)
4571
+ );
4572
+ return null;
4444
4573
  }
4445
- return null;
4446
4574
  }
4447
4575
  async function updateTaskStatus(specFilePath, taskId, status) {
4448
4576
  const tasks = await loadTasksForSpec(specFilePath);
@@ -4456,13 +4584,13 @@ async function updateTaskStatus(specFilePath, taskId, status) {
4456
4584
  }
4457
4585
 
4458
4586
  // core/dsl-extractor.ts
4459
- var import_chalk5 = __toESM(require("chalk"));
4587
+ var import_chalk6 = __toESM(require("chalk"));
4460
4588
  var fs6 = __toESM(require("fs-extra"));
4461
4589
  var path4 = __toESM(require("path"));
4462
4590
  var import_prompts2 = require("@inquirer/prompts");
4463
4591
 
4464
4592
  // core/dsl-validator.ts
4465
- var import_chalk4 = __toESM(require("chalk"));
4593
+ var import_chalk5 = __toESM(require("chalk"));
4466
4594
  var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
4467
4595
  var MAX_MODELS = 50;
4468
4596
  var MAX_FIELDS_PER_MODEL = 100;
@@ -5033,7 +5161,7 @@ ${preview}`);
5033
5161
  } catch {
5034
5162
  }
5035
5163
  }
5036
- const httpImportRegex = /^import\s+(?:\w+|\{[^}]+\})\s+from\s+['"](@\/[^'"]+|axios|ky)['"]/m;
5164
+ const httpImportRegex = /^import(?!\s+type)\s+(?:[\w*]+|\{[^}]+\})\s+from\s+['"]((?:@{1,2}|~|#)[/\\][^'"]+|\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*|axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)['"]/im;
5037
5165
  for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
5038
5166
  try {
5039
5167
  const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
@@ -5046,31 +5174,51 @@ ${preview}`);
5046
5174
  }
5047
5175
  }
5048
5176
  const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
5049
- const paginationInterfaceRegex = new RegExp(
5050
- `(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
5051
- "s"
5052
- );
5053
- const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
5054
5177
  for (const relPath of ctx.existingApiFiles) {
5055
5178
  if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
5056
5179
  try {
5057
5180
  const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
5058
5181
  if (!paginationFieldNames.some((f) => content.includes(f))) continue;
5059
- const interfaceMatch = content.match(paginationInterfaceRegex);
5060
- if (!interfaceMatch) continue;
5061
- const interfaceName = interfaceMatch[1];
5062
- const fnRegex = new RegExp(
5063
- `export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${interfaceName}[^)]*\\)[\\s\\S]*?\\n\\}`,
5064
- ""
5065
- );
5066
- const fnMatch = content.match(fnRegex);
5067
- if (fnMatch) {
5182
+ const lines = content.split("\n");
5183
+ let interfaceName = "";
5184
+ let interfaceBlock = "";
5185
+ for (let i = 0; i < lines.length; i++) {
5186
+ const m = lines[i].match(/(?:interface|type)\s+(\w*(?:Params|Query|Request|Filter|Page)\w*)\s*[={<]/);
5187
+ if (!m) continue;
5188
+ const blockLines = [];
5189
+ let depth = 0;
5190
+ for (let j2 = i; j2 < Math.min(i + 40, lines.length); j2++) {
5191
+ blockLines.push(lines[j2]);
5192
+ depth += (lines[j2].match(/\{/g) ?? []).length;
5193
+ depth -= (lines[j2].match(/\}/g) ?? []).length;
5194
+ if (depth === 0 && j2 > i) break;
5195
+ }
5196
+ const blockText = blockLines.join("\n");
5197
+ if (!paginationFieldNames.some((f) => new RegExp(`\\b${f}\\b`).test(blockText))) continue;
5198
+ interfaceName = m[1];
5199
+ interfaceBlock = blockText;
5200
+ break;
5201
+ }
5202
+ if (!interfaceName) continue;
5203
+ for (let i = 0; i < lines.length; i++) {
5204
+ const line = lines[i];
5205
+ if (!/export\s+(async\s+)?(function|const)/.test(line)) continue;
5206
+ if (!line.includes(interfaceName)) continue;
5207
+ const fnLines = [];
5208
+ let depth = 0;
5209
+ for (let j2 = i; j2 < Math.min(i + 30, lines.length); j2++) {
5210
+ fnLines.push(lines[j2]);
5211
+ depth += (lines[j2].match(/\{/g) ?? []).length;
5212
+ depth -= (lines[j2].match(/\}/g) ?? []).length;
5213
+ if (depth === 0 && j2 > i) break;
5214
+ }
5068
5215
  ctx.paginationExample = `// From ${relPath}
5069
- ${interfaceMatch[0]}
5216
+ ${interfaceBlock}
5070
5217
 
5071
- ${fnMatch[0]}`;
5218
+ ${fnLines.join("\n")}`;
5072
5219
  break;
5073
5220
  }
5221
+ if (ctx.paginationExample) break;
5074
5222
  } catch {
5075
5223
  }
5076
5224
  }
@@ -5283,6 +5431,21 @@ Shared component structure patterns:`);
5283
5431
  return lines.join("\n");
5284
5432
  }
5285
5433
 
5434
+ // core/run-snapshot.ts
5435
+ var fs8 = __toESM(require("fs-extra"));
5436
+ var _activeSnapshot = null;
5437
+ function getActiveSnapshot() {
5438
+ return _activeSnapshot;
5439
+ }
5440
+
5441
+ // core/run-logger.ts
5442
+ var fs9 = __toESM(require("fs-extra"));
5443
+ var import_chalk7 = __toESM(require("chalk"));
5444
+ var _activeLogger = null;
5445
+ function getActiveLogger() {
5446
+ return _activeLogger;
5447
+ }
5448
+
5286
5449
  // core/code-generator.ts
5287
5450
  function buildSharedConfigSection(context) {
5288
5451
  if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
@@ -5449,13 +5612,13 @@ var CodeGenerator = class {
5449
5612
  let effectiveMode = this.mode;
5450
5613
  if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
5451
5614
  console.log(
5452
- import_chalk6.default.yellow(
5615
+ import_chalk8.default.yellow(
5453
5616
  `
5454
5617
  \u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
5455
5618
  )
5456
5619
  );
5457
- console.log(import_chalk6.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
5458
- console.log(import_chalk6.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5620
+ console.log(import_chalk8.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
5621
+ console.log(import_chalk8.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5459
5622
  `));
5460
5623
  effectiveMode = "api";
5461
5624
  }
@@ -5480,16 +5643,16 @@ var CodeGenerator = class {
5480
5643
  }
5481
5644
  }
5482
5645
  async runClaudeCode(specFilePath, workingDir, options = {}) {
5483
- console.log(import_chalk6.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5646
+ console.log(import_chalk8.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5484
5647
  if (!this.isClaudeCLIAvailable()) {
5485
- console.log(import_chalk6.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5486
- console.log(import_chalk6.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
5648
+ console.log(import_chalk8.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5649
+ console.log(import_chalk8.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
5487
5650
  return this.runPlanMode(specFilePath);
5488
5651
  }
5489
5652
  const rtkAvailable = isRtkAvailable();
5490
5653
  const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
5491
5654
  if (rtkAvailable) {
5492
- console.log(import_chalk6.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
5655
+ console.log(import_chalk8.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
5493
5656
  }
5494
5657
  const tasks = await loadTasksForSpec(specFilePath);
5495
5658
  if (options.auto && tasks && tasks.length > 0) {
@@ -5503,29 +5666,29 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5503
5666
  Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
5504
5667
  const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
5505
5668
  const promptFile = path6.join(workingDir, ".claude-prompt.txt");
5506
- await fs8.writeFile(promptFile, promptContent, "utf-8");
5669
+ await fs10.writeFile(promptFile, promptContent, "utf-8");
5507
5670
  if (options.auto) {
5508
- console.log(import_chalk6.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5509
- console.log(import_chalk6.default.gray(` Spec: ${specFilePath}`));
5671
+ console.log(import_chalk8.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5672
+ console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
5510
5673
  try {
5511
5674
  (0, import_child_process.execSync)(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
5512
5675
  cwd: workingDir,
5513
5676
  stdio: "inherit"
5514
5677
  });
5515
- console.log(import_chalk6.default.green("\n \u2714 Claude Code completed."));
5678
+ console.log(import_chalk8.default.green("\n \u2714 Claude Code completed."));
5516
5679
  } catch {
5517
- console.log(import_chalk6.default.yellow("\n Claude Code exited. Check output above."));
5680
+ console.log(import_chalk8.default.yellow("\n Claude Code exited. Check output above."));
5518
5681
  }
5519
5682
  } else {
5520
- console.log(import_chalk6.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
5521
- console.log(import_chalk6.default.gray(` Spec: ${specFilePath}`));
5522
- if (tasks) console.log(import_chalk6.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
5523
- console.log(import_chalk6.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
5683
+ console.log(import_chalk8.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
5684
+ console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
5685
+ if (tasks) console.log(import_chalk8.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
5686
+ console.log(import_chalk8.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
5524
5687
  try {
5525
5688
  (0, import_child_process.execSync)(claudeCmd, { cwd: workingDir, stdio: "inherit" });
5526
- console.log(import_chalk6.default.green("\n \u2714 Claude Code session completed."));
5689
+ console.log(import_chalk8.default.green("\n \u2714 Claude Code session completed."));
5527
5690
  } catch {
5528
- console.log(import_chalk6.default.yellow("\n Claude Code session ended. Continuing workflow."));
5691
+ console.log(import_chalk8.default.yellow("\n Claude Code session ended. Continuing workflow."));
5529
5692
  }
5530
5693
  }
5531
5694
  }
@@ -5538,10 +5701,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5538
5701
  const pending = tasks.filter((t) => t.status !== "done");
5539
5702
  const doneCount = tasks.length - pending.length;
5540
5703
  if (options.resume && doneCount > 0) {
5541
- console.log(import_chalk6.default.cyan(`
5704
+ console.log(import_chalk8.default.cyan(`
5542
5705
  Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
5543
5706
  } else {
5544
- console.log(import_chalk6.default.cyan(`
5707
+ console.log(import_chalk8.default.cyan(`
5545
5708
  Incremental mode: ${tasks.length} tasks`));
5546
5709
  }
5547
5710
  let completed = doneCount;
@@ -5569,32 +5732,32 @@ Implement ONLY this task. Do not implement other tasks.`;
5569
5732
  completed++;
5570
5733
  } catch {
5571
5734
  taskStatus = "failed";
5572
- console.log(import_chalk6.default.yellow(`
5735
+ console.log(import_chalk8.default.yellow(`
5573
5736
  \u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
5574
5737
  }
5575
5738
  await updateTaskStatus(specFilePath, task.id, taskStatus);
5576
5739
  }
5577
5740
  const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
5578
5741
  console.log(
5579
- import_chalk6.default.bold(
5742
+ import_chalk8.default.bold(
5580
5743
  `
5581
- ${successCount === tasks.length ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
5744
+ ${successCount === tasks.length ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
5582
5745
  )
5583
5746
  );
5584
5747
  }
5585
5748
  // ── Mode: api ─────────────────────────────────────────────────────────────
5586
5749
  async runApiMode(specFilePath, workingDir, context, options = {}) {
5587
5750
  console.log(
5588
- import_chalk6.default.blue(
5751
+ import_chalk8.default.blue(
5589
5752
  `
5590
5753
  \u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
5591
5754
  )
5592
5755
  );
5593
5756
  const systemPrompt = getCodeGenSystemPrompt(options.repoType);
5594
5757
  if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
5595
- console.log(import_chalk6.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
5758
+ console.log(import_chalk8.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
5596
5759
  }
5597
- const spec = await fs8.readFile(specFilePath, "utf-8");
5760
+ const spec = await fs10.readFile(specFilePath, "utf-8");
5598
5761
  const constitutionSection = context?.constitution ? `
5599
5762
  === Project Constitution (MUST follow) ===
5600
5763
  ${context.constitution}
@@ -5610,7 +5773,7 @@ ${buildDslContextSection(dsl)}
5610
5773
  if (dsl) {
5611
5774
  const cmpCount = dsl.components?.length ?? 0;
5612
5775
  const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
5613
- console.log(import_chalk6.default.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
5776
+ console.log(import_chalk8.default.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
5614
5777
  }
5615
5778
  const isFrontend = isFrontendDeps(context?.dependencies ?? []);
5616
5779
  let frontendSection = "";
@@ -5619,13 +5782,13 @@ ${buildDslContextSection(dsl)}
5619
5782
  frontendSection = `
5620
5783
  ${buildFrontendContextSection(fctx)}
5621
5784
  `;
5622
- console.log(import_chalk6.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
5785
+ console.log(import_chalk8.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
5623
5786
  }
5624
5787
  const tasks = await loadTasksForSpec(specFilePath);
5625
5788
  if (tasks && tasks.length > 0) {
5626
5789
  return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
5627
5790
  }
5628
- console.log(import_chalk6.default.gray(" [1/2] Planning implementation files..."));
5791
+ console.log(import_chalk8.default.gray(" [1/2] Planning implementation files..."));
5629
5792
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
5630
5793
 
5631
5794
  IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
@@ -5648,18 +5811,18 @@ Output ONLY a valid JSON array:
5648
5811
  const planResponse = await this.provider.generate(planPrompt, systemPrompt);
5649
5812
  filePlan = parseJsonArray(planResponse);
5650
5813
  } catch (err) {
5651
- console.error(import_chalk6.default.red(" Failed to generate file plan:"), err);
5814
+ console.error(import_chalk8.default.red(" Failed to generate file plan:"), err);
5652
5815
  }
5653
5816
  if (filePlan.length === 0) {
5654
- console.log(import_chalk6.default.yellow(" Could not determine file plan. Falling back to plan mode."));
5817
+ console.log(import_chalk8.default.yellow(" Could not determine file plan. Falling back to plan mode."));
5655
5818
  await this.runPlanMode(specFilePath);
5656
5819
  return [];
5657
5820
  }
5658
- console.log(import_chalk6.default.cyan(`
5821
+ console.log(import_chalk8.default.cyan(`
5659
5822
  Plan: ${filePlan.length} file(s) to process`));
5660
5823
  filePlan.forEach((item) => {
5661
- const icon = item.action === "create" ? import_chalk6.default.green("+") : import_chalk6.default.yellow("~");
5662
- console.log(` ${icon} ${item.file}: ${import_chalk6.default.gray(item.description)}`);
5824
+ const icon = item.action === "create" ? import_chalk8.default.green("+") : import_chalk8.default.yellow("~");
5825
+ console.log(` ${icon} ${item.file}: ${import_chalk8.default.gray(item.description)}`);
5663
5826
  });
5664
5827
  const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
5665
5828
  return files;
@@ -5668,13 +5831,13 @@ Output ONLY a valid JSON array:
5668
5831
  const pendingTasks = tasks.filter((t) => t.status !== "done");
5669
5832
  const doneCount = tasks.length - pendingTasks.length;
5670
5833
  if (options.resume && doneCount > 0) {
5671
- console.log(import_chalk6.default.cyan(`
5672
- Task-based generation (resume): ${tasks.length} tasks (${import_chalk6.default.green(doneCount + " already done")}, skipping)`));
5834
+ console.log(import_chalk8.default.cyan(`
5835
+ Task-based generation (resume): ${tasks.length} tasks (${import_chalk8.default.green(doneCount + " already done")}, skipping)`));
5673
5836
  } else if (doneCount > 0) {
5674
- console.log(import_chalk6.default.cyan(`
5675
- Task-based generation: ${tasks.length} tasks (${import_chalk6.default.green(doneCount + " already done")}, resuming from checkpoint)`));
5837
+ console.log(import_chalk8.default.cyan(`
5838
+ Task-based generation: ${tasks.length} tasks (${import_chalk8.default.green(doneCount + " already done")}, resuming from checkpoint)`));
5676
5839
  } else {
5677
- console.log(import_chalk6.default.cyan(`
5840
+ console.log(import_chalk8.default.cyan(`
5678
5841
  Task-based generation: ${tasks.length} tasks`));
5679
5842
  }
5680
5843
  const sharedConfigPaths = new Set(
@@ -5706,9 +5869,9 @@ Output ONLY a valid JSON array:
5706
5869
  const pct = Math.round(completedTasks / tasks.length * 100);
5707
5870
  const barWidth = 20;
5708
5871
  const filled = Math.round(pct / 100 * barWidth);
5709
- const bar = import_chalk6.default.green("\u2588".repeat(filled)) + import_chalk6.default.gray("\u2591".repeat(barWidth - filled));
5872
+ const bar = import_chalk8.default.green("\u2588".repeat(filled)) + import_chalk8.default.gray("\u2591".repeat(barWidth - filled));
5710
5873
  console.log(
5711
- import_chalk6.default.bold(`
5874
+ import_chalk8.default.bold(`
5712
5875
  [${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
5713
5876
  );
5714
5877
  } else {
@@ -5716,12 +5879,12 @@ Output ONLY a valid JSON array:
5716
5879
  }
5717
5880
  const executeTask = async (task, batchIsParallel) => {
5718
5881
  if (task.filesToTouch.length === 0) {
5719
- if (!batchIsParallel) console.log(import_chalk6.default.gray(" No files specified, skipping."));
5882
+ if (!batchIsParallel) console.log(import_chalk8.default.gray(" No files specified, skipping."));
5720
5883
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
5721
5884
  }
5722
5885
  const filePlan = await Promise.all(
5723
5886
  task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
5724
- const exists = await fs8.pathExists(path6.join(workingDir, f));
5887
+ const exists = await fs10.pathExists(path6.join(workingDir, f));
5725
5888
  return {
5726
5889
  file: f,
5727
5890
  action: exists ? "modify" : "create",
@@ -5761,7 +5924,7 @@ ${taskContext}`,
5761
5924
  const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
5762
5925
  if (isCodeFile || isViewFile) {
5763
5926
  try {
5764
- const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs8.readFile(path6.join(workingDir, writtenFile), "utf-8");
5927
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path6.join(workingDir, writtenFile), "utf-8");
5765
5928
  generatedFileCache.set(writtenFile, content);
5766
5929
  } catch {
5767
5930
  }
@@ -5773,7 +5936,12 @@ ${taskContext}`,
5773
5936
  const layerResults = [];
5774
5937
  for (const batch of taskBatches) {
5775
5938
  const batchIsParallel = batch.length > 1;
5776
- const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
5939
+ const batchResultPromises = batch.map(
5940
+ (task) => executeTask(task, batchIsParallel).catch((err) => {
5941
+ console.log(import_chalk8.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
5942
+ return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
5943
+ })
5944
+ );
5777
5945
  const batchResults = await Promise.all(batchResultPromises);
5778
5946
  layerResults.push(...batchResults);
5779
5947
  await updateCacheFromBatch(batchResults);
@@ -5786,14 +5954,14 @@ ${taskContext}`,
5786
5954
  totalFiles += result.total;
5787
5955
  allGeneratedFiles.push(...result.files);
5788
5956
  if (isParallel) {
5789
- const icon = result.success === result.total ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!");
5957
+ const icon = result.success === result.total ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!");
5790
5958
  const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
5791
5959
  console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
5792
5960
  }
5793
5961
  const taskStatus = result.success === result.total ? "done" : "failed";
5794
5962
  await updateTaskStatus(specFilePath, result.task.id, taskStatus);
5795
5963
  if (taskStatus === "failed") {
5796
- console.log(import_chalk6.default.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
5964
+ console.log(import_chalk8.default.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
5797
5965
  }
5798
5966
  }
5799
5967
  completedTasks += layerTasks.length;
@@ -5808,7 +5976,7 @@ ${taskContext}`,
5808
5976
  if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
5809
5977
  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.`;
5810
5978
  }
5811
- console.log(import_chalk6.default.gray(`
5979
+ console.log(import_chalk8.default.gray(`
5812
5980
  + updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
5813
5981
  const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
5814
5982
  await this.generateFiles(
@@ -5826,17 +5994,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
5826
5994
  }
5827
5995
  }
5828
5996
  console.log(
5829
- import_chalk6.default.bold(
5997
+ import_chalk8.default.bold(
5830
5998
  `
5831
- ${totalSuccess === totalFiles ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
5999
+ ${totalSuccess === totalFiles ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
5832
6000
  )
5833
6001
  );
5834
6002
  return allGeneratedFiles;
5835
6003
  }
5836
6004
  async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
5837
- const prefix = taskLabel ? ` [${import_chalk6.default.cyan(taskLabel)}] ` : " ";
6005
+ const prefix = taskLabel ? ` [${import_chalk8.default.cyan(taskLabel)}] ` : " ";
5838
6006
  if (!taskLabel) {
5839
- console.log(import_chalk6.default.gray(`
6007
+ console.log(import_chalk8.default.gray(`
5840
6008
  Generating ${filePlan.length} file(s)...`));
5841
6009
  }
5842
6010
  let successCount = 0;
@@ -5844,8 +6012,8 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
5844
6012
  for (const item of filePlan) {
5845
6013
  const fullPath = path6.join(workingDir, item.file);
5846
6014
  let existingContent = "";
5847
- if (await fs8.pathExists(fullPath)) {
5848
- existingContent = await fs8.readFile(fullPath, "utf-8");
6015
+ if (await fs10.pathExists(fullPath)) {
6016
+ existingContent = await fs10.readFile(fullPath, "utf-8");
5849
6017
  }
5850
6018
  const codePrompt = `Implement this file.
5851
6019
 
@@ -5860,19 +6028,21 @@ ${existingContent || "Output only the complete file content."}`;
5860
6028
  try {
5861
6029
  const raw = await this.provider.generate(codePrompt, systemPrompt);
5862
6030
  const fileContent = stripCodeFences(raw);
5863
- await fs8.ensureDir(path6.dirname(fullPath));
5864
- await fs8.writeFile(fullPath, fileContent, "utf-8");
5865
- console.log(`${prefix}${existingContent ? import_chalk6.default.yellow("~") : import_chalk6.default.green("+")} ${import_chalk6.default.bold(item.file)} ${import_chalk6.default.green("\u2714")}`);
6031
+ await getActiveSnapshot()?.snapshotFile(fullPath);
6032
+ await fs10.ensureDir(path6.dirname(fullPath));
6033
+ await fs10.writeFile(fullPath, fileContent, "utf-8");
6034
+ getActiveLogger()?.fileWritten(item.file);
6035
+ console.log(`${prefix}${existingContent ? import_chalk8.default.yellow("~") : import_chalk8.default.green("+")} ${import_chalk8.default.bold(item.file)} ${import_chalk8.default.green("\u2714")}`);
5866
6036
  successCount++;
5867
6037
  writtenFiles.push(item.file);
5868
6038
  } catch (err) {
5869
- console.log(`${prefix}${import_chalk6.default.red("\u2718")} ${import_chalk6.default.bold(item.file)} \u2014 ${import_chalk6.default.red(err.message)}`);
6039
+ console.log(`${prefix}${import_chalk8.default.red("\u2718")} ${import_chalk8.default.bold(item.file)} \u2014 ${import_chalk8.default.red(err.message)}`);
5870
6040
  }
5871
6041
  }
5872
6042
  if (!taskLabel) {
5873
6043
  console.log(
5874
- import_chalk6.default.bold(
5875
- ` ${successCount === filePlan.length ? import_chalk6.default.green("\u2714") : import_chalk6.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
6044
+ import_chalk8.default.bold(
6045
+ ` ${successCount === filePlan.length ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
5876
6046
  )
5877
6047
  );
5878
6048
  }
@@ -5880,8 +6050,8 @@ ${existingContent || "Output only the complete file content."}`;
5880
6050
  }
5881
6051
  // ── Mode: plan ─────────────────────────────────────────────────────────────
5882
6052
  async runPlanMode(specFilePath) {
5883
- console.log(import_chalk6.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5884
- const spec = await fs8.readFile(specFilePath, "utf-8");
6053
+ console.log(import_chalk8.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6054
+ const spec = await fs10.readFile(specFilePath, "utf-8");
5885
6055
  const plan = await this.provider.generate(
5886
6056
  `Create a detailed, step-by-step implementation plan for the following feature spec.
5887
6057
  Be specific about:
@@ -5893,7 +6063,7 @@ Be specific about:
5893
6063
  ${spec}`,
5894
6064
  "You are a senior developer creating an actionable implementation guide."
5895
6065
  );
5896
- console.log(import_chalk6.default.cyan("\n") + plan);
6066
+ console.log(import_chalk8.default.cyan("\n") + plan);
5897
6067
  }
5898
6068
  };
5899
6069
  function topoSortLayerTasks(tasks) {
@@ -5944,32 +6114,32 @@ function printTaskProgress(completed, total, task, mode) {
5944
6114
  const pct = total > 0 ? Math.round(completed / total * 100) : 0;
5945
6115
  const barWidth = 20;
5946
6116
  const filled = Math.round(pct / 100 * barWidth);
5947
- const bar = import_chalk6.default.green("\u2588".repeat(filled)) + import_chalk6.default.gray("\u2591".repeat(barWidth - filled));
6117
+ const bar = import_chalk8.default.green("\u2588".repeat(filled)) + import_chalk8.default.gray("\u2591".repeat(barWidth - filled));
5948
6118
  const icon = LAYER_ICONS[task.layer] ?? " ";
5949
6119
  if (mode === "skip") {
5950
6120
  console.log(
5951
- import_chalk6.default.gray(`
6121
+ import_chalk8.default.gray(`
5952
6122
  [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
5953
6123
  );
5954
6124
  } else {
5955
6125
  console.log(
5956
- import_chalk6.default.bold(`
6126
+ import_chalk8.default.bold(`
5957
6127
  [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
5958
6128
  );
5959
6129
  }
5960
6130
  }
5961
6131
 
5962
6132
  // core/reviewer.ts
5963
- var import_chalk7 = __toESM(require("chalk"));
6133
+ var import_chalk9 = __toESM(require("chalk"));
5964
6134
  var import_child_process2 = require("child_process");
5965
6135
  var path7 = __toESM(require("path"));
5966
- var fs9 = __toESM(require("fs-extra"));
6136
+ var fs11 = __toESM(require("fs-extra"));
5967
6137
  var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5968
6138
  async function loadReviewHistory(projectRoot) {
5969
6139
  const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
5970
6140
  try {
5971
- if (await fs9.pathExists(historyPath)) {
5972
- return await fs9.readJson(historyPath);
6141
+ if (await fs11.pathExists(historyPath)) {
6142
+ return await fs11.readJson(historyPath);
5973
6143
  }
5974
6144
  } catch {
5975
6145
  }
@@ -5980,7 +6150,7 @@ async function appendReviewHistory(projectRoot, entry) {
5980
6150
  const existing = await loadReviewHistory(projectRoot);
5981
6151
  const updated = [...existing, entry].slice(-20);
5982
6152
  try {
5983
- await fs9.writeJson(historyPath, updated, { spaces: 2 });
6153
+ await fs11.writeJson(historyPath, updated, { spaces: 2 });
5984
6154
  } catch {
5985
6155
  }
5986
6156
  }
@@ -5988,6 +6158,14 @@ function extractScore(reviewText) {
5988
6158
  const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
5989
6159
  return match ? parseFloat(match[1]) : 0;
5990
6160
  }
6161
+ function extractImpactLevel(reviewText) {
6162
+ const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
6163
+ return match ? match[1] : void 0;
6164
+ }
6165
+ function extractComplexityLevel(reviewText) {
6166
+ const match = reviewText.match(/复杂度等级[::]\s*(低|中|高)/);
6167
+ return match ? match[1] : void 0;
6168
+ }
5991
6169
  function extractTopIssues(reviewText) {
5992
6170
  const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
5993
6171
  return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
@@ -6033,15 +6211,14 @@ var CodeReviewer = class {
6033
6211
  };
6034
6212
  }
6035
6213
  /**
6036
- * Two-pass review:
6214
+ * Three-pass review:
6037
6215
  * Pass 1 — architecture (spec compliance, layer separation, auth)
6038
6216
  * Pass 2 — implementation details (validation, error handling, edge cases)
6039
6217
  * + historical issue recurrence check
6040
- *
6041
- * Falls back to single-pass if the two-pass flag is not set.
6218
+ * Pass 3 — impact assessment + code complexity
6042
6219
  */
6043
- async runTwoPassReview(specContent, codeContext, specFile) {
6044
- console.log(import_chalk7.default.gray(" Pass 1/2: Architecture review..."));
6220
+ async runThreePassReview(specContent, codeContext, specFile) {
6221
+ console.log(import_chalk9.default.gray(" Pass 1/3: Architecture review..."));
6045
6222
  const archPrompt = `Review the architecture of this change.
6046
6223
 
6047
6224
  === Feature Spec ===
@@ -6050,7 +6227,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6050
6227
  === Code ===
6051
6228
  ${codeContext}`;
6052
6229
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6053
- console.log(import_chalk7.default.gray(" Pass 2/2: Implementation review..."));
6230
+ console.log(import_chalk9.default.gray(" Pass 2/3: Implementation review..."));
6054
6231
  const history = await loadReviewHistory(this.projectRoot);
6055
6232
  const historyContext = buildHistoryContext(history);
6056
6233
  const implPrompt = `Review the implementation details of this change.
@@ -6065,61 +6242,85 @@ ${codeContext}
6065
6242
  ${archReview}
6066
6243
  ${historyContext}`;
6067
6244
  const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
6068
- const combined = `${archReview}
6245
+ console.log(import_chalk9.default.gray(" Pass 3/3: Impact & complexity assessment..."));
6246
+ const impactPrompt = `Assess the impact and complexity of this change.
6069
6247
 
6070
- ${"\u2500".repeat(52)}
6248
+ === Feature Spec ===
6249
+ ${specContent || "(No spec \u2014 review for general code quality)"}
6071
6250
 
6251
+ === Code ===
6252
+ ${codeContext}
6253
+
6254
+ === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
6255
+ ${archReview}
6256
+
6257
+ === Implementation Review (Pass 2 \u2014 do NOT repeat) ===
6072
6258
  ${implReview}`;
6259
+ const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
6260
+ const sep = "\u2500".repeat(52);
6261
+ const combined = `${archReview}
6262
+
6263
+ ${sep}
6264
+
6265
+ ${implReview}
6266
+
6267
+ ${sep}
6268
+
6269
+ ${impactReview}`;
6073
6270
  const score = extractScore(implReview) || extractScore(archReview);
6074
6271
  const topIssues = extractTopIssues(implReview);
6272
+ const impactLevel = extractImpactLevel(impactReview);
6273
+ const complexityLevel = extractComplexityLevel(impactReview);
6075
6274
  if (score > 0 && specFile) {
6076
6275
  await appendReviewHistory(this.projectRoot, {
6077
6276
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
6078
6277
  specFile: path7.relative(this.projectRoot, specFile),
6079
6278
  score,
6080
- topIssues
6279
+ topIssues,
6280
+ ...impactLevel ? { impactLevel } : {},
6281
+ ...complexityLevel ? { complexityLevel } : {}
6081
6282
  });
6082
6283
  }
6083
6284
  return combined;
6084
6285
  }
6085
6286
  async reviewCode(specContent, specFile) {
6086
- console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6287
+ console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6087
6288
  const diff = this.getGitDiff();
6088
6289
  if (!diff.trim()) {
6089
6290
  console.log(
6090
- import_chalk7.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
6291
+ import_chalk9.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
6091
6292
  );
6092
- console.log(import_chalk7.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6293
+ console.log(import_chalk9.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6093
6294
  return "No changes";
6094
6295
  }
6095
6296
  const { files, added, removed } = this.getDiffStats(diff);
6096
6297
  console.log(
6097
- import_chalk7.default.gray(` Diff: ${files} file(s), ${import_chalk7.default.green("+" + added)} ${import_chalk7.default.red("-" + removed)}`)
6298
+ import_chalk9.default.gray(` Diff: ${files} file(s), ${import_chalk9.default.green("+" + added)} ${import_chalk9.default.red("-" + removed)}`)
6098
6299
  );
6099
6300
  console.log(
6100
- import_chalk7.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6301
+ import_chalk9.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6101
6302
  );
6102
6303
  const codeContext = diff.slice(0, 1e4);
6103
- const reviewResult = await this.runTwoPassReview(specContent, codeContext, specFile);
6104
- console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6304
+ const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
6305
+ console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6105
6306
  console.log(reviewResult);
6106
- console.log(import_chalk7.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6307
+ console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6107
6308
  return reviewResult;
6108
6309
  }
6109
6310
  /**
6110
6311
  * Review directly from generated file contents (for api mode where git diff is empty).
6111
6312
  */
6112
6313
  async reviewFiles(specContent, filePaths, workingDir, specFile) {
6113
- console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6114
- console.log(import_chalk7.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6314
+ console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6315
+ console.log(import_chalk9.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6115
6316
  console.log(
6116
- import_chalk7.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6317
+ import_chalk9.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6117
6318
  );
6118
6319
  let filesSection = "";
6119
6320
  for (const filePath of filePaths) {
6120
6321
  const fullPath = path7.join(workingDir, filePath);
6121
6322
  try {
6122
- const content = await fs9.readFile(fullPath, "utf-8");
6323
+ const content = await fs11.readFile(fullPath, "utf-8");
6123
6324
  filesSection += `
6124
6325
 
6125
6326
  === ${filePath} ===
@@ -6133,33 +6334,33 @@ ${content.slice(0, 3e3)}`;
6133
6334
  (file not found)`;
6134
6335
  }
6135
6336
  }
6136
- const reviewResult = await this.runTwoPassReview(specContent, filesSection, specFile);
6137
- console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6337
+ const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
6338
+ console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6138
6339
  console.log(reviewResult);
6139
- console.log(import_chalk7.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6340
+ console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6140
6341
  return reviewResult;
6141
6342
  }
6142
6343
  /** Print score trend from history (last N reviews) */
6143
6344
  async printScoreTrend(limit = 5) {
6144
6345
  const history = await loadReviewHistory(this.projectRoot);
6145
6346
  if (history.length === 0) {
6146
- console.log(import_chalk7.default.gray(" No review history yet."));
6347
+ console.log(import_chalk9.default.gray(" No review history yet."));
6147
6348
  return;
6148
6349
  }
6149
6350
  const recent = history.slice(-limit);
6150
- console.log(import_chalk7.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6351
+ console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6151
6352
  for (const entry of recent) {
6152
6353
  const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
6153
- const color = entry.score >= 8 ? import_chalk7.default.green : entry.score >= 6 ? import_chalk7.default.yellow : import_chalk7.default.red;
6354
+ const color = entry.score >= 8 ? import_chalk9.default.green : entry.score >= 6 ? import_chalk9.default.yellow : import_chalk9.default.red;
6154
6355
  console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path7.basename(entry.specFile)}`);
6155
6356
  }
6156
- console.log(import_chalk7.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6357
+ console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6157
6358
  }
6158
6359
  };
6159
6360
 
6160
6361
  // core/constitution-generator.ts
6161
- var import_chalk8 = __toESM(require("chalk"));
6162
- var fs10 = __toESM(require("fs-extra"));
6362
+ var import_chalk10 = __toESM(require("chalk"));
6363
+ var fs12 = __toESM(require("fs-extra"));
6163
6364
  var path8 = __toESM(require("path"));
6164
6365
 
6165
6366
  // prompts/constitution.prompt.ts
@@ -6241,7 +6442,7 @@ var ConstitutionGenerator = class {
6241
6442
  }
6242
6443
  async saveConstitution(projectRoot, content) {
6243
6444
  const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6244
- await fs10.writeFile(filePath, content, "utf-8");
6445
+ await fs12.writeFile(filePath, content, "utf-8");
6245
6446
  return filePath;
6246
6447
  }
6247
6448
  };
@@ -6300,15 +6501,15 @@ ${sections.join("\n")}
6300
6501
  }
6301
6502
  async function loadConstitution(projectRoot) {
6302
6503
  const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6303
- if (await fs10.pathExists(filePath)) {
6304
- return fs10.readFile(filePath, "utf-8");
6504
+ if (await fs12.pathExists(filePath)) {
6505
+ return fs12.readFile(filePath, "utf-8");
6305
6506
  }
6306
6507
  return void 0;
6307
6508
  }
6308
6509
  function printConstitutionHint(exists) {
6309
6510
  if (!exists) {
6310
6511
  console.log(
6311
- import_chalk8.default.yellow(
6512
+ import_chalk10.default.yellow(
6312
6513
  " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6313
6514
  )
6314
6515
  );
@@ -6353,8 +6554,8 @@ function parseSpecAndTasks(raw) {
6353
6554
  // git/worktree.ts
6354
6555
  var import_child_process3 = require("child_process");
6355
6556
  var path9 = __toESM(require("path"));
6356
- var fs11 = __toESM(require("fs-extra"));
6357
- var import_chalk9 = __toESM(require("chalk"));
6557
+ var fs13 = __toESM(require("fs-extra"));
6558
+ var import_chalk11 = __toESM(require("chalk"));
6358
6559
  var GitWorktreeManager = class {
6359
6560
  constructor(baseDir) {
6360
6561
  this.baseDir = baseDir;
@@ -6381,30 +6582,30 @@ var GitWorktreeManager = class {
6381
6582
  for (const dir of candidates) {
6382
6583
  const src = path9.join(this.baseDir, dir);
6383
6584
  const dest = path9.join(worktreePath, dir);
6384
- if (!await fs11.pathExists(src)) continue;
6385
- if (await fs11.pathExists(dest)) continue;
6585
+ if (!await fs13.pathExists(src)) continue;
6586
+ if (await fs13.pathExists(dest)) continue;
6386
6587
  try {
6387
- await fs11.ensureSymlink(src, dest, "dir");
6388
- console.log(import_chalk9.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6588
+ await fs13.ensureSymlink(src, dest, "dir");
6589
+ console.log(import_chalk11.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6389
6590
  } catch (err) {
6390
- console.log(import_chalk9.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6391
- console.log(import_chalk9.default.yellow(` Run \`npm install\` inside the worktree manually.`));
6591
+ console.log(import_chalk11.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6592
+ console.log(import_chalk11.default.yellow(` Run \`npm install\` inside the worktree manually.`));
6392
6593
  }
6393
6594
  }
6394
6595
  }
6395
6596
  async createWorktree(idea) {
6396
6597
  if (!this.isGitRepo()) {
6397
- console.log(import_chalk9.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6598
+ console.log(import_chalk11.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6398
6599
  return null;
6399
6600
  }
6400
6601
  const featureName = this.sanitizeFeatureName(idea);
6401
6602
  const branchName = `feature/${featureName}`;
6402
6603
  const repoName = path9.basename(this.baseDir);
6403
6604
  const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
6404
- console.log(import_chalk9.default.cyan(`
6605
+ console.log(import_chalk11.default.cyan(`
6405
6606
  --- Setting up Git Worktree ---`));
6406
- if (await fs11.pathExists(worktreePath)) {
6407
- console.log(import_chalk9.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6607
+ if (await fs13.pathExists(worktreePath)) {
6608
+ console.log(import_chalk11.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6408
6609
  await this.linkDependencies(worktreePath);
6409
6610
  return worktreePath;
6410
6611
  }
@@ -6418,7 +6619,7 @@ var GitWorktreeManager = class {
6418
6619
  branchExists = true;
6419
6620
  } catch {
6420
6621
  }
6421
- console.log(import_chalk9.default.gray(`Creating worktree at: ${worktreePath}`));
6622
+ console.log(import_chalk11.default.gray(`Creating worktree at: ${worktreePath}`));
6422
6623
  if (branchExists) {
6423
6624
  (0, import_child_process3.execSync)(`git worktree add "${worktreePath}" ${branchName}`, {
6424
6625
  cwd: this.baseDir,
@@ -6431,12 +6632,12 @@ var GitWorktreeManager = class {
6431
6632
  });
6432
6633
  }
6433
6634
  console.log(
6434
- import_chalk9.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6635
+ import_chalk11.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6435
6636
  );
6436
6637
  await this.linkDependencies(worktreePath);
6437
6638
  return worktreePath;
6438
6639
  } catch (error) {
6439
- console.error(import_chalk9.default.red("Failed to create git worktree:"), error);
6640
+ console.error(import_chalk11.default.red("Failed to create git worktree:"), error);
6440
6641
  return null;
6441
6642
  }
6442
6643
  }