ai-spec-dev 0.24.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.mjs CHANGED
@@ -109,6 +109,67 @@ model ExampleModel {
109
109
 
110
110
  \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`;
111
111
 
112
+ // core/provider-utils.ts
113
+ import chalk from "chalk";
114
+ var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
115
+ var ProviderError = class extends Error {
116
+ constructor(message, kind, originalError) {
117
+ super(message);
118
+ this.kind = kind;
119
+ this.originalError = originalError;
120
+ this.name = "ProviderError";
121
+ }
122
+ };
123
+ function classifyError(err, label) {
124
+ const e = err;
125
+ const status = e.status ?? e.response?.status;
126
+ if (status === 401 || status === 403)
127
+ return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
128
+ if (status === 429)
129
+ return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
130
+ if (e._timeout || e.message?.toLowerCase().includes("timed out"))
131
+ return new ProviderError(`Request timed out (${label})`, "timeout", err);
132
+ if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
133
+ return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
134
+ return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
135
+ }
136
+ function isRetryable(err) {
137
+ const e = err;
138
+ const status = e.status ?? e.response?.status;
139
+ if (status === 401 || status === 403) return false;
140
+ if (status === 429 || status !== void 0 && status >= 500) return true;
141
+ if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED") return true;
142
+ if (e.message?.toLowerCase().includes("timed out")) return true;
143
+ return true;
144
+ }
145
+ async function withReliability(fn, opts) {
146
+ const { retries = 2, timeoutMs = 9e4, label = "AI call", onRetry } = opts ?? {};
147
+ for (let attempt = 0; attempt <= retries; attempt++) {
148
+ try {
149
+ return await Promise.race([
150
+ fn(),
151
+ new Promise(
152
+ (_2, reject) => setTimeout(
153
+ () => reject(Object.assign(new Error(`timed out after ${timeoutMs / 1e3}s`), { _timeout: true })),
154
+ timeoutMs
155
+ )
156
+ )
157
+ ]);
158
+ } catch (err) {
159
+ if (!isRetryable(err) || attempt === retries) {
160
+ throw classifyError(err, label);
161
+ }
162
+ const waitMs = attempt === 0 ? 2e3 : 6e3;
163
+ console.warn(
164
+ chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
165
+ );
166
+ onRetry?.(attempt + 1, err);
167
+ await sleep(waitMs);
168
+ }
169
+ }
170
+ throw new Error("unreachable");
171
+ }
172
+
112
173
  // core/spec-generator.ts
113
174
  function geminiRequestOptions() {
114
175
  const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
@@ -254,12 +315,17 @@ var GeminiProvider = class {
254
315
  this.modelName = modelName;
255
316
  }
256
317
  async generate(prompt, systemInstruction) {
257
- const model = this.genAI.getGenerativeModel(
258
- { model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
259
- geminiRequestOptions()
318
+ return withReliability(
319
+ async () => {
320
+ const model = this.genAI.getGenerativeModel(
321
+ { model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
322
+ geminiRequestOptions()
323
+ );
324
+ const result = await model.generateContent(prompt);
325
+ return result.response.text();
326
+ },
327
+ { label: `${this.providerName}/${this.modelName}` }
260
328
  );
261
- const result = await model.generateContent(prompt);
262
- return result.response.text();
263
329
  }
264
330
  };
265
331
  var ClaudeProvider = class {
@@ -271,15 +337,20 @@ var ClaudeProvider = class {
271
337
  this.modelName = modelName;
272
338
  }
273
339
  async generate(prompt, systemInstruction) {
274
- const message = await this.client.messages.create({
275
- model: this.modelName,
276
- max_tokens: 8192,
277
- ...systemInstruction ? { system: systemInstruction } : {},
278
- messages: [{ role: "user", content: prompt }]
279
- });
280
- const block = message.content[0];
281
- if (block.type === "text") return block.text;
282
- throw new Error("Unexpected response type from Claude API");
340
+ return withReliability(
341
+ async () => {
342
+ const message = await this.client.messages.create({
343
+ model: this.modelName,
344
+ max_tokens: 8192,
345
+ ...systemInstruction ? { system: systemInstruction } : {},
346
+ messages: [{ role: "user", content: prompt }]
347
+ });
348
+ const block = message.content[0];
349
+ if (block.type === "text") return block.text;
350
+ throw new Error("Unexpected response type from Claude API");
351
+ },
352
+ { label: `${this.providerName}/${this.modelName}` }
353
+ );
283
354
  }
284
355
  };
285
356
  var OpenAICompatibleProvider = class {
@@ -299,19 +370,24 @@ var OpenAICompatibleProvider = class {
299
370
  });
300
371
  }
301
372
  async generate(prompt, systemInstruction) {
302
- const messages = [];
303
- if (systemInstruction) {
304
- const isOSeries = /^o[13]/.test(this.modelName);
305
- const role = isOSeries ? "developer" : this.systemRole;
306
- messages.push({ role, content: systemInstruction });
307
- }
308
- messages.push({ role: "user", content: prompt });
309
- const completion = await this.client.chat.completions.create({
310
- model: this.modelName,
311
- messages,
312
- ...this.extraBody ? { extra_body: this.extraBody } : {}
313
- });
314
- return completion.choices[0].message.content ?? "";
373
+ return withReliability(
374
+ async () => {
375
+ const messages = [];
376
+ if (systemInstruction) {
377
+ const isOSeries = /^o[13]/.test(this.modelName);
378
+ const role = isOSeries ? "developer" : this.systemRole;
379
+ messages.push({ role, content: systemInstruction });
380
+ }
381
+ messages.push({ role: "user", content: prompt });
382
+ const completion = await this.client.chat.completions.create({
383
+ model: this.modelName,
384
+ messages,
385
+ ...this.extraBody ? { extra_body: this.extraBody } : {}
386
+ });
387
+ return completion.choices[0].message.content ?? "";
388
+ },
389
+ { label: `${this.providerName}/${this.modelName}` }
390
+ );
315
391
  }
316
392
  };
317
393
  var MiMoProvider = class {
@@ -324,32 +400,37 @@ var MiMoProvider = class {
324
400
  this.modelName = modelName;
325
401
  }
326
402
  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)}`);
403
+ return withReliability(
404
+ async () => {
405
+ const body = {
406
+ model: this.modelName,
407
+ max_tokens: 16384,
408
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
409
+ top_p: 0.95,
410
+ stream: false,
411
+ temperature: 1,
412
+ stop_sequences: null
413
+ };
414
+ if (systemInstruction) {
415
+ body.system = systemInstruction;
416
+ }
417
+ const response = await axios.post(this.baseUrl, body, {
418
+ headers: {
419
+ "api-key": this.apiKey,
420
+ "Content-Type": "application/json"
421
+ }
422
+ });
423
+ const data = response.data;
424
+ const blocks = data?.content ?? [];
425
+ const textBlock = blocks.find((b) => b.type === "text");
426
+ if (textBlock?.text) return textBlock.text;
427
+ if (data?.stop_reason === "max_tokens") {
428
+ 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.`);
429
+ }
430
+ throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
431
+ },
432
+ { label: `${this.providerName}/${this.modelName}` }
433
+ );
353
434
  }
354
435
  };
355
436
  function createProvider(providerName, apiKey, modelName) {
@@ -3805,10 +3886,10 @@ ${preview}
3805
3886
 
3806
3887
  // core/spec-refiner.ts
3807
3888
  import { editor, confirm, select } from "@inquirer/prompts";
3808
- import chalk2 from "chalk";
3889
+ import chalk3 from "chalk";
3809
3890
 
3810
3891
  // core/spec-versioning.ts
3811
- import chalk from "chalk";
3892
+ import chalk2 from "chalk";
3812
3893
  import * as fs4 from "fs-extra";
3813
3894
  function computeDiff(oldText, newText) {
3814
3895
  const oldLines = oldText.split("\n");
@@ -3859,7 +3940,7 @@ function computeSimpleDiff(oldLines, newLines) {
3859
3940
  var CONTEXT_LINES = 3;
3860
3941
  function printDiff(diff) {
3861
3942
  if (diff.added === 0 && diff.removed === 0) {
3862
- console.log(chalk.gray(" (no changes)"));
3943
+ console.log(chalk2.gray(" (no changes)"));
3863
3944
  return;
3864
3945
  }
3865
3946
  const { lines } = diff;
@@ -3876,25 +3957,25 @@ function printDiff(diff) {
3876
3957
  let prevIdx = -2;
3877
3958
  for (const idx of sorted) {
3878
3959
  if (idx > prevIdx + 1 && prevIdx !== -2) {
3879
- console.log(chalk.cyan(" @@"));
3960
+ console.log(chalk2.cyan(" @@"));
3880
3961
  }
3881
3962
  const l = lines[idx];
3882
3963
  if (l.type === "added") {
3883
- console.log(chalk.green(` + ${l.content}`));
3964
+ console.log(chalk2.green(` + ${l.content}`));
3884
3965
  } else if (l.type === "removed") {
3885
- console.log(chalk.red(` - ${l.content}`));
3966
+ console.log(chalk2.red(` - ${l.content}`));
3886
3967
  } else {
3887
- console.log(chalk.gray(` ${l.content}`));
3968
+ console.log(chalk2.gray(` ${l.content}`));
3888
3969
  }
3889
3970
  prevIdx = idx;
3890
3971
  }
3891
3972
  }
3892
3973
  function printDiffSummary(diff, label) {
3893
3974
  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`));
3975
+ if (diff.added > 0) parts.push(chalk2.green(`+${diff.added}`));
3976
+ if (diff.removed > 0) parts.push(chalk2.red(`-${diff.removed}`));
3977
+ if (parts.length === 0) parts.push(chalk2.gray("no change"));
3978
+ console.log(chalk2.bold(` ${label}: `) + parts.join(" ") + chalk2.gray(` lines`));
3898
3979
  }
3899
3980
 
3900
3981
  // core/spec-refiner.ts
@@ -3906,16 +3987,16 @@ var SpecRefiner = class {
3906
3987
  let currentSpec = initialSpec;
3907
3988
  let round = 1;
3908
3989
  while (true) {
3909
- console.log(chalk2.cyan(`
3990
+ console.log(chalk3.cyan(`
3910
3991
  \u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
3911
- console.log(chalk2.gray(" Opening spec in editor. Save and close to continue."));
3992
+ console.log(chalk3.gray(" Opening spec in editor. Save and close to continue."));
3912
3993
  currentSpec = await editor({
3913
3994
  message: "Review and edit the spec:",
3914
3995
  default: currentSpec,
3915
3996
  postfix: ".md",
3916
3997
  waitForUserInput: false
3917
3998
  });
3918
- console.log(chalk2.green(" \u2714 Spec saved."));
3999
+ console.log(chalk3.green(" \u2714 Spec saved."));
3919
4000
  const action = await select({
3920
4001
  message: "What would you like to do?",
3921
4002
  choices: [
@@ -3928,7 +4009,7 @@ var SpecRefiner = class {
3928
4009
  break;
3929
4010
  }
3930
4011
  if (action === "ai") {
3931
- console.log(chalk2.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
4012
+ console.log(chalk3.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
3932
4013
  try {
3933
4014
  const improved = await this.provider.generate(
3934
4015
  `Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
@@ -3938,30 +4019,30 @@ Output ONLY the improved markdown spec, nothing else.
3938
4019
  ${currentSpec}`,
3939
4020
  "You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
3940
4021
  );
3941
- console.log(chalk2.yellow("\n AI has suggested improvements. Opening diff in editor..."));
4022
+ console.log(chalk3.yellow("\n AI has suggested improvements. Opening diff in editor..."));
3942
4023
  const acceptImproved = await confirm({
3943
4024
  message: "Accept AI improvements? (opens editor so you can review first)",
3944
4025
  default: true
3945
4026
  });
3946
4027
  if (acceptImproved) {
3947
4028
  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"));
4029
+ console.log(chalk3.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
4030
  printDiffSummary(diff, "AI edits");
3950
4031
  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"));
4032
+ console.log(chalk3.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"));
3952
4033
  currentSpec = await editor({
3953
4034
  message: "Review AI-improved spec (edit if needed, then save):",
3954
4035
  default: improved,
3955
4036
  postfix: ".md",
3956
4037
  waitForUserInput: false
3957
4038
  });
3958
- console.log(chalk2.green(" \u2714 AI-improved spec accepted."));
4039
+ console.log(chalk3.green(" \u2714 AI-improved spec accepted."));
3959
4040
  } else {
3960
- console.log(chalk2.gray(" AI improvements discarded. Keeping your version."));
4041
+ console.log(chalk3.gray(" AI improvements discarded. Keeping your version."));
3961
4042
  }
3962
4043
  } catch (err) {
3963
- console.error(chalk2.red(" AI improvement failed:"), err);
3964
- console.log(chalk2.gray(" Continuing with current spec."));
4044
+ console.error(chalk3.red(" AI improvement failed:"), err);
4045
+ console.log(chalk3.gray(" Continuing with current spec."));
3965
4046
  }
3966
4047
  }
3967
4048
  round++;
@@ -3971,10 +4052,10 @@ ${currentSpec}`,
3971
4052
  };
3972
4053
 
3973
4054
  // core/code-generator.ts
3974
- import chalk6 from "chalk";
4055
+ import chalk8 from "chalk";
3975
4056
  import { execSync } from "child_process";
3976
4057
  import * as path6 from "path";
3977
- import * as fs8 from "fs-extra";
4058
+ import * as fs10 from "fs-extra";
3978
4059
 
3979
4060
  // prompts/codegen.prompt.ts
3980
4061
  var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
@@ -4199,9 +4280,51 @@ Actionable, concrete improvements.
4199
4280
  Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
4200
4281
 
4201
4282
  Be specific. Reference actual code, not vague principles.`;
4283
+ var reviewImpactComplexitySystemPrompt = `You are a Senior Staff Engineer assessing the RISK and MAINTAINABILITY of a code change.
4284
+
4285
+ Two previous review passes have already covered architecture compliance and implementation correctness. Do NOT repeat their findings.
4286
+
4287
+ Your job is to answer exactly two questions:
4288
+
4289
+ ---
4290
+
4291
+ ## \u{1F30A} \u5F71\u54CD\u9762\u8BC4\u4F30 (Impact Assessment)
4292
+
4293
+ Evaluate what this change touches beyond its own files:
4294
+
4295
+ 1. **\u76F4\u63A5\u5F71\u54CD\u6587\u4EF6** \u2014 List the files created or modified.
4296
+ 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)
4297
+ 3. **\u7834\u574F\u6027\u53D8\u66F4 (Breaking Changes)** \u2014 Are there changes requiring coordinated updates elsewhere?
4298
+ - API endpoint signature changes (path, method, required params)
4299
+ - Database schema changes (renamed columns, dropped fields, new NOT NULL constraints)
4300
+ - Config / env variable changes
4301
+ - Exported function/type renames
4302
+ - If none: state "\u65E0\u7834\u574F\u6027\u53D8\u66F4"
4303
+ 4. **\u5F71\u54CD\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
4304
+ - \u4F4E: isolated to new files, no shared interface changes
4305
+ - \u4E2D: modifies shared utilities or existing public interfaces, backwards compatible
4306
+ - \u9AD8: breaking changes, or touches core auth / payment / data integrity paths
4307
+
4308
+ ---
4309
+
4310
+ ## \u{1F9EE} \u4EE3\u7801\u590D\u6742\u5EA6\u8BC4\u4F30 (Complexity Assessment)
4311
+
4312
+ Evaluate how hard the new code will be to understand and change in the future:
4313
+
4314
+ 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).
4315
+ 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?
4316
+ 3. **\u53EF\u7EF4\u62A4\u6027\u98CE\u9669** \u2014 Flag patterns that will cause pain when requirements change:
4317
+ - Magic numbers / hardcoded strings that should be constants
4318
+ - Business logic buried in framework lifecycle hooks or constructors
4319
+ - Implicit temporal coupling (functions that must be called in a specific order)
4320
+ 4. **\u590D\u6742\u5EA6\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
4321
+
4322
+ ---
4323
+
4324
+ Format using ONLY these two sections. Be specific. Reference file names and line ranges where relevant.`;
4202
4325
 
4203
4326
  // core/task-generator.ts
4204
- import chalk3 from "chalk";
4327
+ import chalk4 from "chalk";
4205
4328
  import * as fs5 from "fs-extra";
4206
4329
  import * as path3 from "path";
4207
4330
 
@@ -4355,31 +4478,36 @@ function parseTasks(raw) {
4355
4478
  }
4356
4479
  function printTasks(tasks) {
4357
4480
  const layerColors = {
4358
- data: chalk3.magenta,
4359
- infra: chalk3.gray,
4360
- service: chalk3.blue,
4361
- api: chalk3.cyan,
4362
- view: chalk3.yellow,
4363
- route: chalk3.white,
4364
- test: chalk3.green
4481
+ data: chalk4.magenta,
4482
+ infra: chalk4.gray,
4483
+ service: chalk4.blue,
4484
+ api: chalk4.cyan,
4485
+ view: chalk4.yellow,
4486
+ route: chalk4.white,
4487
+ test: chalk4.green
4365
4488
  };
4366
- console.log(chalk3.bold(`
4489
+ console.log(chalk4.bold(`
4367
4490
  Tasks (${tasks.length}):`));
4368
4491
  for (const task of tasks) {
4369
- const color = layerColors[task.layer] ?? chalk3.white;
4492
+ const color = layerColors[task.layer] ?? chalk4.white;
4370
4493
  const badge = color(`[${task.layer}]`);
4371
- const prio = task.priority === "high" ? chalk3.red("\u25CF") : task.priority === "medium" ? chalk3.yellow("\u25CF") : chalk3.gray("\u25CF");
4372
- console.log(` ${prio} ${chalk3.bold(task.id)} ${badge} ${task.title}`);
4494
+ const prio = task.priority === "high" ? chalk4.red("\u25CF") : task.priority === "medium" ? chalk4.yellow("\u25CF") : chalk4.gray("\u25CF");
4495
+ console.log(` ${prio} ${chalk4.bold(task.id)} ${badge} ${task.title}`);
4373
4496
  }
4374
4497
  }
4375
4498
  async function loadTasksForSpec(specFilePath) {
4376
4499
  const base = path3.basename(specFilePath, ".md");
4377
4500
  const dir = path3.dirname(specFilePath);
4378
4501
  const tasksFile = path3.join(dir, `${base}-tasks.json`);
4379
- if (await fs5.pathExists(tasksFile)) {
4380
- return fs5.readJson(tasksFile);
4502
+ if (!await fs5.pathExists(tasksFile)) return null;
4503
+ try {
4504
+ return await fs5.readJson(tasksFile);
4505
+ } catch {
4506
+ console.warn(
4507
+ chalk4.yellow(` \u26A0 Tasks file is corrupt or unreadable (${path3.basename(tasksFile)}).`) + chalk4.gray(` Re-run \`ai-spec tasks <spec>\` to regenerate.`)
4508
+ );
4509
+ return null;
4381
4510
  }
4382
- return null;
4383
4511
  }
4384
4512
  async function updateTaskStatus(specFilePath, taskId, status) {
4385
4513
  const tasks = await loadTasksForSpec(specFilePath);
@@ -4393,13 +4521,13 @@ async function updateTaskStatus(specFilePath, taskId, status) {
4393
4521
  }
4394
4522
 
4395
4523
  // core/dsl-extractor.ts
4396
- import chalk5 from "chalk";
4524
+ import chalk6 from "chalk";
4397
4525
  import * as fs6 from "fs-extra";
4398
4526
  import * as path4 from "path";
4399
4527
  import { select as select2 } from "@inquirer/prompts";
4400
4528
 
4401
4529
  // core/dsl-validator.ts
4402
- import chalk4 from "chalk";
4530
+ import chalk5 from "chalk";
4403
4531
  var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
4404
4532
  var MAX_MODELS = 50;
4405
4533
  var MAX_FIELDS_PER_MODEL = 100;
@@ -4970,7 +5098,7 @@ ${preview}`);
4970
5098
  } catch {
4971
5099
  }
4972
5100
  }
4973
- const httpImportRegex = /^import\s+(?:\w+|\{[^}]+\})\s+from\s+['"](@\/[^'"]+|axios|ky)['"]/m;
5101
+ 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;
4974
5102
  for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
4975
5103
  try {
4976
5104
  const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
@@ -4983,31 +5111,51 @@ ${preview}`);
4983
5111
  }
4984
5112
  }
4985
5113
  const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
4986
- const paginationInterfaceRegex = new RegExp(
4987
- `(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
4988
- "s"
4989
- );
4990
- const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
4991
5114
  for (const relPath of ctx.existingApiFiles) {
4992
5115
  if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
4993
5116
  try {
4994
5117
  const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
4995
5118
  if (!paginationFieldNames.some((f) => content.includes(f))) continue;
4996
- const interfaceMatch = content.match(paginationInterfaceRegex);
4997
- if (!interfaceMatch) continue;
4998
- const interfaceName = interfaceMatch[1];
4999
- const fnRegex = new RegExp(
5000
- `export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${interfaceName}[^)]*\\)[\\s\\S]*?\\n\\}`,
5001
- ""
5002
- );
5003
- const fnMatch = content.match(fnRegex);
5004
- if (fnMatch) {
5119
+ const lines = content.split("\n");
5120
+ let interfaceName = "";
5121
+ let interfaceBlock = "";
5122
+ for (let i = 0; i < lines.length; i++) {
5123
+ const m = lines[i].match(/(?:interface|type)\s+(\w*(?:Params|Query|Request|Filter|Page)\w*)\s*[={<]/);
5124
+ if (!m) continue;
5125
+ const blockLines = [];
5126
+ let depth = 0;
5127
+ for (let j2 = i; j2 < Math.min(i + 40, lines.length); j2++) {
5128
+ blockLines.push(lines[j2]);
5129
+ depth += (lines[j2].match(/\{/g) ?? []).length;
5130
+ depth -= (lines[j2].match(/\}/g) ?? []).length;
5131
+ if (depth === 0 && j2 > i) break;
5132
+ }
5133
+ const blockText = blockLines.join("\n");
5134
+ if (!paginationFieldNames.some((f) => new RegExp(`\\b${f}\\b`).test(blockText))) continue;
5135
+ interfaceName = m[1];
5136
+ interfaceBlock = blockText;
5137
+ break;
5138
+ }
5139
+ if (!interfaceName) continue;
5140
+ for (let i = 0; i < lines.length; i++) {
5141
+ const line = lines[i];
5142
+ if (!/export\s+(async\s+)?(function|const)/.test(line)) continue;
5143
+ if (!line.includes(interfaceName)) continue;
5144
+ const fnLines = [];
5145
+ let depth = 0;
5146
+ for (let j2 = i; j2 < Math.min(i + 30, lines.length); j2++) {
5147
+ fnLines.push(lines[j2]);
5148
+ depth += (lines[j2].match(/\{/g) ?? []).length;
5149
+ depth -= (lines[j2].match(/\}/g) ?? []).length;
5150
+ if (depth === 0 && j2 > i) break;
5151
+ }
5005
5152
  ctx.paginationExample = `// From ${relPath}
5006
- ${interfaceMatch[0]}
5153
+ ${interfaceBlock}
5007
5154
 
5008
- ${fnMatch[0]}`;
5155
+ ${fnLines.join("\n")}`;
5009
5156
  break;
5010
5157
  }
5158
+ if (ctx.paginationExample) break;
5011
5159
  } catch {
5012
5160
  }
5013
5161
  }
@@ -5220,6 +5368,21 @@ Shared component structure patterns:`);
5220
5368
  return lines.join("\n");
5221
5369
  }
5222
5370
 
5371
+ // core/run-snapshot.ts
5372
+ import * as fs8 from "fs-extra";
5373
+ var _activeSnapshot = null;
5374
+ function getActiveSnapshot() {
5375
+ return _activeSnapshot;
5376
+ }
5377
+
5378
+ // core/run-logger.ts
5379
+ import * as fs9 from "fs-extra";
5380
+ import chalk7 from "chalk";
5381
+ var _activeLogger = null;
5382
+ function getActiveLogger() {
5383
+ return _activeLogger;
5384
+ }
5385
+
5223
5386
  // core/code-generator.ts
5224
5387
  function buildSharedConfigSection(context) {
5225
5388
  if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
@@ -5386,13 +5549,13 @@ var CodeGenerator = class {
5386
5549
  let effectiveMode = this.mode;
5387
5550
  if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
5388
5551
  console.log(
5389
- chalk6.yellow(
5552
+ chalk8.yellow(
5390
5553
  `
5391
5554
  \u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
5392
5555
  )
5393
5556
  );
5394
- 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`));
5395
- console.log(chalk6.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5557
+ console.log(chalk8.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
5558
+ console.log(chalk8.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5396
5559
  `));
5397
5560
  effectiveMode = "api";
5398
5561
  }
@@ -5417,16 +5580,16 @@ var CodeGenerator = class {
5417
5580
  }
5418
5581
  }
5419
5582
  async runClaudeCode(specFilePath, workingDir, options = {}) {
5420
- console.log(chalk6.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5583
+ console.log(chalk8.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5421
5584
  if (!this.isClaudeCLIAvailable()) {
5422
- console.log(chalk6.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5423
- console.log(chalk6.gray(" Install: npm install -g @anthropic-ai/claude-code"));
5585
+ console.log(chalk8.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5586
+ console.log(chalk8.gray(" Install: npm install -g @anthropic-ai/claude-code"));
5424
5587
  return this.runPlanMode(specFilePath);
5425
5588
  }
5426
5589
  const rtkAvailable = isRtkAvailable();
5427
5590
  const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
5428
5591
  if (rtkAvailable) {
5429
- console.log(chalk6.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
5592
+ console.log(chalk8.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
5430
5593
  }
5431
5594
  const tasks = await loadTasksForSpec(specFilePath);
5432
5595
  if (options.auto && tasks && tasks.length > 0) {
@@ -5440,29 +5603,29 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5440
5603
  Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
5441
5604
  const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
5442
5605
  const promptFile = path6.join(workingDir, ".claude-prompt.txt");
5443
- await fs8.writeFile(promptFile, promptContent, "utf-8");
5606
+ await fs10.writeFile(promptFile, promptContent, "utf-8");
5444
5607
  if (options.auto) {
5445
- console.log(chalk6.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5446
- console.log(chalk6.gray(` Spec: ${specFilePath}`));
5608
+ console.log(chalk8.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5609
+ console.log(chalk8.gray(` Spec: ${specFilePath}`));
5447
5610
  try {
5448
5611
  execSync(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
5449
5612
  cwd: workingDir,
5450
5613
  stdio: "inherit"
5451
5614
  });
5452
- console.log(chalk6.green("\n \u2714 Claude Code completed."));
5615
+ console.log(chalk8.green("\n \u2714 Claude Code completed."));
5453
5616
  } catch {
5454
- console.log(chalk6.yellow("\n Claude Code exited. Check output above."));
5617
+ console.log(chalk8.yellow("\n Claude Code exited. Check output above."));
5455
5618
  }
5456
5619
  } else {
5457
- console.log(chalk6.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
5458
- console.log(chalk6.gray(` Spec: ${specFilePath}`));
5459
- if (tasks) console.log(chalk6.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
5460
- console.log(chalk6.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
5620
+ console.log(chalk8.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
5621
+ console.log(chalk8.gray(` Spec: ${specFilePath}`));
5622
+ if (tasks) console.log(chalk8.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
5623
+ console.log(chalk8.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
5461
5624
  try {
5462
5625
  execSync(claudeCmd, { cwd: workingDir, stdio: "inherit" });
5463
- console.log(chalk6.green("\n \u2714 Claude Code session completed."));
5626
+ console.log(chalk8.green("\n \u2714 Claude Code session completed."));
5464
5627
  } catch {
5465
- console.log(chalk6.yellow("\n Claude Code session ended. Continuing workflow."));
5628
+ console.log(chalk8.yellow("\n Claude Code session ended. Continuing workflow."));
5466
5629
  }
5467
5630
  }
5468
5631
  }
@@ -5475,10 +5638,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5475
5638
  const pending = tasks.filter((t) => t.status !== "done");
5476
5639
  const doneCount = tasks.length - pending.length;
5477
5640
  if (options.resume && doneCount > 0) {
5478
- console.log(chalk6.cyan(`
5641
+ console.log(chalk8.cyan(`
5479
5642
  Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
5480
5643
  } else {
5481
- console.log(chalk6.cyan(`
5644
+ console.log(chalk8.cyan(`
5482
5645
  Incremental mode: ${tasks.length} tasks`));
5483
5646
  }
5484
5647
  let completed = doneCount;
@@ -5506,32 +5669,32 @@ Implement ONLY this task. Do not implement other tasks.`;
5506
5669
  completed++;
5507
5670
  } catch {
5508
5671
  taskStatus = "failed";
5509
- console.log(chalk6.yellow(`
5672
+ console.log(chalk8.yellow(`
5510
5673
  \u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
5511
5674
  }
5512
5675
  await updateTaskStatus(specFilePath, task.id, taskStatus);
5513
5676
  }
5514
5677
  const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
5515
5678
  console.log(
5516
- chalk6.bold(
5679
+ chalk8.bold(
5517
5680
  `
5518
- ${successCount === tasks.length ? chalk6.green("\u2714") : chalk6.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
5681
+ ${successCount === tasks.length ? chalk8.green("\u2714") : chalk8.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
5519
5682
  )
5520
5683
  );
5521
5684
  }
5522
5685
  // ── Mode: api ─────────────────────────────────────────────────────────────
5523
5686
  async runApiMode(specFilePath, workingDir, context, options = {}) {
5524
5687
  console.log(
5525
- chalk6.blue(
5688
+ chalk8.blue(
5526
5689
  `
5527
5690
  \u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
5528
5691
  )
5529
5692
  );
5530
5693
  const systemPrompt = getCodeGenSystemPrompt(options.repoType);
5531
5694
  if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
5532
- console.log(chalk6.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
5695
+ console.log(chalk8.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
5533
5696
  }
5534
- const spec = await fs8.readFile(specFilePath, "utf-8");
5697
+ const spec = await fs10.readFile(specFilePath, "utf-8");
5535
5698
  const constitutionSection = context?.constitution ? `
5536
5699
  === Project Constitution (MUST follow) ===
5537
5700
  ${context.constitution}
@@ -5547,7 +5710,7 @@ ${buildDslContextSection(dsl)}
5547
5710
  if (dsl) {
5548
5711
  const cmpCount = dsl.components?.length ?? 0;
5549
5712
  const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
5550
- console.log(chalk6.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
5713
+ console.log(chalk8.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
5551
5714
  }
5552
5715
  const isFrontend = isFrontendDeps(context?.dependencies ?? []);
5553
5716
  let frontendSection = "";
@@ -5556,13 +5719,13 @@ ${buildDslContextSection(dsl)}
5556
5719
  frontendSection = `
5557
5720
  ${buildFrontendContextSection(fctx)}
5558
5721
  `;
5559
- console.log(chalk6.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
5722
+ console.log(chalk8.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
5560
5723
  }
5561
5724
  const tasks = await loadTasksForSpec(specFilePath);
5562
5725
  if (tasks && tasks.length > 0) {
5563
5726
  return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
5564
5727
  }
5565
- console.log(chalk6.gray(" [1/2] Planning implementation files..."));
5728
+ console.log(chalk8.gray(" [1/2] Planning implementation files..."));
5566
5729
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
5567
5730
 
5568
5731
  IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
@@ -5585,18 +5748,18 @@ Output ONLY a valid JSON array:
5585
5748
  const planResponse = await this.provider.generate(planPrompt, systemPrompt);
5586
5749
  filePlan = parseJsonArray(planResponse);
5587
5750
  } catch (err) {
5588
- console.error(chalk6.red(" Failed to generate file plan:"), err);
5751
+ console.error(chalk8.red(" Failed to generate file plan:"), err);
5589
5752
  }
5590
5753
  if (filePlan.length === 0) {
5591
- console.log(chalk6.yellow(" Could not determine file plan. Falling back to plan mode."));
5754
+ console.log(chalk8.yellow(" Could not determine file plan. Falling back to plan mode."));
5592
5755
  await this.runPlanMode(specFilePath);
5593
5756
  return [];
5594
5757
  }
5595
- console.log(chalk6.cyan(`
5758
+ console.log(chalk8.cyan(`
5596
5759
  Plan: ${filePlan.length} file(s) to process`));
5597
5760
  filePlan.forEach((item) => {
5598
- const icon = item.action === "create" ? chalk6.green("+") : chalk6.yellow("~");
5599
- console.log(` ${icon} ${item.file}: ${chalk6.gray(item.description)}`);
5761
+ const icon = item.action === "create" ? chalk8.green("+") : chalk8.yellow("~");
5762
+ console.log(` ${icon} ${item.file}: ${chalk8.gray(item.description)}`);
5600
5763
  });
5601
5764
  const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
5602
5765
  return files;
@@ -5605,13 +5768,13 @@ Output ONLY a valid JSON array:
5605
5768
  const pendingTasks = tasks.filter((t) => t.status !== "done");
5606
5769
  const doneCount = tasks.length - pendingTasks.length;
5607
5770
  if (options.resume && doneCount > 0) {
5608
- console.log(chalk6.cyan(`
5609
- Task-based generation (resume): ${tasks.length} tasks (${chalk6.green(doneCount + " already done")}, skipping)`));
5771
+ console.log(chalk8.cyan(`
5772
+ Task-based generation (resume): ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, skipping)`));
5610
5773
  } else if (doneCount > 0) {
5611
- console.log(chalk6.cyan(`
5612
- Task-based generation: ${tasks.length} tasks (${chalk6.green(doneCount + " already done")}, resuming from checkpoint)`));
5774
+ console.log(chalk8.cyan(`
5775
+ Task-based generation: ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, resuming from checkpoint)`));
5613
5776
  } else {
5614
- console.log(chalk6.cyan(`
5777
+ console.log(chalk8.cyan(`
5615
5778
  Task-based generation: ${tasks.length} tasks`));
5616
5779
  }
5617
5780
  const sharedConfigPaths = new Set(
@@ -5643,9 +5806,9 @@ Output ONLY a valid JSON array:
5643
5806
  const pct = Math.round(completedTasks / tasks.length * 100);
5644
5807
  const barWidth = 20;
5645
5808
  const filled = Math.round(pct / 100 * barWidth);
5646
- const bar = chalk6.green("\u2588".repeat(filled)) + chalk6.gray("\u2591".repeat(barWidth - filled));
5809
+ const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
5647
5810
  console.log(
5648
- chalk6.bold(`
5811
+ chalk8.bold(`
5649
5812
  [${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
5650
5813
  );
5651
5814
  } else {
@@ -5653,12 +5816,12 @@ Output ONLY a valid JSON array:
5653
5816
  }
5654
5817
  const executeTask = async (task, batchIsParallel) => {
5655
5818
  if (task.filesToTouch.length === 0) {
5656
- if (!batchIsParallel) console.log(chalk6.gray(" No files specified, skipping."));
5819
+ if (!batchIsParallel) console.log(chalk8.gray(" No files specified, skipping."));
5657
5820
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
5658
5821
  }
5659
5822
  const filePlan = await Promise.all(
5660
5823
  task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
5661
- const exists = await fs8.pathExists(path6.join(workingDir, f));
5824
+ const exists = await fs10.pathExists(path6.join(workingDir, f));
5662
5825
  return {
5663
5826
  file: f,
5664
5827
  action: exists ? "modify" : "create",
@@ -5698,7 +5861,7 @@ ${taskContext}`,
5698
5861
  const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
5699
5862
  if (isCodeFile || isViewFile) {
5700
5863
  try {
5701
- const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs8.readFile(path6.join(workingDir, writtenFile), "utf-8");
5864
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path6.join(workingDir, writtenFile), "utf-8");
5702
5865
  generatedFileCache.set(writtenFile, content);
5703
5866
  } catch {
5704
5867
  }
@@ -5710,7 +5873,12 @@ ${taskContext}`,
5710
5873
  const layerResults = [];
5711
5874
  for (const batch of taskBatches) {
5712
5875
  const batchIsParallel = batch.length > 1;
5713
- const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
5876
+ const batchResultPromises = batch.map(
5877
+ (task) => executeTask(task, batchIsParallel).catch((err) => {
5878
+ console.log(chalk8.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
5879
+ return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
5880
+ })
5881
+ );
5714
5882
  const batchResults = await Promise.all(batchResultPromises);
5715
5883
  layerResults.push(...batchResults);
5716
5884
  await updateCacheFromBatch(batchResults);
@@ -5723,14 +5891,14 @@ ${taskContext}`,
5723
5891
  totalFiles += result.total;
5724
5892
  allGeneratedFiles.push(...result.files);
5725
5893
  if (isParallel) {
5726
- const icon = result.success === result.total ? chalk6.green("\u2714") : chalk6.yellow("!");
5894
+ const icon = result.success === result.total ? chalk8.green("\u2714") : chalk8.yellow("!");
5727
5895
  const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
5728
5896
  console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
5729
5897
  }
5730
5898
  const taskStatus = result.success === result.total ? "done" : "failed";
5731
5899
  await updateTaskStatus(specFilePath, result.task.id, taskStatus);
5732
5900
  if (taskStatus === "failed") {
5733
- console.log(chalk6.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
5901
+ console.log(chalk8.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
5734
5902
  }
5735
5903
  }
5736
5904
  completedTasks += layerTasks.length;
@@ -5745,7 +5913,7 @@ ${taskContext}`,
5745
5913
  if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
5746
5914
  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.`;
5747
5915
  }
5748
- console.log(chalk6.gray(`
5916
+ console.log(chalk8.gray(`
5749
5917
  + updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
5750
5918
  const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
5751
5919
  await this.generateFiles(
@@ -5763,17 +5931,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
5763
5931
  }
5764
5932
  }
5765
5933
  console.log(
5766
- chalk6.bold(
5934
+ chalk8.bold(
5767
5935
  `
5768
- ${totalSuccess === totalFiles ? chalk6.green("\u2714") : chalk6.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
5936
+ ${totalSuccess === totalFiles ? chalk8.green("\u2714") : chalk8.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
5769
5937
  )
5770
5938
  );
5771
5939
  return allGeneratedFiles;
5772
5940
  }
5773
5941
  async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
5774
- const prefix = taskLabel ? ` [${chalk6.cyan(taskLabel)}] ` : " ";
5942
+ const prefix = taskLabel ? ` [${chalk8.cyan(taskLabel)}] ` : " ";
5775
5943
  if (!taskLabel) {
5776
- console.log(chalk6.gray(`
5944
+ console.log(chalk8.gray(`
5777
5945
  Generating ${filePlan.length} file(s)...`));
5778
5946
  }
5779
5947
  let successCount = 0;
@@ -5781,8 +5949,8 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
5781
5949
  for (const item of filePlan) {
5782
5950
  const fullPath = path6.join(workingDir, item.file);
5783
5951
  let existingContent = "";
5784
- if (await fs8.pathExists(fullPath)) {
5785
- existingContent = await fs8.readFile(fullPath, "utf-8");
5952
+ if (await fs10.pathExists(fullPath)) {
5953
+ existingContent = await fs10.readFile(fullPath, "utf-8");
5786
5954
  }
5787
5955
  const codePrompt = `Implement this file.
5788
5956
 
@@ -5797,19 +5965,21 @@ ${existingContent || "Output only the complete file content."}`;
5797
5965
  try {
5798
5966
  const raw = await this.provider.generate(codePrompt, systemPrompt);
5799
5967
  const fileContent = stripCodeFences(raw);
5800
- await fs8.ensureDir(path6.dirname(fullPath));
5801
- await fs8.writeFile(fullPath, fileContent, "utf-8");
5802
- console.log(`${prefix}${existingContent ? chalk6.yellow("~") : chalk6.green("+")} ${chalk6.bold(item.file)} ${chalk6.green("\u2714")}`);
5968
+ await getActiveSnapshot()?.snapshotFile(fullPath);
5969
+ await fs10.ensureDir(path6.dirname(fullPath));
5970
+ await fs10.writeFile(fullPath, fileContent, "utf-8");
5971
+ getActiveLogger()?.fileWritten(item.file);
5972
+ console.log(`${prefix}${existingContent ? chalk8.yellow("~") : chalk8.green("+")} ${chalk8.bold(item.file)} ${chalk8.green("\u2714")}`);
5803
5973
  successCount++;
5804
5974
  writtenFiles.push(item.file);
5805
5975
  } catch (err) {
5806
- console.log(`${prefix}${chalk6.red("\u2718")} ${chalk6.bold(item.file)} \u2014 ${chalk6.red(err.message)}`);
5976
+ console.log(`${prefix}${chalk8.red("\u2718")} ${chalk8.bold(item.file)} \u2014 ${chalk8.red(err.message)}`);
5807
5977
  }
5808
5978
  }
5809
5979
  if (!taskLabel) {
5810
5980
  console.log(
5811
- chalk6.bold(
5812
- ` ${successCount === filePlan.length ? chalk6.green("\u2714") : chalk6.yellow("!")} ${successCount}/${filePlan.length} files written.`
5981
+ chalk8.bold(
5982
+ ` ${successCount === filePlan.length ? chalk8.green("\u2714") : chalk8.yellow("!")} ${successCount}/${filePlan.length} files written.`
5813
5983
  )
5814
5984
  );
5815
5985
  }
@@ -5817,8 +5987,8 @@ ${existingContent || "Output only the complete file content."}`;
5817
5987
  }
5818
5988
  // ── Mode: plan ─────────────────────────────────────────────────────────────
5819
5989
  async runPlanMode(specFilePath) {
5820
- 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"));
5821
- const spec = await fs8.readFile(specFilePath, "utf-8");
5990
+ console.log(chalk8.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"));
5991
+ const spec = await fs10.readFile(specFilePath, "utf-8");
5822
5992
  const plan = await this.provider.generate(
5823
5993
  `Create a detailed, step-by-step implementation plan for the following feature spec.
5824
5994
  Be specific about:
@@ -5830,7 +6000,7 @@ Be specific about:
5830
6000
  ${spec}`,
5831
6001
  "You are a senior developer creating an actionable implementation guide."
5832
6002
  );
5833
- console.log(chalk6.cyan("\n") + plan);
6003
+ console.log(chalk8.cyan("\n") + plan);
5834
6004
  }
5835
6005
  };
5836
6006
  function topoSortLayerTasks(tasks) {
@@ -5881,32 +6051,32 @@ function printTaskProgress(completed, total, task, mode) {
5881
6051
  const pct = total > 0 ? Math.round(completed / total * 100) : 0;
5882
6052
  const barWidth = 20;
5883
6053
  const filled = Math.round(pct / 100 * barWidth);
5884
- const bar = chalk6.green("\u2588".repeat(filled)) + chalk6.gray("\u2591".repeat(barWidth - filled));
6054
+ const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
5885
6055
  const icon = LAYER_ICONS[task.layer] ?? " ";
5886
6056
  if (mode === "skip") {
5887
6057
  console.log(
5888
- chalk6.gray(`
6058
+ chalk8.gray(`
5889
6059
  [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
5890
6060
  );
5891
6061
  } else {
5892
6062
  console.log(
5893
- chalk6.bold(`
6063
+ chalk8.bold(`
5894
6064
  [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
5895
6065
  );
5896
6066
  }
5897
6067
  }
5898
6068
 
5899
6069
  // core/reviewer.ts
5900
- import chalk7 from "chalk";
6070
+ import chalk9 from "chalk";
5901
6071
  import { execSync as execSync2 } from "child_process";
5902
6072
  import * as path7 from "path";
5903
- import * as fs9 from "fs-extra";
6073
+ import * as fs11 from "fs-extra";
5904
6074
  var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5905
6075
  async function loadReviewHistory(projectRoot) {
5906
6076
  const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
5907
6077
  try {
5908
- if (await fs9.pathExists(historyPath)) {
5909
- return await fs9.readJson(historyPath);
6078
+ if (await fs11.pathExists(historyPath)) {
6079
+ return await fs11.readJson(historyPath);
5910
6080
  }
5911
6081
  } catch {
5912
6082
  }
@@ -5917,7 +6087,7 @@ async function appendReviewHistory(projectRoot, entry) {
5917
6087
  const existing = await loadReviewHistory(projectRoot);
5918
6088
  const updated = [...existing, entry].slice(-20);
5919
6089
  try {
5920
- await fs9.writeJson(historyPath, updated, { spaces: 2 });
6090
+ await fs11.writeJson(historyPath, updated, { spaces: 2 });
5921
6091
  } catch {
5922
6092
  }
5923
6093
  }
@@ -5925,6 +6095,14 @@ function extractScore(reviewText) {
5925
6095
  const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
5926
6096
  return match ? parseFloat(match[1]) : 0;
5927
6097
  }
6098
+ function extractImpactLevel(reviewText) {
6099
+ const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
6100
+ return match ? match[1] : void 0;
6101
+ }
6102
+ function extractComplexityLevel(reviewText) {
6103
+ const match = reviewText.match(/复杂度等级[::]\s*(低|中|高)/);
6104
+ return match ? match[1] : void 0;
6105
+ }
5928
6106
  function extractTopIssues(reviewText) {
5929
6107
  const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
5930
6108
  return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
@@ -5970,15 +6148,14 @@ var CodeReviewer = class {
5970
6148
  };
5971
6149
  }
5972
6150
  /**
5973
- * Two-pass review:
6151
+ * Three-pass review:
5974
6152
  * Pass 1 — architecture (spec compliance, layer separation, auth)
5975
6153
  * Pass 2 — implementation details (validation, error handling, edge cases)
5976
6154
  * + historical issue recurrence check
5977
- *
5978
- * Falls back to single-pass if the two-pass flag is not set.
6155
+ * Pass 3 — impact assessment + code complexity
5979
6156
  */
5980
- async runTwoPassReview(specContent, codeContext, specFile) {
5981
- console.log(chalk7.gray(" Pass 1/2: Architecture review..."));
6157
+ async runThreePassReview(specContent, codeContext, specFile) {
6158
+ console.log(chalk9.gray(" Pass 1/3: Architecture review..."));
5982
6159
  const archPrompt = `Review the architecture of this change.
5983
6160
 
5984
6161
  === Feature Spec ===
@@ -5987,7 +6164,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
5987
6164
  === Code ===
5988
6165
  ${codeContext}`;
5989
6166
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
5990
- console.log(chalk7.gray(" Pass 2/2: Implementation review..."));
6167
+ console.log(chalk9.gray(" Pass 2/3: Implementation review..."));
5991
6168
  const history = await loadReviewHistory(this.projectRoot);
5992
6169
  const historyContext = buildHistoryContext(history);
5993
6170
  const implPrompt = `Review the implementation details of this change.
@@ -6002,61 +6179,85 @@ ${codeContext}
6002
6179
  ${archReview}
6003
6180
  ${historyContext}`;
6004
6181
  const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
6005
- const combined = `${archReview}
6182
+ console.log(chalk9.gray(" Pass 3/3: Impact & complexity assessment..."));
6183
+ const impactPrompt = `Assess the impact and complexity of this change.
6006
6184
 
6007
- ${"\u2500".repeat(52)}
6185
+ === Feature Spec ===
6186
+ ${specContent || "(No spec \u2014 review for general code quality)"}
6008
6187
 
6188
+ === Code ===
6189
+ ${codeContext}
6190
+
6191
+ === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
6192
+ ${archReview}
6193
+
6194
+ === Implementation Review (Pass 2 \u2014 do NOT repeat) ===
6009
6195
  ${implReview}`;
6196
+ const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
6197
+ const sep = "\u2500".repeat(52);
6198
+ const combined = `${archReview}
6199
+
6200
+ ${sep}
6201
+
6202
+ ${implReview}
6203
+
6204
+ ${sep}
6205
+
6206
+ ${impactReview}`;
6010
6207
  const score = extractScore(implReview) || extractScore(archReview);
6011
6208
  const topIssues = extractTopIssues(implReview);
6209
+ const impactLevel = extractImpactLevel(impactReview);
6210
+ const complexityLevel = extractComplexityLevel(impactReview);
6012
6211
  if (score > 0 && specFile) {
6013
6212
  await appendReviewHistory(this.projectRoot, {
6014
6213
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
6015
6214
  specFile: path7.relative(this.projectRoot, specFile),
6016
6215
  score,
6017
- topIssues
6216
+ topIssues,
6217
+ ...impactLevel ? { impactLevel } : {},
6218
+ ...complexityLevel ? { complexityLevel } : {}
6018
6219
  });
6019
6220
  }
6020
6221
  return combined;
6021
6222
  }
6022
6223
  async reviewCode(specContent, specFile) {
6023
- 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"));
6224
+ console.log(chalk9.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"));
6024
6225
  const diff = this.getGitDiff();
6025
6226
  if (!diff.trim()) {
6026
6227
  console.log(
6027
- chalk7.yellow(" No git diff found. Stage or commit changes first, then run review.")
6228
+ chalk9.yellow(" No git diff found. Stage or commit changes first, then run review.")
6028
6229
  );
6029
- console.log(chalk7.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6230
+ console.log(chalk9.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6030
6231
  return "No changes";
6031
6232
  }
6032
6233
  const { files, added, removed } = this.getDiffStats(diff);
6033
6234
  console.log(
6034
- chalk7.gray(` Diff: ${files} file(s), ${chalk7.green("+" + added)} ${chalk7.red("-" + removed)}`)
6235
+ chalk9.gray(` Diff: ${files} file(s), ${chalk9.green("+" + added)} ${chalk9.red("-" + removed)}`)
6035
6236
  );
6036
6237
  console.log(
6037
- chalk7.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6238
+ chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6038
6239
  );
6039
6240
  const codeContext = diff.slice(0, 1e4);
6040
- const reviewResult = await this.runTwoPassReview(specContent, codeContext, specFile);
6041
- 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"));
6241
+ const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
6242
+ console.log(chalk9.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"));
6042
6243
  console.log(reviewResult);
6043
- 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"));
6244
+ console.log(chalk9.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"));
6044
6245
  return reviewResult;
6045
6246
  }
6046
6247
  /**
6047
6248
  * Review directly from generated file contents (for api mode where git diff is empty).
6048
6249
  */
6049
6250
  async reviewFiles(specContent, filePaths, workingDir, specFile) {
6050
- console.log(chalk7.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6051
- console.log(chalk7.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6251
+ console.log(chalk9.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6252
+ console.log(chalk9.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6052
6253
  console.log(
6053
- chalk7.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6254
+ chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6054
6255
  );
6055
6256
  let filesSection = "";
6056
6257
  for (const filePath of filePaths) {
6057
6258
  const fullPath = path7.join(workingDir, filePath);
6058
6259
  try {
6059
- const content = await fs9.readFile(fullPath, "utf-8");
6260
+ const content = await fs11.readFile(fullPath, "utf-8");
6060
6261
  filesSection += `
6061
6262
 
6062
6263
  === ${filePath} ===
@@ -6070,33 +6271,33 @@ ${content.slice(0, 3e3)}`;
6070
6271
  (file not found)`;
6071
6272
  }
6072
6273
  }
6073
- const reviewResult = await this.runTwoPassReview(specContent, filesSection, specFile);
6074
- 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"));
6274
+ const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
6275
+ console.log(chalk9.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"));
6075
6276
  console.log(reviewResult);
6076
- 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"));
6277
+ console.log(chalk9.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"));
6077
6278
  return reviewResult;
6078
6279
  }
6079
6280
  /** Print score trend from history (last N reviews) */
6080
6281
  async printScoreTrend(limit = 5) {
6081
6282
  const history = await loadReviewHistory(this.projectRoot);
6082
6283
  if (history.length === 0) {
6083
- console.log(chalk7.gray(" No review history yet."));
6284
+ console.log(chalk9.gray(" No review history yet."));
6084
6285
  return;
6085
6286
  }
6086
6287
  const recent = history.slice(-limit);
6087
- 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"));
6288
+ console.log(chalk9.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"));
6088
6289
  for (const entry of recent) {
6089
6290
  const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
6090
- const color = entry.score >= 8 ? chalk7.green : entry.score >= 6 ? chalk7.yellow : chalk7.red;
6291
+ const color = entry.score >= 8 ? chalk9.green : entry.score >= 6 ? chalk9.yellow : chalk9.red;
6091
6292
  console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path7.basename(entry.specFile)}`);
6092
6293
  }
6093
- 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"));
6294
+ console.log(chalk9.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"));
6094
6295
  }
6095
6296
  };
6096
6297
 
6097
6298
  // core/constitution-generator.ts
6098
- import chalk8 from "chalk";
6099
- import * as fs10 from "fs-extra";
6299
+ import chalk10 from "chalk";
6300
+ import * as fs12 from "fs-extra";
6100
6301
  import * as path8 from "path";
6101
6302
 
6102
6303
  // prompts/constitution.prompt.ts
@@ -6178,7 +6379,7 @@ var ConstitutionGenerator = class {
6178
6379
  }
6179
6380
  async saveConstitution(projectRoot, content) {
6180
6381
  const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6181
- await fs10.writeFile(filePath, content, "utf-8");
6382
+ await fs12.writeFile(filePath, content, "utf-8");
6182
6383
  return filePath;
6183
6384
  }
6184
6385
  };
@@ -6237,15 +6438,15 @@ ${sections.join("\n")}
6237
6438
  }
6238
6439
  async function loadConstitution(projectRoot) {
6239
6440
  const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6240
- if (await fs10.pathExists(filePath)) {
6241
- return fs10.readFile(filePath, "utf-8");
6441
+ if (await fs12.pathExists(filePath)) {
6442
+ return fs12.readFile(filePath, "utf-8");
6242
6443
  }
6243
6444
  return void 0;
6244
6445
  }
6245
6446
  function printConstitutionHint(exists) {
6246
6447
  if (!exists) {
6247
6448
  console.log(
6248
- chalk8.yellow(
6449
+ chalk10.yellow(
6249
6450
  " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6250
6451
  )
6251
6452
  );
@@ -6290,8 +6491,8 @@ function parseSpecAndTasks(raw) {
6290
6491
  // git/worktree.ts
6291
6492
  import { execSync as execSync3 } from "child_process";
6292
6493
  import * as path9 from "path";
6293
- import * as fs11 from "fs-extra";
6294
- import chalk9 from "chalk";
6494
+ import * as fs13 from "fs-extra";
6495
+ import chalk11 from "chalk";
6295
6496
  var GitWorktreeManager = class {
6296
6497
  constructor(baseDir) {
6297
6498
  this.baseDir = baseDir;
@@ -6318,30 +6519,30 @@ var GitWorktreeManager = class {
6318
6519
  for (const dir of candidates) {
6319
6520
  const src = path9.join(this.baseDir, dir);
6320
6521
  const dest = path9.join(worktreePath, dir);
6321
- if (!await fs11.pathExists(src)) continue;
6322
- if (await fs11.pathExists(dest)) continue;
6522
+ if (!await fs13.pathExists(src)) continue;
6523
+ if (await fs13.pathExists(dest)) continue;
6323
6524
  try {
6324
- await fs11.ensureSymlink(src, dest, "dir");
6325
- console.log(chalk9.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6525
+ await fs13.ensureSymlink(src, dest, "dir");
6526
+ console.log(chalk11.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6326
6527
  } catch (err) {
6327
- console.log(chalk9.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6328
- console.log(chalk9.yellow(` Run \`npm install\` inside the worktree manually.`));
6528
+ console.log(chalk11.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6529
+ console.log(chalk11.yellow(` Run \`npm install\` inside the worktree manually.`));
6329
6530
  }
6330
6531
  }
6331
6532
  }
6332
6533
  async createWorktree(idea) {
6333
6534
  if (!this.isGitRepo()) {
6334
- console.log(chalk9.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6535
+ console.log(chalk11.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6335
6536
  return null;
6336
6537
  }
6337
6538
  const featureName = this.sanitizeFeatureName(idea);
6338
6539
  const branchName = `feature/${featureName}`;
6339
6540
  const repoName = path9.basename(this.baseDir);
6340
6541
  const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
6341
- console.log(chalk9.cyan(`
6542
+ console.log(chalk11.cyan(`
6342
6543
  --- Setting up Git Worktree ---`));
6343
- if (await fs11.pathExists(worktreePath)) {
6344
- console.log(chalk9.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6544
+ if (await fs13.pathExists(worktreePath)) {
6545
+ console.log(chalk11.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6345
6546
  await this.linkDependencies(worktreePath);
6346
6547
  return worktreePath;
6347
6548
  }
@@ -6355,7 +6556,7 @@ var GitWorktreeManager = class {
6355
6556
  branchExists = true;
6356
6557
  } catch {
6357
6558
  }
6358
- console.log(chalk9.gray(`Creating worktree at: ${worktreePath}`));
6559
+ console.log(chalk11.gray(`Creating worktree at: ${worktreePath}`));
6359
6560
  if (branchExists) {
6360
6561
  execSync3(`git worktree add "${worktreePath}" ${branchName}`, {
6361
6562
  cwd: this.baseDir,
@@ -6368,12 +6569,12 @@ var GitWorktreeManager = class {
6368
6569
  });
6369
6570
  }
6370
6571
  console.log(
6371
- chalk9.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6572
+ chalk11.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6372
6573
  );
6373
6574
  await this.linkDependencies(worktreePath);
6374
6575
  return worktreePath;
6375
6576
  } catch (error) {
6376
- console.error(chalk9.red("Failed to create git worktree:"), error);
6577
+ console.error(chalk11.red("Failed to create git worktree:"), error);
6377
6578
  return null;
6378
6579
  }
6379
6580
  }