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.
@@ -20,6 +20,7 @@ __export(codegen_prompt_exports, {
20
20
  codeGenSystemPrompt: () => codeGenSystemPrompt,
21
21
  getCodeGenSystemPrompt: () => getCodeGenSystemPrompt,
22
22
  reviewArchitectureSystemPrompt: () => reviewArchitectureSystemPrompt,
23
+ reviewImpactComplexitySystemPrompt: () => reviewImpactComplexitySystemPrompt,
23
24
  reviewImplementationSystemPrompt: () => reviewImplementationSystemPrompt,
24
25
  reviewSystemPrompt: () => reviewSystemPrompt
25
26
  });
@@ -39,7 +40,7 @@ function getCodeGenSystemPrompt(repoType) {
39
40
  return codeGenSystemPrompt;
40
41
  }
41
42
  }
42
- var codeGenSystemPrompt, codeGenGoSystemPrompt, codeGenPythonSystemPrompt, codeGenJavaSystemPrompt, codeGenRustSystemPrompt, codeGenPhpSystemPrompt, reviewSystemPrompt, reviewArchitectureSystemPrompt, reviewImplementationSystemPrompt;
43
+ var codeGenSystemPrompt, codeGenGoSystemPrompt, codeGenPythonSystemPrompt, codeGenJavaSystemPrompt, codeGenRustSystemPrompt, codeGenPhpSystemPrompt, reviewSystemPrompt, reviewArchitectureSystemPrompt, reviewImplementationSystemPrompt, reviewImpactComplexitySystemPrompt;
43
44
  var init_codegen_prompt = __esm({
44
45
  "prompts/codegen.prompt.ts"() {
45
46
  "use strict";
@@ -266,6 +267,48 @@ Actionable, concrete improvements.
266
267
  Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
267
268
 
268
269
  Be specific. Reference actual code, not vague principles.`;
270
+ reviewImpactComplexitySystemPrompt = `You are a Senior Staff Engineer assessing the RISK and MAINTAINABILITY of a code change.
271
+
272
+ Two previous review passes have already covered architecture compliance and implementation correctness. Do NOT repeat their findings.
273
+
274
+ Your job is to answer exactly two questions:
275
+
276
+ ---
277
+
278
+ ## \u{1F30A} \u5F71\u54CD\u9762\u8BC4\u4F30 (Impact Assessment)
279
+
280
+ Evaluate what this change touches beyond its own files:
281
+
282
+ 1. **\u76F4\u63A5\u5F71\u54CD\u6587\u4EF6** \u2014 List the files created or modified.
283
+ 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)
284
+ 3. **\u7834\u574F\u6027\u53D8\u66F4 (Breaking Changes)** \u2014 Are there changes requiring coordinated updates elsewhere?
285
+ - API endpoint signature changes (path, method, required params)
286
+ - Database schema changes (renamed columns, dropped fields, new NOT NULL constraints)
287
+ - Config / env variable changes
288
+ - Exported function/type renames
289
+ - If none: state "\u65E0\u7834\u574F\u6027\u53D8\u66F4"
290
+ 4. **\u5F71\u54CD\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
291
+ - \u4F4E: isolated to new files, no shared interface changes
292
+ - \u4E2D: modifies shared utilities or existing public interfaces, backwards compatible
293
+ - \u9AD8: breaking changes, or touches core auth / payment / data integrity paths
294
+
295
+ ---
296
+
297
+ ## \u{1F9EE} \u4EE3\u7801\u590D\u6742\u5EA6\u8BC4\u4F30 (Complexity Assessment)
298
+
299
+ Evaluate how hard the new code will be to understand and change in the future:
300
+
301
+ 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).
302
+ 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?
303
+ 3. **\u53EF\u7EF4\u62A4\u6027\u98CE\u9669** \u2014 Flag patterns that will cause pain when requirements change:
304
+ - Magic numbers / hardcoded strings that should be constants
305
+ - Business logic buried in framework lifecycle hooks or constructors
306
+ - Implicit temporal coupling (functions that must be called in a specific order)
307
+ 4. **\u590D\u6742\u5EA6\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
308
+
309
+ ---
310
+
311
+ Format using ONLY these two sections. Be specific. Reference file names and line ranges where relevant.`;
269
312
  }
270
313
  });
271
314
 
@@ -276,31 +319,31 @@ __export(workspace_loader_exports, {
276
319
  WorkspaceLoader: () => WorkspaceLoader,
277
320
  detectRepoType: () => detectRepoType
278
321
  });
279
- import * as fs16 from "fs-extra";
280
- import * as path15 from "path";
322
+ import * as fs18 from "fs-extra";
323
+ import * as path17 from "path";
281
324
  async function detectRepoType(repoAbsPath) {
282
- if (await fs16.pathExists(path15.join(repoAbsPath, "go.mod"))) {
325
+ if (await fs18.pathExists(path17.join(repoAbsPath, "go.mod"))) {
283
326
  return { type: "go", role: "backend" };
284
327
  }
285
- if (await fs16.pathExists(path15.join(repoAbsPath, "composer.json"))) {
328
+ if (await fs18.pathExists(path17.join(repoAbsPath, "composer.json"))) {
286
329
  return { type: "php", role: "backend" };
287
330
  }
288
- if (await fs16.pathExists(path15.join(repoAbsPath, "Cargo.toml"))) {
331
+ if (await fs18.pathExists(path17.join(repoAbsPath, "Cargo.toml"))) {
289
332
  return { type: "rust", role: "backend" };
290
333
  }
291
- if (await fs16.pathExists(path15.join(repoAbsPath, "pom.xml")) || await fs16.pathExists(path15.join(repoAbsPath, "build.gradle")) || await fs16.pathExists(path15.join(repoAbsPath, "build.gradle.kts"))) {
334
+ if (await fs18.pathExists(path17.join(repoAbsPath, "pom.xml")) || await fs18.pathExists(path17.join(repoAbsPath, "build.gradle")) || await fs18.pathExists(path17.join(repoAbsPath, "build.gradle.kts"))) {
292
335
  return { type: "java", role: "backend" };
293
336
  }
294
- if (await fs16.pathExists(path15.join(repoAbsPath, "requirements.txt")) || await fs16.pathExists(path15.join(repoAbsPath, "pyproject.toml")) || await fs16.pathExists(path15.join(repoAbsPath, "setup.py"))) {
337
+ if (await fs18.pathExists(path17.join(repoAbsPath, "requirements.txt")) || await fs18.pathExists(path17.join(repoAbsPath, "pyproject.toml")) || await fs18.pathExists(path17.join(repoAbsPath, "setup.py"))) {
295
338
  return { type: "python", role: "backend" };
296
339
  }
297
- const pkgPath = path15.join(repoAbsPath, "package.json");
298
- if (!await fs16.pathExists(pkgPath)) {
340
+ const pkgPath = path17.join(repoAbsPath, "package.json");
341
+ if (!await fs18.pathExists(pkgPath)) {
299
342
  return { type: "unknown", role: "shared" };
300
343
  }
301
344
  let pkg = {};
302
345
  try {
303
- pkg = await fs16.readJson(pkgPath);
346
+ pkg = await fs18.readJson(pkgPath);
304
347
  } catch {
305
348
  return { type: "unknown", role: "shared" };
306
349
  }
@@ -344,13 +387,13 @@ var init_workspace_loader = __esm({
344
387
  * Returns null if the file does not exist (graceful degradation).
345
388
  */
346
389
  async load() {
347
- const configPath = path15.join(this.workspaceRoot, WORKSPACE_CONFIG_FILE);
348
- if (!await fs16.pathExists(configPath)) {
390
+ const configPath = path17.join(this.workspaceRoot, WORKSPACE_CONFIG_FILE);
391
+ if (!await fs18.pathExists(configPath)) {
349
392
  return null;
350
393
  }
351
394
  let raw;
352
395
  try {
353
- raw = await fs16.readJson(configPath);
396
+ raw = await fs18.readJson(configPath);
354
397
  } catch (err) {
355
398
  throw new Error(
356
399
  `Failed to parse ${WORKSPACE_CONFIG_FILE}: ${err.message}`
@@ -373,15 +416,15 @@ var init_workspace_loader = __esm({
373
416
  * Auto-detects type and role from dependencies.
374
417
  */
375
418
  async autoDetect(names) {
376
- const entries = await fs16.readdir(this.workspaceRoot);
419
+ const entries = await fs18.readdir(this.workspaceRoot);
377
420
  const repos = [];
378
421
  for (const entry of entries) {
379
- const absPath = path15.join(this.workspaceRoot, entry);
380
- const stat4 = await fs16.stat(absPath).catch(() => null);
422
+ const absPath = path17.join(this.workspaceRoot, entry);
423
+ const stat4 = await fs18.stat(absPath).catch(() => null);
381
424
  if (!stat4 || !stat4.isDirectory()) continue;
382
425
  if (entry.startsWith(".") || entry === "node_modules") continue;
383
426
  if (names && !names.includes(entry)) continue;
384
- const hasManifest = await fs16.pathExists(path15.join(absPath, "package.json")) || await fs16.pathExists(path15.join(absPath, "go.mod")) || await fs16.pathExists(path15.join(absPath, "Cargo.toml")) || await fs16.pathExists(path15.join(absPath, "pom.xml")) || await fs16.pathExists(path15.join(absPath, "build.gradle")) || await fs16.pathExists(path15.join(absPath, "requirements.txt")) || await fs16.pathExists(path15.join(absPath, "pyproject.toml")) || await fs16.pathExists(path15.join(absPath, "composer.json"));
427
+ const hasManifest = await fs18.pathExists(path17.join(absPath, "package.json")) || await fs18.pathExists(path17.join(absPath, "go.mod")) || await fs18.pathExists(path17.join(absPath, "Cargo.toml")) || await fs18.pathExists(path17.join(absPath, "pom.xml")) || await fs18.pathExists(path17.join(absPath, "build.gradle")) || await fs18.pathExists(path17.join(absPath, "requirements.txt")) || await fs18.pathExists(path17.join(absPath, "pyproject.toml")) || await fs18.pathExists(path17.join(absPath, "composer.json"));
385
428
  if (!hasManifest) continue;
386
429
  const { type, role } = await detectRepoType(absPath);
387
430
  repos.push({ name: entry, path: entry, type, role });
@@ -394,11 +437,11 @@ var init_workspace_loader = __esm({
394
437
  async resolveRepoPaths(config2) {
395
438
  const resolved = [];
396
439
  for (const repo of config2.repos) {
397
- const absPath = path15.resolve(this.workspaceRoot, repo.path);
440
+ const absPath = path17.resolve(this.workspaceRoot, repo.path);
398
441
  let constitution;
399
- const constitutionFile = path15.join(absPath, ".ai-spec-constitution.md");
400
- if (await fs16.pathExists(constitutionFile)) {
401
- constitution = await fs16.readFile(constitutionFile, "utf-8");
442
+ const constitutionFile = path17.join(absPath, ".ai-spec-constitution.md");
443
+ if (await fs18.pathExists(constitutionFile)) {
444
+ constitution = await fs18.readFile(constitutionFile, "utf-8");
402
445
  }
403
446
  resolved.push({ ...repo, constitution });
404
447
  }
@@ -408,19 +451,19 @@ var init_workspace_loader = __esm({
408
451
  * Save a workspace config to disk.
409
452
  */
410
453
  async save(config2) {
411
- const configPath = path15.join(this.workspaceRoot, WORKSPACE_CONFIG_FILE);
454
+ const configPath = path17.join(this.workspaceRoot, WORKSPACE_CONFIG_FILE);
412
455
  const toSave = {
413
456
  name: config2.name,
414
457
  repos: config2.repos.map(({ constitution: _c, ...rest }) => rest)
415
458
  };
416
- await fs16.writeJson(configPath, toSave, { spaces: 2 });
459
+ await fs18.writeJson(configPath, toSave, { spaces: 2 });
417
460
  return configPath;
418
461
  }
419
462
  /**
420
463
  * Resolve the absolute path of a repo given its config.
421
464
  */
422
465
  resolveAbsPath(repo) {
423
- return path15.resolve(this.workspaceRoot, repo.path);
466
+ return path17.resolve(this.workspaceRoot, repo.path);
424
467
  }
425
468
  /**
426
469
  * Find which repos are backend (contract providers) and which depend on them.
@@ -442,9 +485,9 @@ var init_workspace_loader = __esm({
442
485
 
443
486
  // cli/index.ts
444
487
  import { Command } from "commander";
445
- import * as path21 from "path";
446
- import * as fs22 from "fs-extra";
447
- import chalk17 from "chalk";
488
+ import * as path23 from "path";
489
+ import * as fs24 from "fs-extra";
490
+ import chalk19 from "chalk";
448
491
  import * as dotenv from "dotenv";
449
492
  import { input, confirm as confirm2, select as select3 } from "@inquirer/prompts";
450
493
 
@@ -559,6 +602,67 @@ model ExampleModel {
559
602
 
560
603
  \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`;
561
604
 
605
+ // core/provider-utils.ts
606
+ import chalk from "chalk";
607
+ var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
608
+ var ProviderError = class extends Error {
609
+ constructor(message, kind, originalError) {
610
+ super(message);
611
+ this.kind = kind;
612
+ this.originalError = originalError;
613
+ this.name = "ProviderError";
614
+ }
615
+ };
616
+ function classifyError(err, label) {
617
+ const e = err;
618
+ const status = e.status ?? e.response?.status;
619
+ if (status === 401 || status === 403)
620
+ return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
621
+ if (status === 429)
622
+ return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
623
+ if (e._timeout || e.message?.toLowerCase().includes("timed out"))
624
+ return new ProviderError(`Request timed out (${label})`, "timeout", err);
625
+ if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
626
+ return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
627
+ return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
628
+ }
629
+ function isRetryable(err) {
630
+ const e = err;
631
+ const status = e.status ?? e.response?.status;
632
+ if (status === 401 || status === 403) return false;
633
+ if (status === 429 || status !== void 0 && status >= 500) return true;
634
+ if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED") return true;
635
+ if (e.message?.toLowerCase().includes("timed out")) return true;
636
+ return true;
637
+ }
638
+ async function withReliability(fn, opts) {
639
+ const { retries = 2, timeoutMs = 9e4, label = "AI call", onRetry } = opts ?? {};
640
+ for (let attempt = 0; attempt <= retries; attempt++) {
641
+ try {
642
+ return await Promise.race([
643
+ fn(),
644
+ new Promise(
645
+ (_2, reject) => setTimeout(
646
+ () => reject(Object.assign(new Error(`timed out after ${timeoutMs / 1e3}s`), { _timeout: true })),
647
+ timeoutMs
648
+ )
649
+ )
650
+ ]);
651
+ } catch (err) {
652
+ if (!isRetryable(err) || attempt === retries) {
653
+ throw classifyError(err, label);
654
+ }
655
+ const waitMs = attempt === 0 ? 2e3 : 6e3;
656
+ console.warn(
657
+ chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
658
+ );
659
+ onRetry?.(attempt + 1, err);
660
+ await sleep(waitMs);
661
+ }
662
+ }
663
+ throw new Error("unreachable");
664
+ }
665
+
562
666
  // core/spec-generator.ts
563
667
  function geminiRequestOptions() {
564
668
  const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
@@ -704,12 +808,17 @@ var GeminiProvider = class {
704
808
  this.modelName = modelName;
705
809
  }
706
810
  async generate(prompt, systemInstruction) {
707
- const model = this.genAI.getGenerativeModel(
708
- { model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
709
- geminiRequestOptions()
811
+ return withReliability(
812
+ async () => {
813
+ const model = this.genAI.getGenerativeModel(
814
+ { model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
815
+ geminiRequestOptions()
816
+ );
817
+ const result = await model.generateContent(prompt);
818
+ return result.response.text();
819
+ },
820
+ { label: `${this.providerName}/${this.modelName}` }
710
821
  );
711
- const result = await model.generateContent(prompt);
712
- return result.response.text();
713
822
  }
714
823
  };
715
824
  var ClaudeProvider = class {
@@ -721,15 +830,20 @@ var ClaudeProvider = class {
721
830
  this.modelName = modelName;
722
831
  }
723
832
  async generate(prompt, systemInstruction) {
724
- const message = await this.client.messages.create({
725
- model: this.modelName,
726
- max_tokens: 8192,
727
- ...systemInstruction ? { system: systemInstruction } : {},
728
- messages: [{ role: "user", content: prompt }]
729
- });
730
- const block = message.content[0];
731
- if (block.type === "text") return block.text;
732
- throw new Error("Unexpected response type from Claude API");
833
+ return withReliability(
834
+ async () => {
835
+ const message = await this.client.messages.create({
836
+ model: this.modelName,
837
+ max_tokens: 8192,
838
+ ...systemInstruction ? { system: systemInstruction } : {},
839
+ messages: [{ role: "user", content: prompt }]
840
+ });
841
+ const block = message.content[0];
842
+ if (block.type === "text") return block.text;
843
+ throw new Error("Unexpected response type from Claude API");
844
+ },
845
+ { label: `${this.providerName}/${this.modelName}` }
846
+ );
733
847
  }
734
848
  };
735
849
  var OpenAICompatibleProvider = class {
@@ -749,19 +863,24 @@ var OpenAICompatibleProvider = class {
749
863
  });
750
864
  }
751
865
  async generate(prompt, systemInstruction) {
752
- const messages = [];
753
- if (systemInstruction) {
754
- const isOSeries = /^o[13]/.test(this.modelName);
755
- const role = isOSeries ? "developer" : this.systemRole;
756
- messages.push({ role, content: systemInstruction });
757
- }
758
- messages.push({ role: "user", content: prompt });
759
- const completion = await this.client.chat.completions.create({
760
- model: this.modelName,
761
- messages,
762
- ...this.extraBody ? { extra_body: this.extraBody } : {}
763
- });
764
- return completion.choices[0].message.content ?? "";
866
+ return withReliability(
867
+ async () => {
868
+ const messages = [];
869
+ if (systemInstruction) {
870
+ const isOSeries = /^o[13]/.test(this.modelName);
871
+ const role = isOSeries ? "developer" : this.systemRole;
872
+ messages.push({ role, content: systemInstruction });
873
+ }
874
+ messages.push({ role: "user", content: prompt });
875
+ const completion = await this.client.chat.completions.create({
876
+ model: this.modelName,
877
+ messages,
878
+ ...this.extraBody ? { extra_body: this.extraBody } : {}
879
+ });
880
+ return completion.choices[0].message.content ?? "";
881
+ },
882
+ { label: `${this.providerName}/${this.modelName}` }
883
+ );
765
884
  }
766
885
  };
767
886
  var MiMoProvider = class {
@@ -774,32 +893,37 @@ var MiMoProvider = class {
774
893
  this.modelName = modelName;
775
894
  }
776
895
  async generate(prompt, systemInstruction) {
777
- const body = {
778
- model: this.modelName,
779
- max_tokens: 16384,
780
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
781
- top_p: 0.95,
782
- stream: false,
783
- temperature: 1,
784
- stop_sequences: null
785
- };
786
- if (systemInstruction) {
787
- body.system = systemInstruction;
788
- }
789
- const response = await axios.post(this.baseUrl, body, {
790
- headers: {
791
- "api-key": this.apiKey,
792
- "Content-Type": "application/json"
793
- }
794
- });
795
- const data = response.data;
796
- const blocks = data?.content ?? [];
797
- const textBlock = blocks.find((b) => b.type === "text");
798
- if (textBlock?.text) return textBlock.text;
799
- if (data?.stop_reason === "max_tokens") {
800
- 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.`);
801
- }
802
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
896
+ return withReliability(
897
+ async () => {
898
+ const body = {
899
+ model: this.modelName,
900
+ max_tokens: 16384,
901
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
902
+ top_p: 0.95,
903
+ stream: false,
904
+ temperature: 1,
905
+ stop_sequences: null
906
+ };
907
+ if (systemInstruction) {
908
+ body.system = systemInstruction;
909
+ }
910
+ const response = await axios.post(this.baseUrl, body, {
911
+ headers: {
912
+ "api-key": this.apiKey,
913
+ "Content-Type": "application/json"
914
+ }
915
+ });
916
+ const data = response.data;
917
+ const blocks = data?.content ?? [];
918
+ const textBlock = blocks.find((b) => b.type === "text");
919
+ if (textBlock?.text) return textBlock.text;
920
+ if (data?.stop_reason === "max_tokens") {
921
+ 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.`);
922
+ }
923
+ throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
924
+ },
925
+ { label: `${this.providerName}/${this.modelName}` }
926
+ );
803
927
  }
804
928
  };
805
929
  function createProvider(providerName, apiKey, modelName) {
@@ -4260,10 +4384,10 @@ ${preview}
4260
4384
 
4261
4385
  // core/spec-refiner.ts
4262
4386
  import { editor, confirm, select } from "@inquirer/prompts";
4263
- import chalk2 from "chalk";
4387
+ import chalk3 from "chalk";
4264
4388
 
4265
4389
  // core/spec-versioning.ts
4266
- import chalk from "chalk";
4390
+ import chalk2 from "chalk";
4267
4391
  import * as fs4 from "fs-extra";
4268
4392
  import * as path3 from "path";
4269
4393
  function slugify(idea) {
@@ -4348,7 +4472,7 @@ function computeSimpleDiff(oldLines, newLines) {
4348
4472
  var CONTEXT_LINES = 3;
4349
4473
  function printDiff(diff) {
4350
4474
  if (diff.added === 0 && diff.removed === 0) {
4351
- console.log(chalk.gray(" (no changes)"));
4475
+ console.log(chalk2.gray(" (no changes)"));
4352
4476
  return;
4353
4477
  }
4354
4478
  const { lines } = diff;
@@ -4365,25 +4489,25 @@ function printDiff(diff) {
4365
4489
  let prevIdx = -2;
4366
4490
  for (const idx of sorted) {
4367
4491
  if (idx > prevIdx + 1 && prevIdx !== -2) {
4368
- console.log(chalk.cyan(" @@"));
4492
+ console.log(chalk2.cyan(" @@"));
4369
4493
  }
4370
4494
  const l = lines[idx];
4371
4495
  if (l.type === "added") {
4372
- console.log(chalk.green(` + ${l.content}`));
4496
+ console.log(chalk2.green(` + ${l.content}`));
4373
4497
  } else if (l.type === "removed") {
4374
- console.log(chalk.red(` - ${l.content}`));
4498
+ console.log(chalk2.red(` - ${l.content}`));
4375
4499
  } else {
4376
- console.log(chalk.gray(` ${l.content}`));
4500
+ console.log(chalk2.gray(` ${l.content}`));
4377
4501
  }
4378
4502
  prevIdx = idx;
4379
4503
  }
4380
4504
  }
4381
4505
  function printDiffSummary(diff, label) {
4382
4506
  const parts = [];
4383
- if (diff.added > 0) parts.push(chalk.green(`+${diff.added}`));
4384
- if (diff.removed > 0) parts.push(chalk.red(`-${diff.removed}`));
4385
- if (parts.length === 0) parts.push(chalk.gray("no change"));
4386
- console.log(chalk.bold(` ${label}: `) + parts.join(" ") + chalk.gray(` lines`));
4507
+ if (diff.added > 0) parts.push(chalk2.green(`+${diff.added}`));
4508
+ if (diff.removed > 0) parts.push(chalk2.red(`-${diff.removed}`));
4509
+ if (parts.length === 0) parts.push(chalk2.gray("no change"));
4510
+ console.log(chalk2.bold(` ${label}: `) + parts.join(" ") + chalk2.gray(` lines`));
4387
4511
  }
4388
4512
 
4389
4513
  // core/spec-refiner.ts
@@ -4395,16 +4519,16 @@ var SpecRefiner = class {
4395
4519
  let currentSpec = initialSpec;
4396
4520
  let round = 1;
4397
4521
  while (true) {
4398
- console.log(chalk2.cyan(`
4522
+ console.log(chalk3.cyan(`
4399
4523
  \u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
4400
- console.log(chalk2.gray(" Opening spec in editor. Save and close to continue."));
4524
+ console.log(chalk3.gray(" Opening spec in editor. Save and close to continue."));
4401
4525
  currentSpec = await editor({
4402
4526
  message: "Review and edit the spec:",
4403
4527
  default: currentSpec,
4404
4528
  postfix: ".md",
4405
4529
  waitForUserInput: false
4406
4530
  });
4407
- console.log(chalk2.green(" \u2714 Spec saved."));
4531
+ console.log(chalk3.green(" \u2714 Spec saved."));
4408
4532
  const action = await select({
4409
4533
  message: "What would you like to do?",
4410
4534
  choices: [
@@ -4417,7 +4541,7 @@ var SpecRefiner = class {
4417
4541
  break;
4418
4542
  }
4419
4543
  if (action === "ai") {
4420
- console.log(chalk2.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
4544
+ console.log(chalk3.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
4421
4545
  try {
4422
4546
  const improved = await this.provider.generate(
4423
4547
  `Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
@@ -4427,30 +4551,30 @@ Output ONLY the improved markdown spec, nothing else.
4427
4551
  ${currentSpec}`,
4428
4552
  "You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
4429
4553
  );
4430
- console.log(chalk2.yellow("\n AI has suggested improvements. Opening diff in editor..."));
4554
+ console.log(chalk3.yellow("\n AI has suggested improvements. Opening diff in editor..."));
4431
4555
  const acceptImproved = await confirm({
4432
4556
  message: "Accept AI improvements? (opens editor so you can review first)",
4433
4557
  default: true
4434
4558
  });
4435
4559
  if (acceptImproved) {
4436
4560
  const diff = computeDiff(currentSpec, improved);
4437
- 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"));
4561
+ 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"));
4438
4562
  printDiffSummary(diff, "AI edits");
4439
4563
  printDiff(diff);
4440
- 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"));
4564
+ 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"));
4441
4565
  currentSpec = await editor({
4442
4566
  message: "Review AI-improved spec (edit if needed, then save):",
4443
4567
  default: improved,
4444
4568
  postfix: ".md",
4445
4569
  waitForUserInput: false
4446
4570
  });
4447
- console.log(chalk2.green(" \u2714 AI-improved spec accepted."));
4571
+ console.log(chalk3.green(" \u2714 AI-improved spec accepted."));
4448
4572
  } else {
4449
- console.log(chalk2.gray(" AI improvements discarded. Keeping your version."));
4573
+ console.log(chalk3.gray(" AI improvements discarded. Keeping your version."));
4450
4574
  }
4451
4575
  } catch (err) {
4452
- console.error(chalk2.red(" AI improvement failed:"), err);
4453
- console.log(chalk2.gray(" Continuing with current spec."));
4576
+ console.error(chalk3.red(" AI improvement failed:"), err);
4577
+ console.log(chalk3.gray(" Continuing with current spec."));
4454
4578
  }
4455
4579
  }
4456
4580
  round++;
@@ -4460,14 +4584,14 @@ ${currentSpec}`,
4460
4584
  };
4461
4585
 
4462
4586
  // core/code-generator.ts
4463
- import chalk6 from "chalk";
4587
+ import chalk8 from "chalk";
4464
4588
  import { execSync } from "child_process";
4465
- import * as path7 from "path";
4466
- import * as fs8 from "fs-extra";
4589
+ import * as path9 from "path";
4590
+ import * as fs10 from "fs-extra";
4467
4591
  init_codegen_prompt();
4468
4592
 
4469
4593
  // core/task-generator.ts
4470
- import chalk3 from "chalk";
4594
+ import chalk4 from "chalk";
4471
4595
  import * as fs5 from "fs-extra";
4472
4596
  import * as path4 from "path";
4473
4597
 
@@ -4621,31 +4745,36 @@ function parseTasks(raw) {
4621
4745
  }
4622
4746
  function printTasks(tasks) {
4623
4747
  const layerColors = {
4624
- data: chalk3.magenta,
4625
- infra: chalk3.gray,
4626
- service: chalk3.blue,
4627
- api: chalk3.cyan,
4628
- view: chalk3.yellow,
4629
- route: chalk3.white,
4630
- test: chalk3.green
4748
+ data: chalk4.magenta,
4749
+ infra: chalk4.gray,
4750
+ service: chalk4.blue,
4751
+ api: chalk4.cyan,
4752
+ view: chalk4.yellow,
4753
+ route: chalk4.white,
4754
+ test: chalk4.green
4631
4755
  };
4632
- console.log(chalk3.bold(`
4756
+ console.log(chalk4.bold(`
4633
4757
  Tasks (${tasks.length}):`));
4634
4758
  for (const task of tasks) {
4635
- const color = layerColors[task.layer] ?? chalk3.white;
4759
+ const color = layerColors[task.layer] ?? chalk4.white;
4636
4760
  const badge = color(`[${task.layer}]`);
4637
- const prio = task.priority === "high" ? chalk3.red("\u25CF") : task.priority === "medium" ? chalk3.yellow("\u25CF") : chalk3.gray("\u25CF");
4638
- console.log(` ${prio} ${chalk3.bold(task.id)} ${badge} ${task.title}`);
4761
+ const prio = task.priority === "high" ? chalk4.red("\u25CF") : task.priority === "medium" ? chalk4.yellow("\u25CF") : chalk4.gray("\u25CF");
4762
+ console.log(` ${prio} ${chalk4.bold(task.id)} ${badge} ${task.title}`);
4639
4763
  }
4640
4764
  }
4641
4765
  async function loadTasksForSpec(specFilePath) {
4642
4766
  const base = path4.basename(specFilePath, ".md");
4643
4767
  const dir = path4.dirname(specFilePath);
4644
4768
  const tasksFile = path4.join(dir, `${base}-tasks.json`);
4645
- if (await fs5.pathExists(tasksFile)) {
4646
- return fs5.readJson(tasksFile);
4769
+ if (!await fs5.pathExists(tasksFile)) return null;
4770
+ try {
4771
+ return await fs5.readJson(tasksFile);
4772
+ } catch {
4773
+ console.warn(
4774
+ chalk4.yellow(` \u26A0 Tasks file is corrupt or unreadable (${path4.basename(tasksFile)}).`) + chalk4.gray(` Re-run \`ai-spec tasks <spec>\` to regenerate.`)
4775
+ );
4776
+ return null;
4647
4777
  }
4648
- return null;
4649
4778
  }
4650
4779
  async function updateTaskStatus(specFilePath, taskId, status) {
4651
4780
  const tasks = await loadTasksForSpec(specFilePath);
@@ -4659,13 +4788,13 @@ async function updateTaskStatus(specFilePath, taskId, status) {
4659
4788
  }
4660
4789
 
4661
4790
  // core/dsl-extractor.ts
4662
- import chalk5 from "chalk";
4791
+ import chalk6 from "chalk";
4663
4792
  import * as fs6 from "fs-extra";
4664
4793
  import * as path5 from "path";
4665
4794
  import { select as select2 } from "@inquirer/prompts";
4666
4795
 
4667
4796
  // core/dsl-validator.ts
4668
- import chalk4 from "chalk";
4797
+ import chalk5 from "chalk";
4669
4798
  var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
4670
4799
  var MAX_MODELS = 50;
4671
4800
  var MAX_FIELDS_PER_MODEL = 100;
@@ -4738,221 +4867,221 @@ function validateDsl(raw) {
4738
4867
  }
4739
4868
  return { valid: true, dsl: raw };
4740
4869
  }
4741
- function validateFeature(raw, path22, errors) {
4870
+ function validateFeature(raw, path24, errors) {
4742
4871
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4743
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
4872
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4744
4873
  return;
4745
4874
  }
4746
4875
  const f = raw;
4747
- requireNonEmptyString(f["id"], `${path22}.id`, errors);
4748
- requireNonEmptyString(f["title"], `${path22}.title`, errors);
4749
- requireNonEmptyString(f["description"], `${path22}.description`, errors);
4876
+ requireNonEmptyString(f["id"], `${path24}.id`, errors);
4877
+ requireNonEmptyString(f["title"], `${path24}.title`, errors);
4878
+ requireNonEmptyString(f["description"], `${path24}.description`, errors);
4750
4879
  }
4751
- function validateModel(raw, path22, errors) {
4880
+ function validateModel(raw, path24, errors) {
4752
4881
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4753
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
4882
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4754
4883
  return;
4755
4884
  }
4756
4885
  const m = raw;
4757
- requireNonEmptyString(m["name"], `${path22}.name`, errors);
4886
+ requireNonEmptyString(m["name"], `${path24}.name`, errors);
4758
4887
  if (!Array.isArray(m["fields"])) {
4759
- errors.push({ path: `${path22}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4888
+ errors.push({ path: `${path24}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4760
4889
  } else {
4761
4890
  const fields = m["fields"];
4762
4891
  if (fields.length > MAX_FIELDS_PER_MODEL) {
4763
- errors.push({ path: `${path22}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4892
+ errors.push({ path: `${path24}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4764
4893
  }
4765
4894
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4766
- validateModelField(fields[j2], `${path22}.fields[${j2}]`, errors);
4895
+ validateModelField(fields[j2], `${path24}.fields[${j2}]`, errors);
4767
4896
  }
4768
4897
  }
4769
4898
  if (m["relations"] !== void 0) {
4770
4899
  if (!Array.isArray(m["relations"])) {
4771
- errors.push({ path: `${path22}.relations`, message: "Must be an array of strings if present" });
4900
+ errors.push({ path: `${path24}.relations`, message: "Must be an array of strings if present" });
4772
4901
  } else {
4773
4902
  const rels = m["relations"];
4774
4903
  for (let j2 = 0; j2 < rels.length; j2++) {
4775
4904
  if (typeof rels[j2] !== "string") {
4776
- errors.push({ path: `${path22}.relations[${j2}]`, message: "Must be a string" });
4905
+ errors.push({ path: `${path24}.relations[${j2}]`, message: "Must be a string" });
4777
4906
  }
4778
4907
  }
4779
4908
  }
4780
4909
  }
4781
4910
  }
4782
- function validateModelField(raw, path22, errors) {
4911
+ function validateModelField(raw, path24, errors) {
4783
4912
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4784
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
4913
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4785
4914
  return;
4786
4915
  }
4787
4916
  const f = raw;
4788
- requireNonEmptyString(f["name"], `${path22}.name`, errors);
4789
- requireNonEmptyString(f["type"], `${path22}.type`, errors);
4917
+ requireNonEmptyString(f["name"], `${path24}.name`, errors);
4918
+ requireNonEmptyString(f["type"], `${path24}.type`, errors);
4790
4919
  if (typeof f["required"] !== "boolean") {
4791
- errors.push({ path: `${path22}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4920
+ errors.push({ path: `${path24}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4792
4921
  }
4793
4922
  }
4794
- function validateEndpoint(raw, path22, errors) {
4923
+ function validateEndpoint(raw, path24, errors) {
4795
4924
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4796
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
4925
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4797
4926
  return;
4798
4927
  }
4799
4928
  const e = raw;
4800
- requireNonEmptyString(e["id"], `${path22}.id`, errors);
4801
- requireNonEmptyString(e["description"], `${path22}.description`, errors);
4929
+ requireNonEmptyString(e["id"], `${path24}.id`, errors);
4930
+ requireNonEmptyString(e["description"], `${path24}.description`, errors);
4802
4931
  if (!VALID_METHODS.includes(e["method"])) {
4803
4932
  errors.push({
4804
- path: `${path22}.method`,
4933
+ path: `${path24}.method`,
4805
4934
  message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
4806
4935
  });
4807
4936
  }
4808
4937
  if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
4809
4938
  errors.push({
4810
- path: `${path22}.path`,
4939
+ path: `${path24}.path`,
4811
4940
  message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
4812
4941
  });
4813
4942
  }
4814
4943
  if (typeof e["auth"] !== "boolean") {
4815
- errors.push({ path: `${path22}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4944
+ errors.push({ path: `${path24}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4816
4945
  }
4817
4946
  if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
4818
4947
  errors.push({
4819
- path: `${path22}.successStatus`,
4948
+ path: `${path24}.successStatus`,
4820
4949
  message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
4821
4950
  });
4822
4951
  }
4823
- requireNonEmptyString(e["successDescription"], `${path22}.successDescription`, errors);
4952
+ requireNonEmptyString(e["successDescription"], `${path24}.successDescription`, errors);
4824
4953
  if (e["request"] !== void 0) {
4825
- validateRequestSchema(e["request"], `${path22}.request`, errors);
4954
+ validateRequestSchema(e["request"], `${path24}.request`, errors);
4826
4955
  }
4827
4956
  if (e["errors"] !== void 0) {
4828
4957
  if (!Array.isArray(e["errors"])) {
4829
- errors.push({ path: `${path22}.errors`, message: "Must be an array if present" });
4958
+ errors.push({ path: `${path24}.errors`, message: "Must be an array if present" });
4830
4959
  } else {
4831
4960
  const errs = e["errors"];
4832
4961
  if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
4833
- errors.push({ path: `${path22}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
4962
+ errors.push({ path: `${path24}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
4834
4963
  }
4835
4964
  for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
4836
- validateResponseError(errs[j2], `${path22}.errors[${j2}]`, errors);
4965
+ validateResponseError(errs[j2], `${path24}.errors[${j2}]`, errors);
4837
4966
  }
4838
4967
  }
4839
4968
  }
4840
4969
  }
4841
- function validateRequestSchema(raw, path22, errors) {
4970
+ function validateRequestSchema(raw, path24, errors) {
4842
4971
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4843
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
4972
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4844
4973
  return;
4845
4974
  }
4846
4975
  const r = raw;
4847
4976
  for (const key of ["body", "query", "params"]) {
4848
4977
  if (r[key] !== void 0) {
4849
- validateFieldMap(r[key], `${path22}.${key}`, errors);
4978
+ validateFieldMap(r[key], `${path24}.${key}`, errors);
4850
4979
  }
4851
4980
  }
4852
4981
  }
4853
- function validateFieldMap(raw, path22, errors) {
4982
+ function validateFieldMap(raw, path24, errors) {
4854
4983
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4855
- errors.push({ path: path22, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
4984
+ errors.push({ path: path24, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
4856
4985
  return;
4857
4986
  }
4858
4987
  const map = raw;
4859
4988
  for (const [k2, v2] of Object.entries(map)) {
4860
4989
  if (typeof v2 !== "string") {
4861
- errors.push({ path: `${path22}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
4990
+ errors.push({ path: `${path24}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
4862
4991
  }
4863
4992
  }
4864
4993
  }
4865
- function validateResponseError(raw, path22, errors) {
4994
+ function validateResponseError(raw, path24, errors) {
4866
4995
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4867
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
4996
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4868
4997
  return;
4869
4998
  }
4870
4999
  const e = raw;
4871
5000
  if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
4872
- errors.push({ path: `${path22}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5001
+ errors.push({ path: `${path24}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
4873
5002
  }
4874
- requireNonEmptyString(e["code"], `${path22}.code`, errors);
4875
- requireNonEmptyString(e["description"], `${path22}.description`, errors);
5003
+ requireNonEmptyString(e["code"], `${path24}.code`, errors);
5004
+ requireNonEmptyString(e["description"], `${path24}.description`, errors);
4876
5005
  }
4877
- function validateBehavior(raw, path22, errors) {
5006
+ function validateBehavior(raw, path24, errors) {
4878
5007
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4879
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
5008
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4880
5009
  return;
4881
5010
  }
4882
5011
  const b = raw;
4883
- requireNonEmptyString(b["id"], `${path22}.id`, errors);
4884
- requireNonEmptyString(b["description"], `${path22}.description`, errors);
5012
+ requireNonEmptyString(b["id"], `${path24}.id`, errors);
5013
+ requireNonEmptyString(b["description"], `${path24}.description`, errors);
4885
5014
  if (b["constraints"] !== void 0) {
4886
5015
  if (!Array.isArray(b["constraints"])) {
4887
- errors.push({ path: `${path22}.constraints`, message: "Must be an array of strings if present" });
5016
+ errors.push({ path: `${path24}.constraints`, message: "Must be an array of strings if present" });
4888
5017
  } else {
4889
5018
  const cs2 = b["constraints"];
4890
5019
  for (let j2 = 0; j2 < cs2.length; j2++) {
4891
5020
  if (typeof cs2[j2] !== "string") {
4892
- errors.push({ path: `${path22}.constraints[${j2}]`, message: "Must be a string" });
5021
+ errors.push({ path: `${path24}.constraints[${j2}]`, message: "Must be a string" });
4893
5022
  }
4894
5023
  }
4895
5024
  }
4896
5025
  }
4897
5026
  }
4898
- function validateComponent(raw, path22, errors) {
5027
+ function validateComponent(raw, path24, errors) {
4899
5028
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4900
- errors.push({ path: path22, message: `Must be an object, got: ${typeLabel(raw)}` });
5029
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4901
5030
  return;
4902
5031
  }
4903
5032
  const c = raw;
4904
- requireNonEmptyString(c["id"], `${path22}.id`, errors);
4905
- requireNonEmptyString(c["name"], `${path22}.name`, errors);
4906
- requireNonEmptyString(c["description"], `${path22}.description`, errors);
5033
+ requireNonEmptyString(c["id"], `${path24}.id`, errors);
5034
+ requireNonEmptyString(c["name"], `${path24}.name`, errors);
5035
+ requireNonEmptyString(c["description"], `${path24}.description`, errors);
4907
5036
  if (c["props"] !== void 0) {
4908
5037
  if (!Array.isArray(c["props"])) {
4909
- errors.push({ path: `${path22}.props`, message: "Must be an array if present" });
5038
+ errors.push({ path: `${path24}.props`, message: "Must be an array if present" });
4910
5039
  } else {
4911
5040
  const props = c["props"];
4912
5041
  for (let j2 = 0; j2 < props.length; j2++) {
4913
5042
  const p = props[j2];
4914
5043
  if (typeof p !== "object" || p === null) {
4915
- errors.push({ path: `${path22}.props[${j2}]`, message: "Must be an object" });
5044
+ errors.push({ path: `${path24}.props[${j2}]`, message: "Must be an object" });
4916
5045
  continue;
4917
5046
  }
4918
- requireNonEmptyString(p["name"], `${path22}.props[${j2}].name`, errors);
4919
- requireNonEmptyString(p["type"], `${path22}.props[${j2}].type`, errors);
5047
+ requireNonEmptyString(p["name"], `${path24}.props[${j2}].name`, errors);
5048
+ requireNonEmptyString(p["type"], `${path24}.props[${j2}].type`, errors);
4920
5049
  if (typeof p["required"] !== "boolean") {
4921
- errors.push({ path: `${path22}.props[${j2}].required`, message: "Must be boolean" });
5050
+ errors.push({ path: `${path24}.props[${j2}].required`, message: "Must be boolean" });
4922
5051
  }
4923
5052
  }
4924
5053
  }
4925
5054
  }
4926
5055
  if (c["events"] !== void 0) {
4927
5056
  if (!Array.isArray(c["events"])) {
4928
- errors.push({ path: `${path22}.events`, message: "Must be an array if present" });
5057
+ errors.push({ path: `${path24}.events`, message: "Must be an array if present" });
4929
5058
  } else {
4930
5059
  const events = c["events"];
4931
5060
  for (let j2 = 0; j2 < events.length; j2++) {
4932
5061
  const e = events[j2];
4933
5062
  if (typeof e !== "object" || e === null) {
4934
- errors.push({ path: `${path22}.events[${j2}]`, message: "Must be an object" });
5063
+ errors.push({ path: `${path24}.events[${j2}]`, message: "Must be an object" });
4935
5064
  continue;
4936
5065
  }
4937
- requireNonEmptyString(e["name"], `${path22}.events[${j2}].name`, errors);
5066
+ requireNonEmptyString(e["name"], `${path24}.events[${j2}].name`, errors);
4938
5067
  }
4939
5068
  }
4940
5069
  }
4941
5070
  if (c["state"] !== void 0) {
4942
5071
  if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
4943
- errors.push({ path: `${path22}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5072
+ errors.push({ path: `${path24}.state`, message: "Must be a flat object (Record<string, string>) if present" });
4944
5073
  }
4945
5074
  }
4946
5075
  if (c["apiCalls"] !== void 0) {
4947
5076
  if (!Array.isArray(c["apiCalls"])) {
4948
- errors.push({ path: `${path22}.apiCalls`, message: "Must be an array of strings if present" });
5077
+ errors.push({ path: `${path24}.apiCalls`, message: "Must be an array of strings if present" });
4949
5078
  }
4950
5079
  }
4951
5080
  }
4952
- function requireNonEmptyString(v2, path22, errors) {
5081
+ function requireNonEmptyString(v2, path24, errors) {
4953
5082
  if (typeof v2 !== "string" || v2.trim().length === 0) {
4954
5083
  errors.push({
4955
- path: path22,
5084
+ path: path24,
4956
5085
  message: `Must be a non-empty string, got: ${typeLabel(v2)}`
4957
5086
  });
4958
5087
  }
@@ -4963,29 +5092,29 @@ function typeLabel(v2) {
4963
5092
  return typeof v2;
4964
5093
  }
4965
5094
  function printValidationErrors(errors) {
4966
- console.log(chalk4.red(`
5095
+ console.log(chalk5.red(`
4967
5096
  DSL Validation failed \u2014 ${errors.length} error(s):
4968
5097
  `));
4969
5098
  for (const err of errors) {
4970
- console.log(chalk4.red(` \u2718 ${chalk4.bold(err.path)}: ${err.message}`));
5099
+ console.log(chalk5.red(` \u2718 ${chalk5.bold(err.path)}: ${err.message}`));
4971
5100
  }
4972
5101
  console.log();
4973
5102
  }
4974
5103
  function printDslSummary(dsl) {
4975
- console.log(chalk4.green(" \u2714 DSL valid"));
4976
- console.log(chalk4.gray(` Models : ${dsl.models.length}`));
4977
- console.log(chalk4.gray(` Endpoints : ${dsl.endpoints.length}`));
4978
- console.log(chalk4.gray(` Behaviors : ${dsl.behaviors.length}`));
5104
+ console.log(chalk5.green(" \u2714 DSL valid"));
5105
+ console.log(chalk5.gray(` Models : ${dsl.models.length}`));
5106
+ console.log(chalk5.gray(` Endpoints : ${dsl.endpoints.length}`));
5107
+ console.log(chalk5.gray(` Behaviors : ${dsl.behaviors.length}`));
4979
5108
  if (dsl.components && dsl.components.length > 0) {
4980
- console.log(chalk4.gray(` Components: ${dsl.components.length}`));
5109
+ console.log(chalk5.gray(` Components: ${dsl.components.length}`));
4981
5110
  for (const cmp of dsl.components) {
4982
- console.log(chalk4.gray(` ${cmp.id} ${cmp.name} \u2014 props:${cmp.props.length} events:${cmp.events.length}`));
5111
+ console.log(chalk5.gray(` ${cmp.id} ${cmp.name} \u2014 props:${cmp.props.length} events:${cmp.events.length}`));
4983
5112
  }
4984
5113
  }
4985
5114
  if (dsl.endpoints.length > 0) {
4986
5115
  for (const ep of dsl.endpoints) {
4987
- const auth = ep.auth ? chalk4.yellow(" [auth]") : "";
4988
- console.log(chalk4.gray(` ${ep.method.padEnd(6)} ${ep.path}${auth} \u2014 ${ep.description}`));
5116
+ const auth = ep.auth ? chalk5.yellow(" [auth]") : "";
5117
+ console.log(chalk5.gray(` ${ep.method.padEnd(6)} ${ep.path}${auth} \u2014 ${ep.description}`));
4989
5118
  }
4990
5119
  }
4991
5120
  }
@@ -5238,7 +5367,7 @@ var DslExtractor = class {
5238
5367
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
5239
5368
  const isRetry = attempt > 1;
5240
5369
  if (isRetry) {
5241
- console.log(chalk5.yellow(`
5370
+ console.log(chalk6.yellow(`
5242
5371
  Retry ${attempt - 1}/${MAX_RETRIES - 1}: fixing validation errors...`));
5243
5372
  }
5244
5373
  const activeSystemPrompt = opts.isFrontend ? dslFrontendSystemPrompt : dslSystemPrompt;
@@ -5247,7 +5376,7 @@ var DslExtractor = class {
5247
5376
  try {
5248
5377
  rawOutput = await this.provider.generate(userPrompt, activeSystemPrompt);
5249
5378
  } catch (err) {
5250
- console.log(chalk5.red(` \u2718 AI call failed: ${err.message}`));
5379
+ console.log(chalk6.red(` \u2718 AI call failed: ${err.message}`));
5251
5380
  return this.handleFailure(opts, "AI call failed");
5252
5381
  }
5253
5382
  lastRawOutput = rawOutput;
@@ -5255,7 +5384,7 @@ var DslExtractor = class {
5255
5384
  try {
5256
5385
  parsed = parseJsonFromOutput(rawOutput);
5257
5386
  } catch (parseErr) {
5258
- console.log(chalk5.red(` \u2718 Failed to parse JSON from AI output: ${parseErr.message}`));
5387
+ console.log(chalk6.red(` \u2718 Failed to parse JSON from AI output: ${parseErr.message}`));
5259
5388
  lastErrors = [{ path: "root", message: "Output is not valid JSON \u2014 see raw output above" }];
5260
5389
  if (attempt < MAX_RETRIES) continue;
5261
5390
  return this.handleFailure(opts, "AI produced invalid JSON after retries");
@@ -5268,7 +5397,7 @@ var DslExtractor = class {
5268
5397
  printValidationErrors(result.errors);
5269
5398
  lastErrors = result.errors;
5270
5399
  if (attempt < MAX_RETRIES) {
5271
- console.log(chalk5.gray(` Will retry with error feedback...`));
5400
+ console.log(chalk6.gray(` Will retry with error feedback...`));
5272
5401
  continue;
5273
5402
  }
5274
5403
  return this.handleFailure(opts, `DSL validation failed after ${MAX_RETRIES} attempts`);
@@ -5280,10 +5409,10 @@ var DslExtractor = class {
5280
5409
  * Returns null to skip, or throws to abort the pipeline.
5281
5410
  */
5282
5411
  async handleFailure(opts, reason) {
5283
- console.log(chalk5.yellow(`
5412
+ console.log(chalk6.yellow(`
5284
5413
  \u26A0 DSL extraction failed: ${reason}`));
5285
5414
  if (opts.auto) {
5286
- console.log(chalk5.gray(" --auto mode: skipping DSL, continuing without it."));
5415
+ console.log(chalk6.gray(" --auto mode: skipping DSL, continuing without it."));
5287
5416
  return null;
5288
5417
  }
5289
5418
  const action = await select2({
@@ -5294,10 +5423,10 @@ var DslExtractor = class {
5294
5423
  ]
5295
5424
  });
5296
5425
  if (action === "abort") {
5297
- console.log(chalk5.red(" Pipeline aborted by user."));
5426
+ console.log(chalk6.red(" Pipeline aborted by user."));
5298
5427
  process.exit(1);
5299
5428
  }
5300
- console.log(chalk5.gray(" Continuing without DSL."));
5429
+ console.log(chalk6.gray(" Continuing without DSL."));
5301
5430
  return null;
5302
5431
  }
5303
5432
  // ─── Save ────────────────────────────────────────────────────────────────────
@@ -5574,7 +5703,7 @@ ${preview}`);
5574
5703
  } catch {
5575
5704
  }
5576
5705
  }
5577
- const httpImportRegex = /^import\s+(?:\w+|\{[^}]+\})\s+from\s+['"](@\/[^'"]+|axios|ky)['"]/m;
5706
+ 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;
5578
5707
  for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
5579
5708
  try {
5580
5709
  const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
@@ -5587,31 +5716,51 @@ ${preview}`);
5587
5716
  }
5588
5717
  }
5589
5718
  const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
5590
- const paginationInterfaceRegex = new RegExp(
5591
- `(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
5592
- "s"
5593
- );
5594
- const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
5595
5719
  for (const relPath of ctx.existingApiFiles) {
5596
5720
  if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
5597
5721
  try {
5598
5722
  const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
5599
5723
  if (!paginationFieldNames.some((f) => content.includes(f))) continue;
5600
- const interfaceMatch = content.match(paginationInterfaceRegex);
5601
- if (!interfaceMatch) continue;
5602
- const interfaceName = interfaceMatch[1];
5603
- const fnRegex = new RegExp(
5604
- `export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${interfaceName}[^)]*\\)[\\s\\S]*?\\n\\}`,
5605
- ""
5606
- );
5607
- const fnMatch = content.match(fnRegex);
5608
- if (fnMatch) {
5724
+ const lines = content.split("\n");
5725
+ let interfaceName = "";
5726
+ let interfaceBlock = "";
5727
+ for (let i = 0; i < lines.length; i++) {
5728
+ const m = lines[i].match(/(?:interface|type)\s+(\w*(?:Params|Query|Request|Filter|Page)\w*)\s*[={<]/);
5729
+ if (!m) continue;
5730
+ const blockLines = [];
5731
+ let depth = 0;
5732
+ for (let j2 = i; j2 < Math.min(i + 40, lines.length); j2++) {
5733
+ blockLines.push(lines[j2]);
5734
+ depth += (lines[j2].match(/\{/g) ?? []).length;
5735
+ depth -= (lines[j2].match(/\}/g) ?? []).length;
5736
+ if (depth === 0 && j2 > i) break;
5737
+ }
5738
+ const blockText = blockLines.join("\n");
5739
+ if (!paginationFieldNames.some((f) => new RegExp(`\\b${f}\\b`).test(blockText))) continue;
5740
+ interfaceName = m[1];
5741
+ interfaceBlock = blockText;
5742
+ break;
5743
+ }
5744
+ if (!interfaceName) continue;
5745
+ for (let i = 0; i < lines.length; i++) {
5746
+ const line = lines[i];
5747
+ if (!/export\s+(async\s+)?(function|const)/.test(line)) continue;
5748
+ if (!line.includes(interfaceName)) continue;
5749
+ const fnLines = [];
5750
+ let depth = 0;
5751
+ for (let j2 = i; j2 < Math.min(i + 30, lines.length); j2++) {
5752
+ fnLines.push(lines[j2]);
5753
+ depth += (lines[j2].match(/\{/g) ?? []).length;
5754
+ depth -= (lines[j2].match(/\}/g) ?? []).length;
5755
+ if (depth === 0 && j2 > i) break;
5756
+ }
5609
5757
  ctx.paginationExample = `// From ${relPath}
5610
- ${interfaceMatch[0]}
5758
+ ${interfaceBlock}
5611
5759
 
5612
- ${fnMatch[0]}`;
5760
+ ${fnLines.join("\n")}`;
5613
5761
  break;
5614
5762
  }
5763
+ if (ctx.paginationExample) break;
5615
5764
  } catch {
5616
5765
  }
5617
5766
  }
@@ -5824,6 +5973,152 @@ Shared component structure patterns:`);
5824
5973
  return lines.join("\n");
5825
5974
  }
5826
5975
 
5976
+ // core/run-snapshot.ts
5977
+ import * as fs8 from "fs-extra";
5978
+ import * as path7 from "path";
5979
+ var BACKUP_DIR = ".ai-spec-backup";
5980
+ var RunSnapshot = class {
5981
+ constructor(workingDir, runId) {
5982
+ this.workingDir = workingDir;
5983
+ this.runId = runId;
5984
+ this.backupRoot = path7.join(workingDir, BACKUP_DIR, runId);
5985
+ }
5986
+ backupRoot;
5987
+ snapshotted = /* @__PURE__ */ new Set();
5988
+ /**
5989
+ * Snapshot a file before it gets overwritten.
5990
+ * No-op if the file does not exist yet (new file — nothing to restore).
5991
+ * No-op if this file was already snapshotted in this run.
5992
+ */
5993
+ async snapshotFile(filePath) {
5994
+ const fullPath = path7.isAbsolute(filePath) ? filePath : path7.join(this.workingDir, filePath);
5995
+ if (!await fs8.pathExists(fullPath)) return;
5996
+ if (this.snapshotted.has(fullPath)) return;
5997
+ const relative7 = path7.relative(this.workingDir, fullPath);
5998
+ const dest = path7.join(this.backupRoot, relative7);
5999
+ await fs8.ensureDir(path7.dirname(dest));
6000
+ await fs8.copy(fullPath, dest);
6001
+ this.snapshotted.add(fullPath);
6002
+ }
6003
+ /** Restore all snapshotted files. Returns list of restored relative paths. */
6004
+ async restore() {
6005
+ if (!await fs8.pathExists(this.backupRoot)) return [];
6006
+ const restored = [];
6007
+ const walk = async (dir) => {
6008
+ for (const entry of await fs8.readdir(dir, { withFileTypes: true })) {
6009
+ const full = path7.join(dir, entry.name);
6010
+ if (entry.isDirectory()) {
6011
+ await walk(full);
6012
+ } else {
6013
+ const relative7 = path7.relative(this.backupRoot, full);
6014
+ const dest = path7.join(this.workingDir, relative7);
6015
+ await fs8.ensureDir(path7.dirname(dest));
6016
+ await fs8.copy(full, dest, { overwrite: true });
6017
+ restored.push(relative7);
6018
+ }
6019
+ }
6020
+ };
6021
+ await walk(this.backupRoot);
6022
+ return restored;
6023
+ }
6024
+ get fileCount() {
6025
+ return this.snapshotted.size;
6026
+ }
6027
+ };
6028
+ var _activeSnapshot = null;
6029
+ function setActiveSnapshot(snapshot) {
6030
+ _activeSnapshot = snapshot;
6031
+ }
6032
+ function getActiveSnapshot() {
6033
+ return _activeSnapshot;
6034
+ }
6035
+
6036
+ // core/run-logger.ts
6037
+ import * as fs9 from "fs-extra";
6038
+ import * as path8 from "path";
6039
+ import chalk7 from "chalk";
6040
+ var LOG_DIR = ".ai-spec-logs";
6041
+ var RunLogger = class {
6042
+ constructor(workingDir, runId, meta) {
6043
+ this.workingDir = workingDir;
6044
+ this.runId = runId;
6045
+ this.startMs = Date.now();
6046
+ this.logPath = path8.join(workingDir, LOG_DIR, `${runId}.json`);
6047
+ this.log = {
6048
+ runId,
6049
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6050
+ workingDir,
6051
+ ...meta,
6052
+ entries: [],
6053
+ filesWritten: [],
6054
+ errors: []
6055
+ };
6056
+ this.flush();
6057
+ }
6058
+ log;
6059
+ startMs;
6060
+ logPath;
6061
+ stageStartMs = /* @__PURE__ */ new Map();
6062
+ stageStart(event, data) {
6063
+ this.stageStartMs.set(event, Date.now());
6064
+ this.push(event, data);
6065
+ }
6066
+ stageEnd(event, data) {
6067
+ const start = this.stageStartMs.get(event);
6068
+ const durationMs = start !== void 0 ? Date.now() - start : void 0;
6069
+ this.push(`${event}:done`, { ...data, durationMs });
6070
+ }
6071
+ stageFail(event, error, data) {
6072
+ const start = this.stageStartMs.get(event);
6073
+ const durationMs = start !== void 0 ? Date.now() - start : void 0;
6074
+ this.push(`${event}:failed`, { ...data, error, durationMs });
6075
+ this.log.errors.push(`[${event}] ${error}`);
6076
+ this.flush();
6077
+ }
6078
+ fileWritten(filePath) {
6079
+ if (!this.log.filesWritten.includes(filePath)) {
6080
+ this.log.filesWritten.push(filePath);
6081
+ this.flush();
6082
+ }
6083
+ }
6084
+ finish() {
6085
+ this.log.endedAt = (/* @__PURE__ */ new Date()).toISOString();
6086
+ this.log.totalDurationMs = Date.now() - this.startMs;
6087
+ this.flush();
6088
+ }
6089
+ printSummary() {
6090
+ const dur = this.log.totalDurationMs ? ` in ${(this.log.totalDurationMs / 1e3).toFixed(1)}s` : "";
6091
+ const errPart = this.log.errors.length > 0 ? chalk7.yellow(` \xB7 ${this.log.errors.length} error(s)`) : "";
6092
+ console.log(
6093
+ chalk7.gray(`
6094
+ Run ID: ${chalk7.white(this.runId)}${dur}`) + chalk7.gray(` \xB7 ${this.log.filesWritten.length} file(s) written`) + errPart
6095
+ );
6096
+ console.log(chalk7.gray(` Log : ${path8.relative(this.workingDir, this.logPath)}`));
6097
+ }
6098
+ push(event, data) {
6099
+ this.log.entries.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...data ? { data } : {} });
6100
+ this.flush();
6101
+ }
6102
+ flush() {
6103
+ fs9.ensureDir(path8.dirname(this.logPath)).then(() => fs9.writeJson(this.logPath, this.log, { spaces: 2 })).catch(() => {
6104
+ });
6105
+ }
6106
+ };
6107
+ function generateRunId() {
6108
+ const now = /* @__PURE__ */ new Date();
6109
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
6110
+ const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
6111
+ const rand = Math.random().toString(36).slice(2, 6);
6112
+ return `${date}-${time}-${rand}`;
6113
+ }
6114
+ var _activeLogger = null;
6115
+ function setActiveLogger(logger) {
6116
+ _activeLogger = logger;
6117
+ }
6118
+ function getActiveLogger() {
6119
+ return _activeLogger;
6120
+ }
6121
+
5827
6122
  // core/code-generator.ts
5828
6123
  function buildSharedConfigSection(context) {
5829
6124
  if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
@@ -5990,13 +6285,13 @@ var CodeGenerator = class {
5990
6285
  let effectiveMode = this.mode;
5991
6286
  if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
5992
6287
  console.log(
5993
- chalk6.yellow(
6288
+ chalk8.yellow(
5994
6289
  `
5995
6290
  \u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
5996
6291
  )
5997
6292
  );
5998
- 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`));
5999
- console.log(chalk6.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
6293
+ 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`));
6294
+ console.log(chalk8.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
6000
6295
  `));
6001
6296
  effectiveMode = "api";
6002
6297
  }
@@ -6021,16 +6316,16 @@ var CodeGenerator = class {
6021
6316
  }
6022
6317
  }
6023
6318
  async runClaudeCode(specFilePath, workingDir, options = {}) {
6024
- console.log(chalk6.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6319
+ console.log(chalk8.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6025
6320
  if (!this.isClaudeCLIAvailable()) {
6026
- console.log(chalk6.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
6027
- console.log(chalk6.gray(" Install: npm install -g @anthropic-ai/claude-code"));
6321
+ console.log(chalk8.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
6322
+ console.log(chalk8.gray(" Install: npm install -g @anthropic-ai/claude-code"));
6028
6323
  return this.runPlanMode(specFilePath);
6029
6324
  }
6030
6325
  const rtkAvailable = isRtkAvailable();
6031
6326
  const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
6032
6327
  if (rtkAvailable) {
6033
- console.log(chalk6.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
6328
+ console.log(chalk8.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
6034
6329
  }
6035
6330
  const tasks = await loadTasksForSpec(specFilePath);
6036
6331
  if (options.auto && tasks && tasks.length > 0) {
@@ -6043,30 +6338,30 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
6043
6338
  Files: ${t.filesToTouch.join(", ")}
6044
6339
  Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
6045
6340
  const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
6046
- const promptFile = path7.join(workingDir, ".claude-prompt.txt");
6047
- await fs8.writeFile(promptFile, promptContent, "utf-8");
6341
+ const promptFile = path9.join(workingDir, ".claude-prompt.txt");
6342
+ await fs10.writeFile(promptFile, promptContent, "utf-8");
6048
6343
  if (options.auto) {
6049
- console.log(chalk6.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
6050
- console.log(chalk6.gray(` Spec: ${specFilePath}`));
6344
+ console.log(chalk8.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
6345
+ console.log(chalk8.gray(` Spec: ${specFilePath}`));
6051
6346
  try {
6052
6347
  execSync(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
6053
6348
  cwd: workingDir,
6054
6349
  stdio: "inherit"
6055
6350
  });
6056
- console.log(chalk6.green("\n \u2714 Claude Code completed."));
6351
+ console.log(chalk8.green("\n \u2714 Claude Code completed."));
6057
6352
  } catch {
6058
- console.log(chalk6.yellow("\n Claude Code exited. Check output above."));
6353
+ console.log(chalk8.yellow("\n Claude Code exited. Check output above."));
6059
6354
  }
6060
6355
  } else {
6061
- console.log(chalk6.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
6062
- console.log(chalk6.gray(` Spec: ${specFilePath}`));
6063
- if (tasks) console.log(chalk6.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
6064
- console.log(chalk6.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
6356
+ console.log(chalk8.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
6357
+ console.log(chalk8.gray(` Spec: ${specFilePath}`));
6358
+ if (tasks) console.log(chalk8.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
6359
+ console.log(chalk8.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
6065
6360
  try {
6066
6361
  execSync(claudeCmd, { cwd: workingDir, stdio: "inherit" });
6067
- console.log(chalk6.green("\n \u2714 Claude Code session completed."));
6362
+ console.log(chalk8.green("\n \u2714 Claude Code session completed."));
6068
6363
  } catch {
6069
- console.log(chalk6.yellow("\n Claude Code session ended. Continuing workflow."));
6364
+ console.log(chalk8.yellow("\n Claude Code session ended. Continuing workflow."));
6070
6365
  }
6071
6366
  }
6072
6367
  }
@@ -6079,10 +6374,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
6079
6374
  const pending = tasks.filter((t) => t.status !== "done");
6080
6375
  const doneCount = tasks.length - pending.length;
6081
6376
  if (options.resume && doneCount > 0) {
6082
- console.log(chalk6.cyan(`
6377
+ console.log(chalk8.cyan(`
6083
6378
  Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
6084
6379
  } else {
6085
- console.log(chalk6.cyan(`
6380
+ console.log(chalk8.cyan(`
6086
6381
  Incremental mode: ${tasks.length} tasks`));
6087
6382
  }
6088
6383
  let completed = doneCount;
@@ -6110,32 +6405,32 @@ Implement ONLY this task. Do not implement other tasks.`;
6110
6405
  completed++;
6111
6406
  } catch {
6112
6407
  taskStatus = "failed";
6113
- console.log(chalk6.yellow(`
6408
+ console.log(chalk8.yellow(`
6114
6409
  \u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
6115
6410
  }
6116
6411
  await updateTaskStatus(specFilePath, task.id, taskStatus);
6117
6412
  }
6118
6413
  const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
6119
6414
  console.log(
6120
- chalk6.bold(
6415
+ chalk8.bold(
6121
6416
  `
6122
- ${successCount === tasks.length ? chalk6.green("\u2714") : chalk6.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
6417
+ ${successCount === tasks.length ? chalk8.green("\u2714") : chalk8.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
6123
6418
  )
6124
6419
  );
6125
6420
  }
6126
6421
  // ── Mode: api ─────────────────────────────────────────────────────────────
6127
6422
  async runApiMode(specFilePath, workingDir, context, options = {}) {
6128
6423
  console.log(
6129
- chalk6.blue(
6424
+ chalk8.blue(
6130
6425
  `
6131
6426
  \u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
6132
6427
  )
6133
6428
  );
6134
6429
  const systemPrompt = getCodeGenSystemPrompt(options.repoType);
6135
6430
  if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
6136
- console.log(chalk6.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
6431
+ console.log(chalk8.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
6137
6432
  }
6138
- const spec = await fs8.readFile(specFilePath, "utf-8");
6433
+ const spec = await fs10.readFile(specFilePath, "utf-8");
6139
6434
  const constitutionSection = context?.constitution ? `
6140
6435
  === Project Constitution (MUST follow) ===
6141
6436
  ${context.constitution}
@@ -6151,7 +6446,7 @@ ${buildDslContextSection(dsl)}
6151
6446
  if (dsl) {
6152
6447
  const cmpCount = dsl.components?.length ?? 0;
6153
6448
  const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
6154
- console.log(chalk6.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
6449
+ console.log(chalk8.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
6155
6450
  }
6156
6451
  const isFrontend = isFrontendDeps(context?.dependencies ?? []);
6157
6452
  let frontendSection = "";
@@ -6160,13 +6455,13 @@ ${buildDslContextSection(dsl)}
6160
6455
  frontendSection = `
6161
6456
  ${buildFrontendContextSection(fctx)}
6162
6457
  `;
6163
- console.log(chalk6.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6458
+ console.log(chalk8.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6164
6459
  }
6165
6460
  const tasks = await loadTasksForSpec(specFilePath);
6166
6461
  if (tasks && tasks.length > 0) {
6167
6462
  return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
6168
6463
  }
6169
- console.log(chalk6.gray(" [1/2] Planning implementation files..."));
6464
+ console.log(chalk8.gray(" [1/2] Planning implementation files..."));
6170
6465
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
6171
6466
 
6172
6467
  IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
@@ -6189,18 +6484,18 @@ Output ONLY a valid JSON array:
6189
6484
  const planResponse = await this.provider.generate(planPrompt, systemPrompt);
6190
6485
  filePlan = parseJsonArray(planResponse);
6191
6486
  } catch (err) {
6192
- console.error(chalk6.red(" Failed to generate file plan:"), err);
6487
+ console.error(chalk8.red(" Failed to generate file plan:"), err);
6193
6488
  }
6194
6489
  if (filePlan.length === 0) {
6195
- console.log(chalk6.yellow(" Could not determine file plan. Falling back to plan mode."));
6490
+ console.log(chalk8.yellow(" Could not determine file plan. Falling back to plan mode."));
6196
6491
  await this.runPlanMode(specFilePath);
6197
6492
  return [];
6198
6493
  }
6199
- console.log(chalk6.cyan(`
6494
+ console.log(chalk8.cyan(`
6200
6495
  Plan: ${filePlan.length} file(s) to process`));
6201
6496
  filePlan.forEach((item) => {
6202
- const icon = item.action === "create" ? chalk6.green("+") : chalk6.yellow("~");
6203
- console.log(` ${icon} ${item.file}: ${chalk6.gray(item.description)}`);
6497
+ const icon = item.action === "create" ? chalk8.green("+") : chalk8.yellow("~");
6498
+ console.log(` ${icon} ${item.file}: ${chalk8.gray(item.description)}`);
6204
6499
  });
6205
6500
  const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
6206
6501
  return files;
@@ -6209,13 +6504,13 @@ Output ONLY a valid JSON array:
6209
6504
  const pendingTasks = tasks.filter((t) => t.status !== "done");
6210
6505
  const doneCount = tasks.length - pendingTasks.length;
6211
6506
  if (options.resume && doneCount > 0) {
6212
- console.log(chalk6.cyan(`
6213
- Task-based generation (resume): ${tasks.length} tasks (${chalk6.green(doneCount + " already done")}, skipping)`));
6507
+ console.log(chalk8.cyan(`
6508
+ Task-based generation (resume): ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, skipping)`));
6214
6509
  } else if (doneCount > 0) {
6215
- console.log(chalk6.cyan(`
6216
- Task-based generation: ${tasks.length} tasks (${chalk6.green(doneCount + " already done")}, resuming from checkpoint)`));
6510
+ console.log(chalk8.cyan(`
6511
+ Task-based generation: ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, resuming from checkpoint)`));
6217
6512
  } else {
6218
- console.log(chalk6.cyan(`
6513
+ console.log(chalk8.cyan(`
6219
6514
  Task-based generation: ${tasks.length} tasks`));
6220
6515
  }
6221
6516
  const sharedConfigPaths = new Set(
@@ -6247,9 +6542,9 @@ Output ONLY a valid JSON array:
6247
6542
  const pct = Math.round(completedTasks / tasks.length * 100);
6248
6543
  const barWidth = 20;
6249
6544
  const filled = Math.round(pct / 100 * barWidth);
6250
- const bar = chalk6.green("\u2588".repeat(filled)) + chalk6.gray("\u2591".repeat(barWidth - filled));
6545
+ const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
6251
6546
  console.log(
6252
- chalk6.bold(`
6547
+ chalk8.bold(`
6253
6548
  [${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
6254
6549
  );
6255
6550
  } else {
@@ -6257,12 +6552,12 @@ Output ONLY a valid JSON array:
6257
6552
  }
6258
6553
  const executeTask = async (task, batchIsParallel) => {
6259
6554
  if (task.filesToTouch.length === 0) {
6260
- if (!batchIsParallel) console.log(chalk6.gray(" No files specified, skipping."));
6555
+ if (!batchIsParallel) console.log(chalk8.gray(" No files specified, skipping."));
6261
6556
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6262
6557
  }
6263
6558
  const filePlan = await Promise.all(
6264
6559
  task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
6265
- const exists = await fs8.pathExists(path7.join(workingDir, f));
6560
+ const exists = await fs10.pathExists(path9.join(workingDir, f));
6266
6561
  return {
6267
6562
  file: f,
6268
6563
  action: exists ? "modify" : "create",
@@ -6302,7 +6597,7 @@ ${taskContext}`,
6302
6597
  const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
6303
6598
  if (isCodeFile || isViewFile) {
6304
6599
  try {
6305
- const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs8.readFile(path7.join(workingDir, writtenFile), "utf-8");
6600
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path9.join(workingDir, writtenFile), "utf-8");
6306
6601
  generatedFileCache.set(writtenFile, content);
6307
6602
  } catch {
6308
6603
  }
@@ -6314,7 +6609,12 @@ ${taskContext}`,
6314
6609
  const layerResults = [];
6315
6610
  for (const batch of taskBatches) {
6316
6611
  const batchIsParallel = batch.length > 1;
6317
- const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
6612
+ const batchResultPromises = batch.map(
6613
+ (task) => executeTask(task, batchIsParallel).catch((err) => {
6614
+ console.log(chalk8.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
6615
+ return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6616
+ })
6617
+ );
6318
6618
  const batchResults = await Promise.all(batchResultPromises);
6319
6619
  layerResults.push(...batchResults);
6320
6620
  await updateCacheFromBatch(batchResults);
@@ -6327,14 +6627,14 @@ ${taskContext}`,
6327
6627
  totalFiles += result.total;
6328
6628
  allGeneratedFiles.push(...result.files);
6329
6629
  if (isParallel) {
6330
- const icon = result.success === result.total ? chalk6.green("\u2714") : chalk6.yellow("!");
6630
+ const icon = result.success === result.total ? chalk8.green("\u2714") : chalk8.yellow("!");
6331
6631
  const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
6332
6632
  console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
6333
6633
  }
6334
6634
  const taskStatus = result.success === result.total ? "done" : "failed";
6335
6635
  await updateTaskStatus(specFilePath, result.task.id, taskStatus);
6336
6636
  if (taskStatus === "failed") {
6337
- console.log(chalk6.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
6637
+ console.log(chalk8.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
6338
6638
  }
6339
6639
  }
6340
6640
  completedTasks += layerTasks.length;
@@ -6343,13 +6643,13 @@ ${taskContext}`,
6343
6643
  const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
6344
6644
  for (const sharedFile of context.sharedConfigFiles) {
6345
6645
  if (processedSharedConfigs.has(sharedFile.path)) continue;
6346
- const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path7.basename(f).replace(/\.[jt]sx?$/, ""));
6646
+ const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path9.basename(f).replace(/\.[jt]sx?$/, ""));
6347
6647
  if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
6348
6648
  let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
6349
6649
  if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
6350
6650
  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.`;
6351
6651
  }
6352
- console.log(chalk6.gray(`
6652
+ console.log(chalk8.gray(`
6353
6653
  + updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
6354
6654
  const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
6355
6655
  await this.generateFiles(
@@ -6367,26 +6667,26 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
6367
6667
  }
6368
6668
  }
6369
6669
  console.log(
6370
- chalk6.bold(
6670
+ chalk8.bold(
6371
6671
  `
6372
- ${totalSuccess === totalFiles ? chalk6.green("\u2714") : chalk6.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
6672
+ ${totalSuccess === totalFiles ? chalk8.green("\u2714") : chalk8.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
6373
6673
  )
6374
6674
  );
6375
6675
  return allGeneratedFiles;
6376
6676
  }
6377
6677
  async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
6378
- const prefix = taskLabel ? ` [${chalk6.cyan(taskLabel)}] ` : " ";
6678
+ const prefix = taskLabel ? ` [${chalk8.cyan(taskLabel)}] ` : " ";
6379
6679
  if (!taskLabel) {
6380
- console.log(chalk6.gray(`
6680
+ console.log(chalk8.gray(`
6381
6681
  Generating ${filePlan.length} file(s)...`));
6382
6682
  }
6383
6683
  let successCount = 0;
6384
6684
  const writtenFiles = [];
6385
6685
  for (const item of filePlan) {
6386
- const fullPath = path7.join(workingDir, item.file);
6686
+ const fullPath = path9.join(workingDir, item.file);
6387
6687
  let existingContent = "";
6388
- if (await fs8.pathExists(fullPath)) {
6389
- existingContent = await fs8.readFile(fullPath, "utf-8");
6688
+ if (await fs10.pathExists(fullPath)) {
6689
+ existingContent = await fs10.readFile(fullPath, "utf-8");
6390
6690
  }
6391
6691
  const codePrompt = `Implement this file.
6392
6692
 
@@ -6401,19 +6701,21 @@ ${existingContent || "Output only the complete file content."}`;
6401
6701
  try {
6402
6702
  const raw = await this.provider.generate(codePrompt, systemPrompt);
6403
6703
  const fileContent = stripCodeFences(raw);
6404
- await fs8.ensureDir(path7.dirname(fullPath));
6405
- await fs8.writeFile(fullPath, fileContent, "utf-8");
6406
- console.log(`${prefix}${existingContent ? chalk6.yellow("~") : chalk6.green("+")} ${chalk6.bold(item.file)} ${chalk6.green("\u2714")}`);
6704
+ await getActiveSnapshot()?.snapshotFile(fullPath);
6705
+ await fs10.ensureDir(path9.dirname(fullPath));
6706
+ await fs10.writeFile(fullPath, fileContent, "utf-8");
6707
+ getActiveLogger()?.fileWritten(item.file);
6708
+ console.log(`${prefix}${existingContent ? chalk8.yellow("~") : chalk8.green("+")} ${chalk8.bold(item.file)} ${chalk8.green("\u2714")}`);
6407
6709
  successCount++;
6408
6710
  writtenFiles.push(item.file);
6409
6711
  } catch (err) {
6410
- console.log(`${prefix}${chalk6.red("\u2718")} ${chalk6.bold(item.file)} \u2014 ${chalk6.red(err.message)}`);
6712
+ console.log(`${prefix}${chalk8.red("\u2718")} ${chalk8.bold(item.file)} \u2014 ${chalk8.red(err.message)}`);
6411
6713
  }
6412
6714
  }
6413
6715
  if (!taskLabel) {
6414
6716
  console.log(
6415
- chalk6.bold(
6416
- ` ${successCount === filePlan.length ? chalk6.green("\u2714") : chalk6.yellow("!")} ${successCount}/${filePlan.length} files written.`
6717
+ chalk8.bold(
6718
+ ` ${successCount === filePlan.length ? chalk8.green("\u2714") : chalk8.yellow("!")} ${successCount}/${filePlan.length} files written.`
6417
6719
  )
6418
6720
  );
6419
6721
  }
@@ -6421,8 +6723,8 @@ ${existingContent || "Output only the complete file content."}`;
6421
6723
  }
6422
6724
  // ── Mode: plan ─────────────────────────────────────────────────────────────
6423
6725
  async runPlanMode(specFilePath) {
6424
- 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"));
6425
- const spec = await fs8.readFile(specFilePath, "utf-8");
6726
+ 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"));
6727
+ const spec = await fs10.readFile(specFilePath, "utf-8");
6426
6728
  const plan = await this.provider.generate(
6427
6729
  `Create a detailed, step-by-step implementation plan for the following feature spec.
6428
6730
  Be specific about:
@@ -6434,7 +6736,7 @@ Be specific about:
6434
6736
  ${spec}`,
6435
6737
  "You are a senior developer creating an actionable implementation guide."
6436
6738
  );
6437
- console.log(chalk6.cyan("\n") + plan);
6739
+ console.log(chalk8.cyan("\n") + plan);
6438
6740
  }
6439
6741
  };
6440
6742
  function topoSortLayerTasks(tasks) {
@@ -6485,16 +6787,16 @@ function printTaskProgress(completed, total, task, mode) {
6485
6787
  const pct = total > 0 ? Math.round(completed / total * 100) : 0;
6486
6788
  const barWidth = 20;
6487
6789
  const filled = Math.round(pct / 100 * barWidth);
6488
- const bar = chalk6.green("\u2588".repeat(filled)) + chalk6.gray("\u2591".repeat(barWidth - filled));
6790
+ const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
6489
6791
  const icon = LAYER_ICONS[task.layer] ?? " ";
6490
6792
  if (mode === "skip") {
6491
6793
  console.log(
6492
- chalk6.gray(`
6794
+ chalk8.gray(`
6493
6795
  [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
6494
6796
  );
6495
6797
  } else {
6496
6798
  console.log(
6497
- chalk6.bold(`
6799
+ chalk8.bold(`
6498
6800
  [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
6499
6801
  );
6500
6802
  }
@@ -6502,27 +6804,27 @@ function printTaskProgress(completed, total, task, mode) {
6502
6804
 
6503
6805
  // core/reviewer.ts
6504
6806
  init_codegen_prompt();
6505
- import chalk7 from "chalk";
6807
+ import chalk9 from "chalk";
6506
6808
  import { execSync as execSync2 } from "child_process";
6507
- import * as path8 from "path";
6508
- import * as fs9 from "fs-extra";
6809
+ import * as path10 from "path";
6810
+ import * as fs11 from "fs-extra";
6509
6811
  var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
6510
6812
  async function loadReviewHistory(projectRoot) {
6511
- const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6813
+ const historyPath = path10.join(projectRoot, REVIEW_HISTORY_FILE);
6512
6814
  try {
6513
- if (await fs9.pathExists(historyPath)) {
6514
- return await fs9.readJson(historyPath);
6815
+ if (await fs11.pathExists(historyPath)) {
6816
+ return await fs11.readJson(historyPath);
6515
6817
  }
6516
6818
  } catch {
6517
6819
  }
6518
6820
  return [];
6519
6821
  }
6520
6822
  async function appendReviewHistory(projectRoot, entry) {
6521
- const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6823
+ const historyPath = path10.join(projectRoot, REVIEW_HISTORY_FILE);
6522
6824
  const existing = await loadReviewHistory(projectRoot);
6523
6825
  const updated = [...existing, entry].slice(-20);
6524
6826
  try {
6525
- await fs9.writeJson(historyPath, updated, { spaces: 2 });
6827
+ await fs11.writeJson(historyPath, updated, { spaces: 2 });
6526
6828
  } catch {
6527
6829
  }
6528
6830
  }
@@ -6530,6 +6832,14 @@ function extractScore(reviewText) {
6530
6832
  const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
6531
6833
  return match ? parseFloat(match[1]) : 0;
6532
6834
  }
6835
+ function extractImpactLevel(reviewText) {
6836
+ const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
6837
+ return match ? match[1] : void 0;
6838
+ }
6839
+ function extractComplexityLevel(reviewText) {
6840
+ const match = reviewText.match(/复杂度等级[::]\s*(低|中|高)/);
6841
+ return match ? match[1] : void 0;
6842
+ }
6533
6843
  function extractTopIssues(reviewText) {
6534
6844
  const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
6535
6845
  return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
@@ -6540,7 +6850,7 @@ function buildHistoryContext(history) {
6540
6850
  const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
6541
6851
  for (const entry of recent) {
6542
6852
  lines.push(`
6543
- [${entry.date}] ${path8.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6853
+ [${entry.date}] ${path10.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6544
6854
  entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
6545
6855
  }
6546
6856
  return lines.join("\n") + "\n";
@@ -6575,15 +6885,14 @@ var CodeReviewer = class {
6575
6885
  };
6576
6886
  }
6577
6887
  /**
6578
- * Two-pass review:
6888
+ * Three-pass review:
6579
6889
  * Pass 1 — architecture (spec compliance, layer separation, auth)
6580
6890
  * Pass 2 — implementation details (validation, error handling, edge cases)
6581
6891
  * + historical issue recurrence check
6582
- *
6583
- * Falls back to single-pass if the two-pass flag is not set.
6892
+ * Pass 3 — impact assessment + code complexity
6584
6893
  */
6585
- async runTwoPassReview(specContent, codeContext, specFile) {
6586
- console.log(chalk7.gray(" Pass 1/2: Architecture review..."));
6894
+ async runThreePassReview(specContent, codeContext, specFile) {
6895
+ console.log(chalk9.gray(" Pass 1/3: Architecture review..."));
6587
6896
  const archPrompt = `Review the architecture of this change.
6588
6897
 
6589
6898
  === Feature Spec ===
@@ -6592,7 +6901,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6592
6901
  === Code ===
6593
6902
  ${codeContext}`;
6594
6903
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6595
- console.log(chalk7.gray(" Pass 2/2: Implementation review..."));
6904
+ console.log(chalk9.gray(" Pass 2/3: Implementation review..."));
6596
6905
  const history = await loadReviewHistory(this.projectRoot);
6597
6906
  const historyContext = buildHistoryContext(history);
6598
6907
  const implPrompt = `Review the implementation details of this change.
@@ -6607,61 +6916,85 @@ ${codeContext}
6607
6916
  ${archReview}
6608
6917
  ${historyContext}`;
6609
6918
  const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
6610
- const combined = `${archReview}
6919
+ console.log(chalk9.gray(" Pass 3/3: Impact & complexity assessment..."));
6920
+ const impactPrompt = `Assess the impact and complexity of this change.
6611
6921
 
6612
- ${"\u2500".repeat(52)}
6922
+ === Feature Spec ===
6923
+ ${specContent || "(No spec \u2014 review for general code quality)"}
6924
+
6925
+ === Code ===
6926
+ ${codeContext}
6927
+
6928
+ === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
6929
+ ${archReview}
6613
6930
 
6931
+ === Implementation Review (Pass 2 \u2014 do NOT repeat) ===
6614
6932
  ${implReview}`;
6933
+ const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
6934
+ const sep = "\u2500".repeat(52);
6935
+ const combined = `${archReview}
6936
+
6937
+ ${sep}
6938
+
6939
+ ${implReview}
6940
+
6941
+ ${sep}
6942
+
6943
+ ${impactReview}`;
6615
6944
  const score = extractScore(implReview) || extractScore(archReview);
6616
6945
  const topIssues = extractTopIssues(implReview);
6946
+ const impactLevel = extractImpactLevel(impactReview);
6947
+ const complexityLevel = extractComplexityLevel(impactReview);
6617
6948
  if (score > 0 && specFile) {
6618
6949
  await appendReviewHistory(this.projectRoot, {
6619
6950
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
6620
- specFile: path8.relative(this.projectRoot, specFile),
6951
+ specFile: path10.relative(this.projectRoot, specFile),
6621
6952
  score,
6622
- topIssues
6953
+ topIssues,
6954
+ ...impactLevel ? { impactLevel } : {},
6955
+ ...complexityLevel ? { complexityLevel } : {}
6623
6956
  });
6624
6957
  }
6625
6958
  return combined;
6626
6959
  }
6627
6960
  async reviewCode(specContent, specFile) {
6628
- 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"));
6961
+ 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"));
6629
6962
  const diff = this.getGitDiff();
6630
6963
  if (!diff.trim()) {
6631
6964
  console.log(
6632
- chalk7.yellow(" No git diff found. Stage or commit changes first, then run review.")
6965
+ chalk9.yellow(" No git diff found. Stage or commit changes first, then run review.")
6633
6966
  );
6634
- console.log(chalk7.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6967
+ console.log(chalk9.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6635
6968
  return "No changes";
6636
6969
  }
6637
6970
  const { files, added, removed } = this.getDiffStats(diff);
6638
6971
  console.log(
6639
- chalk7.gray(` Diff: ${files} file(s), ${chalk7.green("+" + added)} ${chalk7.red("-" + removed)}`)
6972
+ chalk9.gray(` Diff: ${files} file(s), ${chalk9.green("+" + added)} ${chalk9.red("-" + removed)}`)
6640
6973
  );
6641
6974
  console.log(
6642
- chalk7.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6975
+ chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6643
6976
  );
6644
6977
  const codeContext = diff.slice(0, 1e4);
6645
- const reviewResult = await this.runTwoPassReview(specContent, codeContext, specFile);
6646
- 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"));
6978
+ const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
6979
+ 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"));
6647
6980
  console.log(reviewResult);
6648
- 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"));
6981
+ 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"));
6649
6982
  return reviewResult;
6650
6983
  }
6651
6984
  /**
6652
6985
  * Review directly from generated file contents (for api mode where git diff is empty).
6653
6986
  */
6654
6987
  async reviewFiles(specContent, filePaths, workingDir, specFile) {
6655
- console.log(chalk7.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6656
- console.log(chalk7.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6988
+ console.log(chalk9.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6989
+ console.log(chalk9.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6657
6990
  console.log(
6658
- chalk7.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6991
+ chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6659
6992
  );
6660
6993
  let filesSection = "";
6661
6994
  for (const filePath of filePaths) {
6662
- const fullPath = path8.join(workingDir, filePath);
6995
+ const fullPath = path10.join(workingDir, filePath);
6663
6996
  try {
6664
- const content = await fs9.readFile(fullPath, "utf-8");
6997
+ const content = await fs11.readFile(fullPath, "utf-8");
6665
6998
  filesSection += `
6666
6999
 
6667
7000
  === ${filePath} ===
@@ -6675,35 +7008,35 @@ ${content.slice(0, 3e3)}`;
6675
7008
  (file not found)`;
6676
7009
  }
6677
7010
  }
6678
- const reviewResult = await this.runTwoPassReview(specContent, filesSection, specFile);
6679
- 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"));
7011
+ const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
7012
+ 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"));
6680
7013
  console.log(reviewResult);
6681
- 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"));
7014
+ 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"));
6682
7015
  return reviewResult;
6683
7016
  }
6684
7017
  /** Print score trend from history (last N reviews) */
6685
7018
  async printScoreTrend(limit = 5) {
6686
7019
  const history = await loadReviewHistory(this.projectRoot);
6687
7020
  if (history.length === 0) {
6688
- console.log(chalk7.gray(" No review history yet."));
7021
+ console.log(chalk9.gray(" No review history yet."));
6689
7022
  return;
6690
7023
  }
6691
7024
  const recent = history.slice(-limit);
6692
- 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"));
7025
+ 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"));
6693
7026
  for (const entry of recent) {
6694
7027
  const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
6695
- const color = entry.score >= 8 ? chalk7.green : entry.score >= 6 ? chalk7.yellow : chalk7.red;
6696
- console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path8.basename(entry.specFile)}`);
7028
+ const color = entry.score >= 8 ? chalk9.green : entry.score >= 6 ? chalk9.yellow : chalk9.red;
7029
+ console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path10.basename(entry.specFile)}`);
6697
7030
  }
6698
- 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"));
7031
+ 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"));
6699
7032
  }
6700
7033
  };
6701
7034
 
6702
7035
  // git/worktree.ts
6703
7036
  import { execSync as execSync3 } from "child_process";
6704
- import * as path9 from "path";
6705
- import * as fs10 from "fs-extra";
6706
- import chalk8 from "chalk";
7037
+ import * as path11 from "path";
7038
+ import * as fs12 from "fs-extra";
7039
+ import chalk10 from "chalk";
6707
7040
  var GitWorktreeManager = class {
6708
7041
  constructor(baseDir) {
6709
7042
  this.baseDir = baseDir;
@@ -6728,32 +7061,32 @@ var GitWorktreeManager = class {
6728
7061
  async linkDependencies(worktreePath) {
6729
7062
  const candidates = ["node_modules", "vendor"];
6730
7063
  for (const dir of candidates) {
6731
- const src = path9.join(this.baseDir, dir);
6732
- const dest = path9.join(worktreePath, dir);
6733
- if (!await fs10.pathExists(src)) continue;
6734
- if (await fs10.pathExists(dest)) continue;
7064
+ const src = path11.join(this.baseDir, dir);
7065
+ const dest = path11.join(worktreePath, dir);
7066
+ if (!await fs12.pathExists(src)) continue;
7067
+ if (await fs12.pathExists(dest)) continue;
6735
7068
  try {
6736
- await fs10.ensureSymlink(src, dest, "dir");
6737
- console.log(chalk8.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
7069
+ await fs12.ensureSymlink(src, dest, "dir");
7070
+ console.log(chalk10.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6738
7071
  } catch (err) {
6739
- console.log(chalk8.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6740
- console.log(chalk8.yellow(` Run \`npm install\` inside the worktree manually.`));
7072
+ console.log(chalk10.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
7073
+ console.log(chalk10.yellow(` Run \`npm install\` inside the worktree manually.`));
6741
7074
  }
6742
7075
  }
6743
7076
  }
6744
7077
  async createWorktree(idea) {
6745
7078
  if (!this.isGitRepo()) {
6746
- console.log(chalk8.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
7079
+ console.log(chalk10.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6747
7080
  return null;
6748
7081
  }
6749
7082
  const featureName = this.sanitizeFeatureName(idea);
6750
7083
  const branchName = `feature/${featureName}`;
6751
- const repoName = path9.basename(this.baseDir);
6752
- const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
6753
- console.log(chalk8.cyan(`
7084
+ const repoName = path11.basename(this.baseDir);
7085
+ const worktreePath = path11.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
7086
+ console.log(chalk10.cyan(`
6754
7087
  --- Setting up Git Worktree ---`));
6755
- if (await fs10.pathExists(worktreePath)) {
6756
- console.log(chalk8.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
7088
+ if (await fs12.pathExists(worktreePath)) {
7089
+ console.log(chalk10.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6757
7090
  await this.linkDependencies(worktreePath);
6758
7091
  return worktreePath;
6759
7092
  }
@@ -6767,7 +7100,7 @@ var GitWorktreeManager = class {
6767
7100
  branchExists = true;
6768
7101
  } catch {
6769
7102
  }
6770
- console.log(chalk8.gray(`Creating worktree at: ${worktreePath}`));
7103
+ console.log(chalk10.gray(`Creating worktree at: ${worktreePath}`));
6771
7104
  if (branchExists) {
6772
7105
  execSync3(`git worktree add "${worktreePath}" ${branchName}`, {
6773
7106
  cwd: this.baseDir,
@@ -6780,21 +7113,21 @@ var GitWorktreeManager = class {
6780
7113
  });
6781
7114
  }
6782
7115
  console.log(
6783
- chalk8.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
7116
+ chalk10.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6784
7117
  );
6785
7118
  await this.linkDependencies(worktreePath);
6786
7119
  return worktreePath;
6787
7120
  } catch (error) {
6788
- console.error(chalk8.red("Failed to create git worktree:"), error);
7121
+ console.error(chalk10.red("Failed to create git worktree:"), error);
6789
7122
  return null;
6790
7123
  }
6791
7124
  }
6792
7125
  };
6793
7126
 
6794
7127
  // core/constitution-generator.ts
6795
- import chalk9 from "chalk";
6796
- import * as fs11 from "fs-extra";
6797
- import * as path10 from "path";
7128
+ import chalk11 from "chalk";
7129
+ import * as fs13 from "fs-extra";
7130
+ import * as path12 from "path";
6798
7131
 
6799
7132
  // prompts/constitution.prompt.ts
6800
7133
  var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
@@ -6874,8 +7207,8 @@ var ConstitutionGenerator = class {
6874
7207
  return this.provider.generate(prompt, constitutionSystemPrompt);
6875
7208
  }
6876
7209
  async saveConstitution(projectRoot, content) {
6877
- const filePath = path10.join(projectRoot, CONSTITUTION_FILE);
6878
- await fs11.writeFile(filePath, content, "utf-8");
7210
+ const filePath = path12.join(projectRoot, CONSTITUTION_FILE);
7211
+ await fs13.writeFile(filePath, content, "utf-8");
6879
7212
  return filePath;
6880
7213
  }
6881
7214
  };
@@ -6934,9 +7267,9 @@ ${sections.join("\n")}
6934
7267
  }
6935
7268
 
6936
7269
  // core/constitution-consolidator.ts
6937
- import chalk10 from "chalk";
6938
- import * as fs12 from "fs-extra";
6939
- import * as path11 from "path";
7270
+ import chalk12 from "chalk";
7271
+ import * as fs14 from "fs-extra";
7272
+ import * as path13 from "path";
6940
7273
 
6941
7274
  // prompts/consolidate.prompt.ts
6942
7275
  var consolidateSystemPrompt = `You are a Principal Engineer maintaining a living "Project Constitution" document.
@@ -6999,26 +7332,26 @@ var ConstitutionConsolidator = class {
6999
7332
  }
7000
7333
  async consolidate(projectRoot, opts = {}) {
7001
7334
  const minLessons = opts.minLessons ?? 5;
7002
- const constitutionPath = path11.join(projectRoot, CONSTITUTION_FILE);
7003
- if (!await fs12.pathExists(constitutionPath)) {
7335
+ const constitutionPath = path13.join(projectRoot, CONSTITUTION_FILE);
7336
+ if (!await fs14.pathExists(constitutionPath)) {
7004
7337
  throw new Error(`No constitution file found at ${constitutionPath}. Run \`ai-spec init\` first.`);
7005
7338
  }
7006
- const original = await fs12.readFile(constitutionPath, "utf-8");
7339
+ const original = await fs14.readFile(constitutionPath, "utf-8");
7007
7340
  const before = parseConstitutionStats(original);
7008
- console.log(chalk10.blue("\n\u2500\u2500\u2500 Constitution Consolidation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7009
- console.log(chalk10.gray(` File : ${CONSTITUTION_FILE}`));
7010
- console.log(chalk10.gray(` Size : ${before.totalLines} lines`));
7011
- console.log(chalk10.gray(` \xA79 items: ${before.lessonCount} accumulated lessons`));
7341
+ console.log(chalk12.blue("\n\u2500\u2500\u2500 Constitution Consolidation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7342
+ console.log(chalk12.gray(` File : ${CONSTITUTION_FILE}`));
7343
+ console.log(chalk12.gray(` Size : ${before.totalLines} lines`));
7344
+ console.log(chalk12.gray(` \xA79 items: ${before.lessonCount} accumulated lessons`));
7012
7345
  if (before.lessonCount < minLessons) {
7013
7346
  console.log(
7014
- chalk10.green(
7347
+ chalk12.green(
7015
7348
  `
7016
7349
  \u2714 \xA79 has only ${before.lessonCount} lesson(s) \u2014 no consolidation needed yet (threshold: ${minLessons}).`
7017
7350
  )
7018
7351
  );
7019
7352
  return { written: false, before, after: before, backupPath: null };
7020
7353
  }
7021
- console.log(chalk10.cyan(`
7354
+ console.log(chalk12.cyan(`
7022
7355
  Consolidating ${before.lessonCount} lesson(s) with AI...`));
7023
7356
  const prompt = buildConsolidatePrompt(original, before.lessonCount);
7024
7357
  let consolidated;
@@ -7030,28 +7363,28 @@ var ConstitutionConsolidator = class {
7030
7363
  }
7031
7364
  const after = parseConstitutionStats(consolidated);
7032
7365
  const diff = computeDiff(original, consolidated);
7033
- console.log(chalk10.blue("\n Changes preview:"));
7366
+ console.log(chalk12.blue("\n Changes preview:"));
7034
7367
  printDiff(diff, 4);
7035
7368
  printDiffSummary(diff);
7036
- console.log(chalk10.cyan("\n After consolidation:"));
7037
- console.log(chalk10.gray(` Size : ${after.totalLines} lines (was ${before.totalLines})`));
7038
- console.log(chalk10.gray(` \xA79 items: ${after.lessonCount} remaining (was ${before.lessonCount})`));
7369
+ console.log(chalk12.cyan("\n After consolidation:"));
7370
+ console.log(chalk12.gray(` Size : ${after.totalLines} lines (was ${before.totalLines})`));
7371
+ console.log(chalk12.gray(` \xA79 items: ${after.lessonCount} remaining (was ${before.lessonCount})`));
7039
7372
  const liftedCount = Math.max(0, before.lessonCount - after.lessonCount);
7040
7373
  if (liftedCount > 0) {
7041
- console.log(chalk10.green(` \u2714 ~${liftedCount} lesson(s) lifted into \xA71\u2013\xA78 or removed`));
7374
+ console.log(chalk12.green(` \u2714 ~${liftedCount} lesson(s) lifted into \xA71\u2013\xA78 or removed`));
7042
7375
  }
7043
7376
  if (opts.dryRun) {
7044
- console.log(chalk10.yellow("\n [dry-run] No changes written."));
7377
+ console.log(chalk12.yellow("\n [dry-run] No changes written."));
7045
7378
  return { written: false, before, after, backupPath: null };
7046
7379
  }
7047
7380
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[T:]/g, "-");
7048
7381
  const backupName = `.ai-spec-constitution.backup-${timestamp}.md`;
7049
- const backupPath = path11.join(projectRoot, backupName);
7050
- await fs12.writeFile(backupPath, original, "utf-8");
7051
- console.log(chalk10.gray(`
7382
+ const backupPath = path13.join(projectRoot, backupName);
7383
+ await fs14.writeFile(backupPath, original, "utf-8");
7384
+ console.log(chalk12.gray(`
7052
7385
  Backup : ${backupName}`));
7053
- await fs12.writeFile(constitutionPath, consolidated, "utf-8");
7054
- console.log(chalk10.green(` \u2714 Constitution updated: ${CONSTITUTION_FILE}`));
7386
+ await fs14.writeFile(constitutionPath, consolidated, "utf-8");
7387
+ console.log(chalk12.green(` \u2714 Constitution updated: ${CONSTITUTION_FILE}`));
7055
7388
  return { written: true, before, after, backupPath };
7056
7389
  }
7057
7390
  };
@@ -7092,9 +7425,9 @@ function parseSpecAndTasks(raw) {
7092
7425
  }
7093
7426
 
7094
7427
  // core/test-generator.ts
7095
- import chalk11 from "chalk";
7096
- import * as fs13 from "fs-extra";
7097
- import * as path12 from "path";
7428
+ import chalk13 from "chalk";
7429
+ import * as fs15 from "fs-extra";
7430
+ import * as path14 from "path";
7098
7431
 
7099
7432
  // prompts/testgen.prompt.ts
7100
7433
  var testGenSystemPrompt = `You are a Senior QA Engineer generating test skeletons for a Node.js/TypeScript project.
@@ -7287,10 +7620,10 @@ function parseTestFiles(raw) {
7287
7620
  return [];
7288
7621
  }
7289
7622
  async function isFrontendProject(workingDir) {
7290
- const pkgPath = path12.join(workingDir, "package.json");
7291
- if (!await fs13.pathExists(pkgPath)) return false;
7623
+ const pkgPath = path14.join(workingDir, "package.json");
7624
+ if (!await fs15.pathExists(pkgPath)) return false;
7292
7625
  try {
7293
- const pkg = await fs13.readJson(pkgPath);
7626
+ const pkg = await fs15.readJson(pkgPath);
7294
7627
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
7295
7628
  const keys = Object.keys(deps);
7296
7629
  return keys.some((k2) => FRONTEND_FRAMEWORKS.includes(k2));
@@ -7308,20 +7641,20 @@ var TestGenerator = class {
7308
7641
  * Returns the list of test file paths written.
7309
7642
  */
7310
7643
  async generate(dsl, workingDir) {
7311
- console.log(chalk11.blue("\n\u2500\u2500\u2500 Test Generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7644
+ console.log(chalk13.blue("\n\u2500\u2500\u2500 Test Generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7312
7645
  const testDir = await this.detectTestDir(workingDir);
7313
7646
  const frontend = await isFrontendProject(workingDir);
7314
7647
  let prompt;
7315
7648
  let systemPrompt;
7316
7649
  if (frontend) {
7317
7650
  const ctx = await loadFrontendContext(workingDir);
7318
- console.log(chalk11.gray(` Mode: frontend (${ctx.framework} / ${ctx.testFramework})`));
7319
- console.log(chalk11.gray(` Test directory: ${testDir}`));
7651
+ console.log(chalk13.gray(` Mode: frontend (${ctx.framework} / ${ctx.testFramework})`));
7652
+ console.log(chalk13.gray(` Test directory: ${testDir}`));
7320
7653
  prompt = buildFrontendTestGenPrompt(dsl, testDir, ctx);
7321
7654
  systemPrompt = testGenFrontendSystemPrompt;
7322
7655
  } else {
7323
- console.log(chalk11.gray(` Mode: backend`));
7324
- console.log(chalk11.gray(` Test directory: ${testDir}`));
7656
+ console.log(chalk13.gray(` Mode: backend`));
7657
+ console.log(chalk13.gray(` Test directory: ${testDir}`));
7325
7658
  prompt = buildBackendTestGenPrompt(dsl, testDir);
7326
7659
  systemPrompt = testGenSystemPrompt;
7327
7660
  }
@@ -7329,23 +7662,23 @@ var TestGenerator = class {
7329
7662
  try {
7330
7663
  rawOutput = await this.provider.generate(prompt, systemPrompt);
7331
7664
  } catch (err) {
7332
- console.log(chalk11.yellow(` \u26A0 Test generation AI call failed: ${err.message}`));
7665
+ console.log(chalk13.yellow(` \u26A0 Test generation AI call failed: ${err.message}`));
7333
7666
  return [];
7334
7667
  }
7335
7668
  const testFiles = parseTestFiles(rawOutput);
7336
7669
  if (testFiles.length === 0) {
7337
- console.log(chalk11.yellow(" \u26A0 Could not parse test files from AI output. Skipping."));
7670
+ console.log(chalk13.yellow(" \u26A0 Could not parse test files from AI output. Skipping."));
7338
7671
  return [];
7339
7672
  }
7340
7673
  const writtenFiles = [];
7341
7674
  for (const tf of testFiles) {
7342
- const fullPath = path12.join(workingDir, tf.file);
7343
- await fs13.ensureDir(path12.dirname(fullPath));
7344
- await fs13.writeFile(fullPath, tf.content, "utf-8");
7345
- console.log(chalk11.green(` + ${tf.file}`));
7675
+ const fullPath = path14.join(workingDir, tf.file);
7676
+ await fs15.ensureDir(path14.dirname(fullPath));
7677
+ await fs15.writeFile(fullPath, tf.content, "utf-8");
7678
+ console.log(chalk13.green(` + ${tf.file}`));
7346
7679
  writtenFiles.push(tf.file);
7347
7680
  }
7348
- console.log(chalk11.green(` \u2714 ${writtenFiles.length} test file(s) generated.`));
7681
+ console.log(chalk13.green(` \u2714 ${writtenFiles.length} test file(s) generated.`));
7349
7682
  return writtenFiles;
7350
7683
  }
7351
7684
  /**
@@ -7354,40 +7687,40 @@ var TestGenerator = class {
7354
7687
  * Only supports backend projects (uses supertest + DSL endpoints/models).
7355
7688
  */
7356
7689
  async generateTdd(dsl, workingDir) {
7357
- console.log(chalk11.blue("\n\u2500\u2500\u2500 TDD Test Generation (pre-implementation) \u2500\u2500\u2500\u2500"));
7690
+ console.log(chalk13.blue("\n\u2500\u2500\u2500 TDD Test Generation (pre-implementation) \u2500\u2500\u2500\u2500"));
7358
7691
  const testDir = await this.detectTestDir(workingDir);
7359
- console.log(chalk11.gray(` Mode: TDD (real assertions \u2014 tests will fail until implementation is complete)`));
7360
- console.log(chalk11.gray(` Test directory: ${testDir}`));
7692
+ console.log(chalk13.gray(` Mode: TDD (real assertions \u2014 tests will fail until implementation is complete)`));
7693
+ console.log(chalk13.gray(` Test directory: ${testDir}`));
7361
7694
  const prompt = buildBackendTestGenPrompt(dsl, testDir);
7362
7695
  let rawOutput;
7363
7696
  try {
7364
7697
  rawOutput = await this.provider.generate(prompt, tddTestGenSystemPrompt);
7365
7698
  } catch (err) {
7366
- console.log(chalk11.yellow(` \u26A0 TDD test generation failed: ${err.message}`));
7699
+ console.log(chalk13.yellow(` \u26A0 TDD test generation failed: ${err.message}`));
7367
7700
  return [];
7368
7701
  }
7369
7702
  const testFiles = parseTestFiles(rawOutput);
7370
7703
  if (testFiles.length === 0) {
7371
- console.log(chalk11.yellow(" \u26A0 Could not parse TDD test files from AI output. Skipping."));
7704
+ console.log(chalk13.yellow(" \u26A0 Could not parse TDD test files from AI output. Skipping."));
7372
7705
  return [];
7373
7706
  }
7374
7707
  const writtenFiles = [];
7375
7708
  for (const tf of testFiles) {
7376
- const fullPath = path12.join(workingDir, tf.file);
7377
- await fs13.ensureDir(path12.dirname(fullPath));
7378
- await fs13.writeFile(fullPath, tf.content, "utf-8");
7379
- console.log(chalk11.green(` + ${tf.file}`));
7709
+ const fullPath = path14.join(workingDir, tf.file);
7710
+ await fs15.ensureDir(path14.dirname(fullPath));
7711
+ await fs15.writeFile(fullPath, tf.content, "utf-8");
7712
+ console.log(chalk13.green(` + ${tf.file}`));
7380
7713
  writtenFiles.push(tf.file);
7381
7714
  }
7382
7715
  console.log(
7383
- chalk11.green(` \u2714 ${writtenFiles.length} TDD test file(s) written.`) + chalk11.gray(" (expected to fail \u2014 implementation will make them pass)")
7716
+ chalk13.green(` \u2714 ${writtenFiles.length} TDD test file(s) written.`) + chalk13.gray(" (expected to fail \u2014 implementation will make them pass)")
7384
7717
  );
7385
7718
  return writtenFiles;
7386
7719
  }
7387
7720
  async detectTestDir(workingDir) {
7388
7721
  const candidates = ["tests", "test", "__tests__", "src/__tests__", "spec"];
7389
7722
  for (const c of candidates) {
7390
- if (await fs13.pathExists(path12.join(workingDir, c))) return c;
7723
+ if (await fs15.pathExists(path14.join(workingDir, c))) return c;
7391
7724
  }
7392
7725
  return "tests";
7393
7726
  }
@@ -7395,10 +7728,10 @@ var TestGenerator = class {
7395
7728
 
7396
7729
  // core/error-feedback.ts
7397
7730
  init_codegen_prompt();
7398
- import chalk12 from "chalk";
7731
+ import chalk14 from "chalk";
7399
7732
  import { execSync as execSync4 } from "child_process";
7400
- import * as fs14 from "fs-extra";
7401
- import * as path13 from "path";
7733
+ import * as fs16 from "fs-extra";
7734
+ import * as path15 from "path";
7402
7735
  function runCommand(cmd, cwd) {
7403
7736
  try {
7404
7737
  const output = execSync4(cmd, { cwd, encoding: "utf-8", timeout: 6e4 });
@@ -7409,10 +7742,10 @@ function runCommand(cmd, cwd) {
7409
7742
  }
7410
7743
  }
7411
7744
  function detectBuildCommand(workingDir) {
7412
- if (!fs14.existsSync(path13.join(workingDir, "tsconfig.json"))) return null;
7413
- const pkgPath = path13.join(workingDir, "package.json");
7745
+ if (!fs16.existsSync(path15.join(workingDir, "tsconfig.json"))) return null;
7746
+ const pkgPath = path15.join(workingDir, "package.json");
7414
7747
  try {
7415
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
7748
+ const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf-8"));
7416
7749
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7417
7750
  if (allDeps["vue-tsc"]) return "npx vue-tsc --noEmit";
7418
7751
  if (pkg.scripts?.["type-check"]) return "npm run type-check";
@@ -7422,53 +7755,53 @@ function detectBuildCommand(workingDir) {
7422
7755
  return "npx tsc --noEmit";
7423
7756
  }
7424
7757
  function detectTestCommand(workingDir) {
7425
- if (fs14.existsSync(path13.join(workingDir, "go.mod"))) return "go test ./...";
7426
- if (fs14.existsSync(path13.join(workingDir, "composer.json"))) {
7427
- return fs14.existsSync(path13.join(workingDir, "vendor", "bin", "phpunit")) ? "./vendor/bin/phpunit --colors=never" : "php artisan test --no-ansi";
7758
+ if (fs16.existsSync(path15.join(workingDir, "go.mod"))) return "go test ./...";
7759
+ if (fs16.existsSync(path15.join(workingDir, "composer.json"))) {
7760
+ return fs16.existsSync(path15.join(workingDir, "vendor", "bin", "phpunit")) ? "./vendor/bin/phpunit --colors=never" : "php artisan test --no-ansi";
7428
7761
  }
7429
- if (fs14.existsSync(path13.join(workingDir, "Cargo.toml"))) return "cargo test";
7430
- if (fs14.existsSync(path13.join(workingDir, "pom.xml"))) return "mvn test -q";
7431
- if (fs14.existsSync(path13.join(workingDir, "build.gradle")) || fs14.existsSync(path13.join(workingDir, "build.gradle.kts"))) {
7762
+ if (fs16.existsSync(path15.join(workingDir, "Cargo.toml"))) return "cargo test";
7763
+ if (fs16.existsSync(path15.join(workingDir, "pom.xml"))) return "mvn test -q";
7764
+ if (fs16.existsSync(path15.join(workingDir, "build.gradle")) || fs16.existsSync(path15.join(workingDir, "build.gradle.kts"))) {
7432
7765
  return "./gradlew test";
7433
7766
  }
7434
- if (fs14.existsSync(path13.join(workingDir, "requirements.txt")) || fs14.existsSync(path13.join(workingDir, "pyproject.toml")) || fs14.existsSync(path13.join(workingDir, "setup.py"))) {
7767
+ if (fs16.existsSync(path15.join(workingDir, "requirements.txt")) || fs16.existsSync(path15.join(workingDir, "pyproject.toml")) || fs16.existsSync(path15.join(workingDir, "setup.py"))) {
7435
7768
  return "pytest";
7436
7769
  }
7437
- const pkgPath = path13.join(workingDir, "package.json");
7770
+ const pkgPath = path15.join(workingDir, "package.json");
7438
7771
  try {
7439
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
7772
+ const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf-8"));
7440
7773
  if (pkg.scripts?.test) return "npm test";
7441
7774
  if (pkg.scripts?.vitest) return "npx vitest run";
7442
7775
  } catch {
7443
7776
  }
7444
7777
  for (const f of ["vitest.config.ts", "vitest.config.js", "jest.config.ts", "jest.config.js"]) {
7445
- if (fs14.existsSync(path13.join(workingDir, f))) {
7778
+ if (fs16.existsSync(path15.join(workingDir, f))) {
7446
7779
  return f.startsWith("vitest") ? "npx vitest run" : "npx jest --forceExit";
7447
7780
  }
7448
7781
  }
7449
7782
  return null;
7450
7783
  }
7451
7784
  function detectLintCommand(workingDir) {
7452
- if (fs14.existsSync(path13.join(workingDir, "go.mod"))) {
7785
+ if (fs16.existsSync(path15.join(workingDir, "go.mod"))) {
7453
7786
  return "go vet ./...";
7454
7787
  }
7455
- if (fs14.existsSync(path13.join(workingDir, "composer.json"))) {
7456
- return fs14.existsSync(path13.join(workingDir, "vendor", "bin", "phpstan")) ? "./vendor/bin/phpstan analyse --no-progress --memory-limit=512M" : null;
7788
+ if (fs16.existsSync(path15.join(workingDir, "composer.json"))) {
7789
+ return fs16.existsSync(path15.join(workingDir, "vendor", "bin", "phpstan")) ? "./vendor/bin/phpstan analyse --no-progress --memory-limit=512M" : null;
7457
7790
  }
7458
- if (fs14.existsSync(path13.join(workingDir, "Cargo.toml"))) return "cargo clippy -- -D warnings";
7459
- if (fs14.existsSync(path13.join(workingDir, "pom.xml")) || fs14.existsSync(path13.join(workingDir, "build.gradle"))) {
7791
+ if (fs16.existsSync(path15.join(workingDir, "Cargo.toml"))) return "cargo clippy -- -D warnings";
7792
+ if (fs16.existsSync(path15.join(workingDir, "pom.xml")) || fs16.existsSync(path15.join(workingDir, "build.gradle"))) {
7460
7793
  return null;
7461
7794
  }
7462
- if (fs14.existsSync(path13.join(workingDir, "requirements.txt")) || fs14.existsSync(path13.join(workingDir, "pyproject.toml")) || fs14.existsSync(path13.join(workingDir, "setup.py"))) {
7795
+ if (fs16.existsSync(path15.join(workingDir, "requirements.txt")) || fs16.existsSync(path15.join(workingDir, "pyproject.toml")) || fs16.existsSync(path15.join(workingDir, "setup.py"))) {
7463
7796
  return "ruff check . || flake8 .";
7464
7797
  }
7465
- const pkgPath = path13.join(workingDir, "package.json");
7798
+ const pkgPath = path15.join(workingDir, "package.json");
7466
7799
  try {
7467
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
7800
+ const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf-8"));
7468
7801
  if (pkg.scripts?.lint) return "npm run lint";
7469
7802
  } catch {
7470
7803
  }
7471
- if (fs14.existsSync(path13.join(workingDir, ".eslintrc")) || fs14.existsSync(path13.join(workingDir, ".eslintrc.js")) || fs14.existsSync(path13.join(workingDir, ".eslintrc.json")) || fs14.existsSync(path13.join(workingDir, "eslint.config.js"))) {
7804
+ if (fs16.existsSync(path15.join(workingDir, ".eslintrc")) || fs16.existsSync(path15.join(workingDir, ".eslintrc.js")) || fs16.existsSync(path15.join(workingDir, ".eslintrc.json")) || fs16.existsSync(path15.join(workingDir, "eslint.config.js"))) {
7472
7805
  return "npx eslint . --max-warnings=0";
7473
7806
  }
7474
7807
  return null;
@@ -7504,10 +7837,10 @@ async function attemptFix(provider, errors, workingDir, dsl) {
7504
7837
  errorsByFile.get(file).push(err);
7505
7838
  }
7506
7839
  for (const [file, fileErrors] of errorsByFile) {
7507
- const fullPath = path13.join(workingDir, file);
7840
+ const fullPath = path15.join(workingDir, file);
7508
7841
  let existingContent = "";
7509
7842
  try {
7510
- existingContent = await fs14.readFile(fullPath, "utf-8");
7843
+ existingContent = await fs16.readFile(fullPath, "utf-8");
7511
7844
  } catch {
7512
7845
  results.push({ fixed: false, file, explanation: "File not found \u2014 cannot auto-fix." });
7513
7846
  continue;
@@ -7530,87 +7863,90 @@ Output ONLY the complete fixed file content. No markdown fences, no explanations
7530
7863
  try {
7531
7864
  const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
7532
7865
  const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
7533
- await fs14.writeFile(fullPath, fixed, "utf-8");
7866
+ await getActiveSnapshot()?.snapshotFile(fullPath);
7867
+ await fs16.writeFile(fullPath, fixed, "utf-8");
7534
7868
  results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
7535
- console.log(chalk12.green(` \u2714 Auto-fixed: ${file}`));
7869
+ console.log(chalk14.green(` \u2714 Auto-fixed: ${file}`));
7536
7870
  } catch (err) {
7537
7871
  results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
7538
- console.log(chalk12.yellow(` \u26A0 Could not auto-fix: ${file}`));
7872
+ console.log(chalk14.yellow(` \u26A0 Could not auto-fix: ${file}`));
7539
7873
  }
7540
7874
  }
7541
7875
  return results;
7542
7876
  }
7543
7877
  async function runErrorFeedback(provider, workingDir, dsl, opts = {}) {
7544
7878
  const maxCycles = opts.maxCycles ?? 2;
7545
- console.log(chalk12.blue("\n\u2500\u2500\u2500 Error Feedback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7879
+ console.log(chalk14.blue("\n\u2500\u2500\u2500 Error Feedback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7546
7880
  const testCmd = opts.skipTests ? null : detectTestCommand(workingDir);
7547
7881
  const lintCmd = opts.skipLint ? null : detectLintCommand(workingDir);
7548
7882
  const buildCmd = opts.skipBuild ? null : detectBuildCommand(workingDir);
7549
7883
  if (!testCmd && !lintCmd && !buildCmd) {
7550
- console.log(chalk12.gray(" No test / lint / type-check commands detected. Skipping error feedback."));
7884
+ console.log(chalk14.gray(" No test / lint / type-check commands detected. Skipping error feedback."));
7551
7885
  return true;
7552
7886
  }
7553
- if (buildCmd) console.log(chalk12.gray(` Type-check: ${buildCmd}`));
7887
+ if (buildCmd) console.log(chalk14.gray(` Type-check: ${buildCmd}`));
7554
7888
  for (let cycle = 1; cycle <= maxCycles; cycle++) {
7555
7889
  const allErrors = [];
7556
7890
  if (buildCmd) {
7557
- console.log(chalk12.gray(`
7891
+ console.log(chalk14.gray(`
7558
7892
  [cycle ${cycle}/${maxCycles}] Type-check: ${buildCmd}`));
7559
7893
  const buildResult = runCommand(buildCmd, workingDir);
7560
7894
  if (!buildResult.success) {
7561
- const isToolCrash = /ReferenceError:|TypeError:|SyntaxError:/.test(buildResult.output) && buildResult.output.includes("node_modules");
7895
+ const hasUncaughtError = /ReferenceError:|TypeError:|SyntaxError:/.test(buildResult.output);
7896
+ const hasToolStackFrame = buildResult.output.split("\n").some((l) => l.trim().startsWith("at ") && l.includes("node_modules"));
7897
+ const isToolCrash = hasUncaughtError && hasToolStackFrame;
7562
7898
  if (isToolCrash) {
7563
- console.log(chalk12.yellow(` \u26A0 Type-check tool crashed (possible version incompatibility). Skipping.`));
7564
- console.log(chalk12.gray(` Tip: run \`${buildCmd}\` manually to investigate.`));
7899
+ console.log(chalk14.yellow(` \u26A0 Type-check tool crashed (possible version incompatibility). Skipping.`));
7900
+ console.log(chalk14.gray(` Tip: run \`${buildCmd}\` manually to investigate.`));
7565
7901
  } else {
7566
7902
  const buildErrors = parseErrors(buildResult.output, "build");
7567
7903
  allErrors.push(...buildErrors);
7568
- console.log(chalk12.yellow(` \u2718 Type errors (${buildErrors.length} captured)`));
7904
+ console.log(chalk14.yellow(` \u2718 Type errors (${buildErrors.length} captured)`));
7569
7905
  }
7570
7906
  } else {
7571
- console.log(chalk12.green(" \u2714 Type-check passed."));
7907
+ console.log(chalk14.green(" \u2714 Type-check passed."));
7572
7908
  }
7573
7909
  }
7574
7910
  if (testCmd) {
7575
- console.log(chalk12.gray(`
7911
+ console.log(chalk14.gray(`
7576
7912
  [cycle ${cycle}/${maxCycles}] Running tests: ${testCmd}`));
7577
7913
  const testResult = runCommand(testCmd, workingDir);
7578
7914
  if (!testResult.success) {
7579
7915
  const testErrors = parseErrors(testResult.output, "test");
7580
7916
  allErrors.push(...testErrors);
7581
- console.log(chalk12.yellow(` \u2718 Tests failed (${testErrors.length} error(s) captured)`));
7917
+ console.log(chalk14.yellow(` \u2718 Tests failed (${testErrors.length} error(s) captured)`));
7582
7918
  } else {
7583
- console.log(chalk12.green(" \u2714 Tests passed."));
7919
+ console.log(chalk14.green(" \u2714 Tests passed."));
7584
7920
  }
7585
7921
  }
7586
7922
  if (lintCmd) {
7587
- console.log(chalk12.gray(` [cycle ${cycle}/${maxCycles}] Running lint: ${lintCmd}`));
7923
+ console.log(chalk14.gray(` [cycle ${cycle}/${maxCycles}] Running lint: ${lintCmd}`));
7588
7924
  const lintResult = runCommand(lintCmd, workingDir);
7589
7925
  if (!lintResult.success) {
7590
7926
  const lintErrors = parseErrors(lintResult.output, "lint");
7591
7927
  allErrors.push(...lintErrors);
7592
- console.log(chalk12.yellow(` \u2718 Lint failed (${lintErrors.length} error(s) captured)`));
7928
+ console.log(chalk14.yellow(` \u2718 Lint failed (${lintErrors.length} error(s) captured)`));
7593
7929
  } else {
7594
- console.log(chalk12.green(" \u2714 Lint passed."));
7930
+ console.log(chalk14.green(" \u2714 Lint passed."));
7595
7931
  }
7596
7932
  }
7597
7933
  if (allErrors.length === 0) {
7598
- console.log(chalk12.green(`
7934
+ console.log(chalk14.green(`
7599
7935
  \u2714 All checks passed after ${cycle} cycle(s).`));
7600
7936
  return true;
7601
7937
  }
7602
7938
  if (cycle < maxCycles) {
7603
- console.log(chalk12.cyan(`
7939
+ console.log(chalk14.cyan(`
7604
7940
  Attempting auto-fix (${allErrors.length} error(s))...`));
7605
7941
  await attemptFix(provider, allErrors, workingDir, dsl);
7606
7942
  }
7607
7943
  }
7608
- console.log(chalk12.yellow("\n \u26A0 Some errors remain after auto-fix cycles. Manual intervention needed."));
7944
+ console.log(chalk14.yellow("\n \u26A0 Some errors remain after auto-fix cycles. Manual intervention needed."));
7609
7945
  return false;
7610
7946
  }
7611
7947
 
7612
7948
  // core/spec-assessor.ts
7613
- import chalk13 from "chalk";
7949
+ import chalk15 from "chalk";
7614
7950
 
7615
7951
  // prompts/spec-assess.prompt.ts
7616
7952
  var specAssessSystemPrompt = `You are a Senior Software Architect reviewing a feature specification before it goes to code generation.
@@ -7665,8 +8001,8 @@ Output ONLY the JSON object \u2014 no explanation, no markdown fences.`;
7665
8001
  // core/spec-assessor.ts
7666
8002
  function scoreBar(score) {
7667
8003
  const filled = Math.round(score);
7668
- const bar = chalk13.green("\u2588".repeat(filled)) + chalk13.gray("\u2591".repeat(10 - filled));
7669
- const color = score >= 8 ? chalk13.green : score >= 6 ? chalk13.yellow : chalk13.red;
8004
+ const bar = chalk15.green("\u2588".repeat(filled)) + chalk15.gray("\u2591".repeat(10 - filled));
8005
+ const color = score >= 8 ? chalk15.green : score >= 6 ? chalk15.yellow : chalk15.red;
7670
8006
  return `[${bar}] ${color(score + "/10")}`;
7671
8007
  }
7672
8008
  function parseAssessment(raw) {
@@ -7696,35 +8032,35 @@ ${spec}`;
7696
8032
  }
7697
8033
  }
7698
8034
  function printSpecAssessment(assessment) {
7699
- console.log(chalk13.blue("\n\u2500\u2500\u2500 Spec Quality Assessment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
8035
+ console.log(chalk15.blue("\n\u2500\u2500\u2500 Spec Quality Assessment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7700
8036
  console.log(` Coverage ${scoreBar(assessment.coverageScore)} error handling, edge cases, auth`);
7701
8037
  console.log(` Clarity ${scoreBar(assessment.clarityScore)} API contracts, response shapes`);
7702
8038
  console.log(` Constitution${scoreBar(assessment.constitutionScore)} naming, error codes, conventions`);
7703
- console.log(chalk13.bold(` Overall ${scoreBar(assessment.overallScore)}`));
8039
+ console.log(chalk15.bold(` Overall ${scoreBar(assessment.overallScore)}`));
7704
8040
  if (!assessment.dslExtractable) {
7705
8041
  console.log(
7706
- chalk13.yellow(
8042
+ chalk15.yellow(
7707
8043
  "\n \u26A0 DSL extraction may be unreliable \u2014 clarityScore < 6 or no structured API section."
7708
8044
  )
7709
8045
  );
7710
- console.log(chalk13.gray(" Consider adding explicit request/response shapes before proceeding."));
8046
+ console.log(chalk15.gray(" Consider adding explicit request/response shapes before proceeding."));
7711
8047
  }
7712
8048
  if (assessment.issues.length > 0) {
7713
- console.log(chalk13.yellow(`
8049
+ console.log(chalk15.yellow(`
7714
8050
  Issues found (${assessment.issues.length}):`));
7715
- assessment.issues.forEach((issue) => console.log(chalk13.yellow(` \xB7 ${issue}`)));
8051
+ assessment.issues.forEach((issue) => console.log(chalk15.yellow(` \xB7 ${issue}`)));
7716
8052
  }
7717
8053
  if (assessment.suggestions.length > 0) {
7718
- console.log(chalk13.cyan("\n Suggestions:"));
7719
- assessment.suggestions.forEach((s) => console.log(chalk13.cyan(` \u{1F4A1} ${s}`)));
8054
+ console.log(chalk15.cyan("\n Suggestions:"));
8055
+ assessment.suggestions.forEach((s) => console.log(chalk15.cyan(` \u{1F4A1} ${s}`)));
7720
8056
  }
7721
- console.log(chalk13.blue("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
8057
+ console.log(chalk15.blue("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7722
8058
  }
7723
8059
 
7724
8060
  // core/knowledge-memory.ts
7725
- import chalk14 from "chalk";
7726
- import * as fs15 from "fs-extra";
7727
- import * as path14 from "path";
8061
+ import chalk16 from "chalk";
8062
+ import * as fs17 from "fs-extra";
8063
+ import * as path16 from "path";
7728
8064
  function extractIssuesFromReview(reviewText) {
7729
8065
  const issues = [];
7730
8066
  const issuesMatch = reviewText.match(
@@ -7761,12 +8097,12 @@ var MEMORY_SECTION_HEADER = "\n\n## 9. \u79EF\u7D2F\u6559\u8BAD (Accumulated Les
7761
8097
  var MEMORY_SECTION_MARKER = "## 9. \u79EF\u7D2F\u6559\u8BAD";
7762
8098
  async function appendLessonsToConstitution(projectRoot, issues) {
7763
8099
  if (issues.length === 0) return;
7764
- const constitutionPath = path14.join(projectRoot, CONSTITUTION_FILE);
8100
+ const constitutionPath = path16.join(projectRoot, CONSTITUTION_FILE);
7765
8101
  let content = "";
7766
8102
  try {
7767
- content = await fs15.readFile(constitutionPath, "utf-8");
8103
+ content = await fs17.readFile(constitutionPath, "utf-8");
7768
8104
  } catch {
7769
- console.log(chalk14.gray(" No constitution file \u2014 skipping knowledge memory."));
8105
+ console.log(chalk16.gray(" No constitution file \u2014 skipping knowledge memory."));
7770
8106
  return;
7771
8107
  }
7772
8108
  const hasMemorySection = content.includes(MEMORY_SECTION_MARKER);
@@ -7779,7 +8115,7 @@ async function appendLessonsToConstitution(projectRoot, issues) {
7779
8115
  newEntries.push(`- ${badge} **[${date}]** ${issue.description}`);
7780
8116
  }
7781
8117
  if (newEntries.length === 0) {
7782
- console.log(chalk14.gray(" No new lessons to add (all deduplicated)."));
8118
+ console.log(chalk16.gray(" No new lessons to add (all deduplicated)."));
7783
8119
  return;
7784
8120
  }
7785
8121
  let updatedContent;
@@ -7792,22 +8128,22 @@ async function appendLessonsToConstitution(projectRoot, issues) {
7792
8128
  } else {
7793
8129
  updatedContent = content + MEMORY_SECTION_HEADER + newEntries.join("\n") + "\n";
7794
8130
  }
7795
- await fs15.writeFile(constitutionPath, updatedContent, "utf-8");
7796
- console.log(chalk14.green(` \u2714 ${newEntries.length} lesson(s) appended to constitution (\xA79).`));
8131
+ await fs17.writeFile(constitutionPath, updatedContent, "utf-8");
8132
+ console.log(chalk16.green(` \u2714 ${newEntries.length} lesson(s) appended to constitution (\xA79).`));
7797
8133
  const stats = parseConstitutionStats(updatedContent);
7798
8134
  if (stats.lessonCount >= 8) {
7799
8135
  console.log(
7800
- chalk14.yellow(
8136
+ chalk16.yellow(
7801
8137
  ` \u26A0 \xA79 now has ${stats.lessonCount} accumulated lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
7802
8138
  )
7803
8139
  );
7804
8140
  }
7805
8141
  }
7806
8142
  async function appendDirectLesson(projectRoot, lessonText) {
7807
- const constitutionPath = path14.join(projectRoot, CONSTITUTION_FILE);
8143
+ const constitutionPath = path16.join(projectRoot, CONSTITUTION_FILE);
7808
8144
  let content = "";
7809
8145
  try {
7810
- content = await fs15.readFile(constitutionPath, "utf-8");
8146
+ content = await fs17.readFile(constitutionPath, "utf-8");
7811
8147
  } catch {
7812
8148
  return { appended: false, reason: "No constitution file found. Run `ai-spec init` first." };
7813
8149
  }
@@ -7828,11 +8164,11 @@ async function appendDirectLesson(projectRoot, lessonText) {
7828
8164
  } else {
7829
8165
  updatedContent = content + MEMORY_SECTION_HEADER + entry + "\n";
7830
8166
  }
7831
- await fs15.writeFile(constitutionPath, updatedContent, "utf-8");
8167
+ await fs17.writeFile(constitutionPath, updatedContent, "utf-8");
7832
8168
  const stats = parseConstitutionStats(updatedContent);
7833
8169
  if (stats.lessonCount >= 8) {
7834
8170
  console.log(
7835
- chalk14.yellow(
8171
+ chalk16.yellow(
7836
8172
  ` \u26A0 \xA79 now has ${stats.lessonCount} lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
7837
8173
  )
7838
8174
  );
@@ -7840,15 +8176,15 @@ async function appendDirectLesson(projectRoot, lessonText) {
7840
8176
  return { appended: true };
7841
8177
  }
7842
8178
  async function accumulateReviewKnowledge(provider, projectRoot, reviewText) {
7843
- console.log(chalk14.blue("\n\u2500\u2500\u2500 Knowledge Memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
8179
+ console.log(chalk16.blue("\n\u2500\u2500\u2500 Knowledge Memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7844
8180
  const issues = extractIssuesFromReview(reviewText);
7845
8181
  if (issues.length === 0) {
7846
- console.log(chalk14.gray(" No actionable issues found in review. Skipping."));
8182
+ console.log(chalk16.gray(" No actionable issues found in review. Skipping."));
7847
8183
  return;
7848
8184
  }
7849
- console.log(chalk14.gray(` Extracted ${issues.length} issue(s) from review:`));
8185
+ console.log(chalk16.gray(` Extracted ${issues.length} issue(s) from review:`));
7850
8186
  for (const issue of issues) {
7851
- console.log(chalk14.gray(` - [${issue.category}] ${issue.description.slice(0, 80)}`));
8187
+ console.log(chalk16.gray(` - [${issue.category}] ${issue.description.slice(0, 80)}`));
7852
8188
  }
7853
8189
  await appendLessonsToConstitution(projectRoot, issues);
7854
8190
  }
@@ -7857,22 +8193,22 @@ async function accumulateReviewKnowledge(provider, projectRoot, reviewText) {
7857
8193
  init_workspace_loader();
7858
8194
 
7859
8195
  // core/key-store.ts
7860
- import * as fs17 from "fs-extra";
7861
- import * as path16 from "path";
8196
+ import * as fs19 from "fs-extra";
8197
+ import * as path18 from "path";
7862
8198
  import * as os3 from "os";
7863
- var KEY_STORE_FILE = path16.join(os3.homedir(), ".ai-spec-keys.json");
8199
+ var KEY_STORE_FILE = path18.join(os3.homedir(), ".ai-spec-keys.json");
7864
8200
  async function readStore() {
7865
8201
  try {
7866
- if (await fs17.pathExists(KEY_STORE_FILE)) {
7867
- return await fs17.readJson(KEY_STORE_FILE);
8202
+ if (await fs19.pathExists(KEY_STORE_FILE)) {
8203
+ return await fs19.readJson(KEY_STORE_FILE);
7868
8204
  }
7869
8205
  } catch {
7870
8206
  }
7871
8207
  return {};
7872
8208
  }
7873
8209
  async function writeStore(store) {
7874
- await fs17.writeJson(KEY_STORE_FILE, store, { spaces: 2 });
7875
- await fs17.chmod(KEY_STORE_FILE, 384);
8210
+ await fs19.writeJson(KEY_STORE_FILE, store, { spaces: 2 });
8211
+ await fs19.chmod(KEY_STORE_FILE, 384);
7876
8212
  }
7877
8213
  async function getSavedKey(provider) {
7878
8214
  const store = await readStore();
@@ -7884,8 +8220,8 @@ async function saveKey(provider, key) {
7884
8220
  await writeStore(store);
7885
8221
  }
7886
8222
  async function clearAllKeys() {
7887
- if (await fs17.pathExists(KEY_STORE_FILE)) {
7888
- await fs17.remove(KEY_STORE_FILE);
8223
+ if (await fs19.pathExists(KEY_STORE_FILE)) {
8224
+ await fs19.remove(KEY_STORE_FILE);
7889
8225
  }
7890
8226
  }
7891
8227
  async function clearKey(provider) {
@@ -7895,19 +8231,19 @@ async function clearKey(provider) {
7895
8231
  }
7896
8232
 
7897
8233
  // cli/welcome.ts
7898
- import chalk15 from "chalk";
8234
+ import chalk17 from "chalk";
7899
8235
  import * as os4 from "os";
7900
- import * as path17 from "path";
7901
- import * as fs18 from "fs-extra";
8236
+ import * as path19 from "path";
8237
+ import * as fs20 from "fs-extra";
7902
8238
  var VERSION = "0.14.1";
7903
8239
  var TOTAL_W = 76;
7904
8240
  var L_WIDTH = 44;
7905
8241
  var R_WIDTH = TOTAL_W - L_WIDTH - 4;
7906
- var ROBOT_COLOR = chalk15.hex("#E8885A");
8242
+ var ROBOT_COLOR = chalk17.hex("#E8885A");
7907
8243
  var ROBOT = [
7908
8244
  ROBOT_COLOR(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),
7909
- ROBOT_COLOR(" \u2502") + chalk15.bold.white(" \u25C9 ") + ROBOT_COLOR(" ") + chalk15.bold.white("\u25C9 ") + ROBOT_COLOR("\u2502"),
7910
- ROBOT_COLOR(" \u2502") + chalk15.dim(" \u2570\u2500\u256F ") + ROBOT_COLOR("\u2502"),
8245
+ ROBOT_COLOR(" \u2502") + chalk17.bold.white(" \u25C9 ") + ROBOT_COLOR(" ") + chalk17.bold.white("\u25C9 ") + ROBOT_COLOR("\u2502"),
8246
+ ROBOT_COLOR(" \u2502") + chalk17.dim(" \u2570\u2500\u256F ") + ROBOT_COLOR("\u2502"),
7911
8247
  ROBOT_COLOR(" \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518"),
7912
8248
  ROBOT_COLOR(" \u2502"),
7913
8249
  ROBOT_COLOR(" \u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500")
@@ -7920,7 +8256,7 @@ function padR(s, width) {
7920
8256
  return vl >= width ? s : s + " ".repeat(width - vl);
7921
8257
  }
7922
8258
  function row(left, right) {
7923
- return padR(left, L_WIDTH) + " " + chalk15.gray("\u2502") + " " + padR(right, R_WIDTH);
8259
+ return padR(left, L_WIDTH) + " " + chalk17.gray("\u2502") + " " + padR(right, R_WIDTH);
7924
8260
  }
7925
8261
  function center(s, width) {
7926
8262
  const vl = visLen(s);
@@ -7928,14 +8264,14 @@ function center(s, width) {
7928
8264
  return " ".repeat(pad) + s;
7929
8265
  }
7930
8266
  async function getRecentSpecs(dir) {
7931
- const specsDir = path17.join(dir, "specs");
7932
- if (!await fs18.pathExists(specsDir)) return [];
8267
+ const specsDir = path19.join(dir, "specs");
8268
+ if (!await fs20.pathExists(specsDir)) return [];
7933
8269
  try {
7934
- const files = await fs18.readdir(specsDir);
8270
+ const files = await fs20.readdir(specsDir);
7935
8271
  const mdFiles = files.filter((f) => f.endsWith(".md"));
7936
8272
  const withStats = await Promise.all(
7937
8273
  mdFiles.map(async (f) => {
7938
- const stat4 = await fs18.stat(path17.join(specsDir, f));
8274
+ const stat4 = await fs20.stat(path19.join(specsDir, f));
7939
8275
  return { name: f, mtime: stat4.mtime.getTime() };
7940
8276
  })
7941
8277
  );
@@ -7946,7 +8282,7 @@ async function getRecentSpecs(dir) {
7946
8282
  const days = Math.floor(hours / 24);
7947
8283
  const age = days > 0 ? `${days}d ago` : hours > 0 ? `${hours}h ago` : "just now";
7948
8284
  const slug = name.replace(/\.md$/, "").slice(0, R_WIDTH - age.length - 2);
7949
- return chalk15.white(slug) + chalk15.dim(" " + age);
8285
+ return chalk17.white(slug) + chalk17.dim(" " + age);
7950
8286
  });
7951
8287
  } catch {
7952
8288
  return [];
@@ -7961,13 +8297,13 @@ async function printWelcome(currentDir, config2) {
7961
8297
  const bottomRaw = [providerBit, shortDir].filter(Boolean).join(" \xB7 ");
7962
8298
  const maxInfoLen = L_WIDTH - 2;
7963
8299
  const bottomTruncated = bottomRaw.length > maxInfoLen ? bottomRaw.slice(0, maxInfoLen - 1) + "\u2026" : bottomRaw;
7964
- const bottomLine = " " + chalk15.dim(bottomTruncated);
8300
+ const bottomLine = " " + chalk17.dim(bottomTruncated);
7965
8301
  const titleInner = `ai-spec v${VERSION} `;
7966
8302
  const titleDashes = "\u2500".repeat(Math.max(0, TOTAL_W - titleInner.length - 4));
7967
8303
  console.log(
7968
- "\n" + chalk15.hex("#FF6B35")("\u2500\u2500\u2500 " + titleInner + titleDashes)
8304
+ "\n" + chalk17.hex("#FF6B35")("\u2500\u2500\u2500 " + titleInner + titleDashes)
7969
8305
  );
7970
- const welcomeText = "Welcome back, " + chalk15.bold.white(username) + "!";
8306
+ const welcomeText = "Welcome back, " + chalk17.bold.white(username) + "!";
7971
8307
  const leftLines = [
7972
8308
  "",
7973
8309
  center(welcomeText, L_WIDTH),
@@ -7978,21 +8314,21 @@ async function printWelcome(currentDir, config2) {
7978
8314
  ""
7979
8315
  ];
7980
8316
  const rightLines = [
7981
- chalk15.hex("#FF8C00").bold("Tips for getting started"),
7982
- chalk15.gray("\u2500".repeat(R_WIDTH)),
7983
- chalk15.white('ai-spec create "feature"'),
7984
- chalk15.gray("ai-spec workspace run"),
7985
- chalk15.gray('ai-spec update "change"'),
8317
+ chalk17.hex("#FF8C00").bold("Tips for getting started"),
8318
+ chalk17.gray("\u2500".repeat(R_WIDTH)),
8319
+ chalk17.white('ai-spec create "feature"'),
8320
+ chalk17.gray("ai-spec workspace run"),
8321
+ chalk17.gray('ai-spec update "change"'),
7986
8322
  "",
7987
- chalk15.hex("#FF8C00").bold("Recent activity"),
7988
- chalk15.gray("\u2500".repeat(R_WIDTH)),
7989
- ...recentSpecs.length > 0 ? recentSpecs : [chalk15.dim("No recent activity")]
8323
+ chalk17.hex("#FF8C00").bold("Recent activity"),
8324
+ chalk17.gray("\u2500".repeat(R_WIDTH)),
8325
+ ...recentSpecs.length > 0 ? recentSpecs : [chalk17.dim("No recent activity")]
7990
8326
  ];
7991
8327
  const maxLines = Math.max(leftLines.length, rightLines.length);
7992
8328
  for (let i = 0; i < maxLines; i++) {
7993
8329
  console.log(row(leftLines[i] ?? "", rightLines[i] ?? ""));
7994
8330
  }
7995
- console.log(chalk15.gray("\u2500".repeat(TOTAL_W)));
8331
+ console.log(chalk17.gray("\u2500".repeat(TOTAL_W)));
7996
8332
  console.log();
7997
8333
  }
7998
8334
 
@@ -8505,8 +8841,8 @@ Existing API/service files:`);
8505
8841
  }
8506
8842
 
8507
8843
  // core/mock-server-generator.ts
8508
- import * as path18 from "path";
8509
- import * as fs19 from "fs-extra";
8844
+ import * as path20 from "path";
8845
+ import * as fs21 from "fs-extra";
8510
8846
  import { spawn } from "child_process";
8511
8847
  function typeToFixture(fieldName, typeDesc) {
8512
8848
  const t = typeDesc.toLowerCase();
@@ -8642,22 +8978,22 @@ function generateMockServerJs(dsl, port) {
8642
8978
  }
8643
8979
  function detectFrontendFramework(projectDir) {
8644
8980
  for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
8645
- if (fs19.existsSync(path18.join(projectDir, f))) return "vite";
8981
+ if (fs21.existsSync(path20.join(projectDir, f))) return "vite";
8646
8982
  }
8647
8983
  for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
8648
- if (fs19.existsSync(path18.join(projectDir, f))) return "next";
8984
+ if (fs21.existsSync(path20.join(projectDir, f))) return "next";
8649
8985
  }
8650
- const pkgPath = path18.join(projectDir, "package.json");
8651
- if (fs19.existsSync(pkgPath)) {
8986
+ const pkgPath = path20.join(projectDir, "package.json");
8987
+ if (fs21.existsSync(pkgPath)) {
8652
8988
  try {
8653
- const pkg = JSON.parse(fs19.readFileSync(pkgPath, "utf-8"));
8989
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
8654
8990
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
8655
8991
  if (deps["react-scripts"]) return "cra";
8656
8992
  } catch {
8657
8993
  }
8658
8994
  }
8659
8995
  for (const f of ["webpack.config.js", "webpack.config.ts"]) {
8660
- if (fs19.existsSync(path18.join(projectDir, f))) return "webpack";
8996
+ if (fs21.existsSync(path20.join(projectDir, f))) return "webpack";
8661
8997
  }
8662
8998
  return "unknown";
8663
8999
  }
@@ -8837,7 +9173,7 @@ export const worker = setupWorker(...handlers);
8837
9173
  var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
8838
9174
  function findViteConfigFile(projectDir) {
8839
9175
  for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
8840
- if (fs19.existsSync(path18.join(projectDir, f))) return f;
9176
+ if (fs21.existsSync(path20.join(projectDir, f))) return f;
8841
9177
  }
8842
9178
  return null;
8843
9179
  }
@@ -8883,73 +9219,73 @@ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
8883
9219
  if (framework === "vite") {
8884
9220
  const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
8885
9221
  const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
8886
- const mockConfigPath = path18.join(frontendDir, "vite.config.ai-spec-mock.ts");
8887
- await fs19.writeFile(mockConfigPath, mockConfigContent, "utf-8");
9222
+ const mockConfigPath = path20.join(frontendDir, "vite.config.ai-spec-mock.ts");
9223
+ await fs21.writeFile(mockConfigPath, mockConfigContent, "utf-8");
8888
9224
  actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
8889
- const pkgPath = path18.join(frontendDir, "package.json");
8890
- if (await fs19.pathExists(pkgPath)) {
8891
- const pkg = await fs19.readJson(pkgPath);
9225
+ const pkgPath = path20.join(frontendDir, "package.json");
9226
+ if (await fs21.pathExists(pkgPath)) {
9227
+ const pkg = await fs21.readJson(pkgPath);
8892
9228
  pkg.scripts = pkg.scripts ?? {};
8893
9229
  const originalValue = pkg.scripts["dev:mock"] ?? null;
8894
9230
  pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
8895
- await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
9231
+ await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
8896
9232
  actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
8897
9233
  }
8898
9234
  const lock2 = { framework, mockPort, frontendDir, actions };
8899
- await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
9235
+ await fs21.writeJson(path20.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
8900
9236
  return { framework, applied: true, devCommand: "npm run dev:mock" };
8901
9237
  }
8902
9238
  if (framework === "cra") {
8903
- const pkgPath = path18.join(frontendDir, "package.json");
8904
- if (await fs19.pathExists(pkgPath)) {
8905
- const pkg = await fs19.readJson(pkgPath);
9239
+ const pkgPath = path20.join(frontendDir, "package.json");
9240
+ if (await fs21.pathExists(pkgPath)) {
9241
+ const pkg = await fs21.readJson(pkgPath);
8906
9242
  const originalProxy = pkg.proxy ?? null;
8907
9243
  pkg.proxy = `http://localhost:${mockPort}`;
8908
- await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
9244
+ await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
8909
9245
  actions.push({ type: "patched-pkg-proxy", originalProxy });
8910
9246
  const lock2 = { framework, mockPort, frontendDir, actions };
8911
- await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
9247
+ await fs21.writeJson(path20.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
8912
9248
  return { framework, applied: true, devCommand: "npm start" };
8913
9249
  }
8914
9250
  return { framework, applied: false, devCommand: null, note: "No package.json found." };
8915
9251
  }
8916
9252
  const lock = { framework, mockPort, frontendDir, actions };
8917
- await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
9253
+ await fs21.writeJson(path20.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
8918
9254
  const manualNote = framework === "next" ? `Add rewrites in next.config.js to proxy API calls to http://localhost:${mockPort}` : `Add proxy in webpack.config.js devServer to target http://localhost:${mockPort}`;
8919
9255
  return { framework, applied: false, devCommand: null, note: manualNote };
8920
9256
  }
8921
9257
  async function restoreMockProxy(frontendDir) {
8922
- const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
8923
- if (!await fs19.pathExists(lockPath)) {
9258
+ const lockPath = path20.join(frontendDir, MOCK_LOCK_FILE);
9259
+ if (!await fs21.pathExists(lockPath)) {
8924
9260
  return { restored: false, note: "No lock file found \u2014 nothing to restore." };
8925
9261
  }
8926
- const lock = await fs19.readJson(lockPath);
9262
+ const lock = await fs21.readJson(lockPath);
8927
9263
  for (const action of lock.actions) {
8928
9264
  if (action.type === "wrote-file") {
8929
- const fp = path18.join(frontendDir, action.filePath);
8930
- if (await fs19.pathExists(fp)) await fs19.remove(fp);
9265
+ const fp = path20.join(frontendDir, action.filePath);
9266
+ if (await fs21.pathExists(fp)) await fs21.remove(fp);
8931
9267
  } else if (action.type === "added-pkg-script") {
8932
- const pkgPath = path18.join(frontendDir, "package.json");
8933
- if (await fs19.pathExists(pkgPath)) {
8934
- const pkg = await fs19.readJson(pkgPath);
9268
+ const pkgPath = path20.join(frontendDir, "package.json");
9269
+ if (await fs21.pathExists(pkgPath)) {
9270
+ const pkg = await fs21.readJson(pkgPath);
8935
9271
  if (action.originalValue == null) {
8936
9272
  delete pkg.scripts?.[action.key];
8937
9273
  } else {
8938
9274
  pkg.scripts = pkg.scripts ?? {};
8939
9275
  pkg.scripts[action.key] = action.originalValue;
8940
9276
  }
8941
- await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
9277
+ await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
8942
9278
  }
8943
9279
  } else if (action.type === "patched-pkg-proxy") {
8944
- const pkgPath = path18.join(frontendDir, "package.json");
8945
- if (await fs19.pathExists(pkgPath)) {
8946
- const pkg = await fs19.readJson(pkgPath);
9280
+ const pkgPath = path20.join(frontendDir, "package.json");
9281
+ if (await fs21.pathExists(pkgPath)) {
9282
+ const pkg = await fs21.readJson(pkgPath);
8947
9283
  if (action.originalProxy == null) {
8948
9284
  delete pkg.proxy;
8949
9285
  } else {
8950
9286
  pkg.proxy = action.originalProxy;
8951
9287
  }
8952
- await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
9288
+ await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
8953
9289
  }
8954
9290
  }
8955
9291
  }
@@ -8959,7 +9295,7 @@ async function restoreMockProxy(frontendDir) {
8959
9295
  } catch {
8960
9296
  }
8961
9297
  }
8962
- await fs19.remove(lockPath);
9298
+ await fs21.remove(lockPath);
8963
9299
  return { restored: true };
8964
9300
  }
8965
9301
  function startMockServerBackground(serverJsPath, port) {
@@ -8972,23 +9308,23 @@ function startMockServerBackground(serverJsPath, port) {
8972
9308
  return child.pid;
8973
9309
  }
8974
9310
  async function saveMockServerPid(frontendDir, pid) {
8975
- const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
8976
- if (await fs19.pathExists(lockPath)) {
8977
- const lock = await fs19.readJson(lockPath);
9311
+ const lockPath = path20.join(frontendDir, MOCK_LOCK_FILE);
9312
+ if (await fs21.pathExists(lockPath)) {
9313
+ const lock = await fs21.readJson(lockPath);
8978
9314
  lock.mockServerPid = pid;
8979
- await fs19.writeJson(lockPath, lock, { spaces: 2 });
9315
+ await fs21.writeJson(lockPath, lock, { spaces: 2 });
8980
9316
  }
8981
9317
  }
8982
9318
  async function generateMockAssets(dsl, projectDir, opts = {}) {
8983
9319
  const port = opts.port ?? 3001;
8984
- const outputDir = path18.join(projectDir, opts.outputDir ?? "mock");
9320
+ const outputDir = path20.join(projectDir, opts.outputDir ?? "mock");
8985
9321
  const result = { files: [] };
8986
- await fs19.ensureDir(outputDir);
9322
+ await fs21.ensureDir(outputDir);
8987
9323
  const serverJs = generateMockServerJs(dsl, port);
8988
- const serverPath = path18.join(outputDir, "server.js");
8989
- await fs19.writeFile(serverPath, serverJs, "utf-8");
9324
+ const serverPath = path20.join(outputDir, "server.js");
9325
+ await fs21.writeFile(serverPath, serverJs, "utf-8");
8990
9326
  result.files.push({
8991
- path: path18.relative(projectDir, serverPath),
9327
+ path: path20.relative(projectDir, serverPath),
8992
9328
  description: `Express mock server \u2014 run with: node mock/server.js`
8993
9329
  });
8994
9330
  const mockReadme = `# Mock Server
@@ -9018,51 +9354,51 @@ ${dsl.endpoints.map(
9018
9354
 
9019
9355
  Append \`?simulate_error=1\` to any request to test error handling (not yet auto-wired \u2014 edit server.js manually).
9020
9356
  `;
9021
- const readmePath = path18.join(outputDir, "README.md");
9022
- await fs19.writeFile(readmePath, mockReadme, "utf-8");
9357
+ const readmePath = path20.join(outputDir, "README.md");
9358
+ await fs21.writeFile(readmePath, mockReadme, "utf-8");
9023
9359
  result.files.push({
9024
- path: path18.relative(projectDir, readmePath),
9360
+ path: path20.relative(projectDir, readmePath),
9025
9361
  description: "Mock server usage guide"
9026
9362
  });
9027
9363
  if (opts.proxy) {
9028
9364
  const { content, filename } = generateProxyConfig(dsl, port, projectDir);
9029
- const proxyPath = path18.join(projectDir, filename);
9030
- await fs19.ensureDir(path18.dirname(proxyPath));
9031
- await fs19.writeFile(proxyPath, content, "utf-8");
9365
+ const proxyPath = path20.join(projectDir, filename);
9366
+ await fs21.ensureDir(path20.dirname(proxyPath));
9367
+ await fs21.writeFile(proxyPath, content, "utf-8");
9032
9368
  result.files.push({
9033
9369
  path: filename,
9034
9370
  description: "Proxy config snippet \u2014 copy instructions into your framework config"
9035
9371
  });
9036
9372
  }
9037
9373
  if (opts.msw) {
9038
- const mswDir = path18.join(projectDir, "src", "mocks");
9039
- await fs19.ensureDir(mswDir);
9374
+ const mswDir = path20.join(projectDir, "src", "mocks");
9375
+ await fs21.ensureDir(mswDir);
9040
9376
  const handlersContent = generateMswHandlers(dsl);
9041
- const handlersPath = path18.join(mswDir, "handlers.ts");
9042
- await fs19.writeFile(handlersPath, handlersContent, "utf-8");
9377
+ const handlersPath = path20.join(mswDir, "handlers.ts");
9378
+ await fs21.writeFile(handlersPath, handlersContent, "utf-8");
9043
9379
  result.files.push({
9044
- path: path18.relative(projectDir, handlersPath),
9380
+ path: path20.relative(projectDir, handlersPath),
9045
9381
  description: "MSW request handlers"
9046
9382
  });
9047
9383
  const browserContent = generateMswBrowser();
9048
- const browserPath = path18.join(mswDir, "browser.ts");
9049
- await fs19.writeFile(browserPath, browserContent, "utf-8");
9384
+ const browserPath = path20.join(mswDir, "browser.ts");
9385
+ await fs21.writeFile(browserPath, browserContent, "utf-8");
9050
9386
  result.files.push({
9051
- path: path18.relative(projectDir, browserPath),
9387
+ path: path20.relative(projectDir, browserPath),
9052
9388
  description: "MSW browser worker setup"
9053
9389
  });
9054
9390
  }
9055
9391
  return result;
9056
9392
  }
9057
9393
  async function findLatestDslFile(projectDir) {
9058
- const specDir = path18.join(projectDir, ".ai-spec");
9059
- if (!await fs19.pathExists(specDir)) return null;
9394
+ const specDir = path20.join(projectDir, ".ai-spec");
9395
+ if (!await fs21.pathExists(specDir)) return null;
9060
9396
  const allFiles = [];
9061
9397
  async function scan(dir) {
9062
- const entries = await fs19.readdir(dir);
9398
+ const entries = await fs21.readdir(dir);
9063
9399
  for (const entry of entries) {
9064
- const abs = path18.join(dir, entry);
9065
- const stat4 = await fs19.stat(abs);
9400
+ const abs = path20.join(dir, entry);
9401
+ const stat4 = await fs21.stat(abs);
9066
9402
  if (stat4.isDirectory()) {
9067
9403
  await scan(abs);
9068
9404
  } else if (entry.endsWith(".dsl.json")) {
@@ -9073,16 +9409,16 @@ async function findLatestDslFile(projectDir) {
9073
9409
  await scan(specDir);
9074
9410
  if (allFiles.length === 0) return null;
9075
9411
  const withMtimes = await Promise.all(
9076
- allFiles.map(async (f) => ({ f, mtime: (await fs19.stat(f)).mtime }))
9412
+ allFiles.map(async (f) => ({ f, mtime: (await fs21.stat(f)).mtime }))
9077
9413
  );
9078
9414
  withMtimes.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
9079
9415
  return withMtimes[0].f;
9080
9416
  }
9081
9417
 
9082
9418
  // core/spec-updater.ts
9083
- import chalk16 from "chalk";
9084
- import * as path19 from "path";
9085
- import * as fs20 from "fs-extra";
9419
+ import chalk18 from "chalk";
9420
+ import * as path21 from "path";
9421
+ import * as fs22 from "fs-extra";
9086
9422
 
9087
9423
  // prompts/update.prompt.ts
9088
9424
  var specUpdateSystemPrompt = `You are a Senior Software Architect updating an existing Feature Spec based on a change request.
@@ -9228,8 +9564,8 @@ var SpecUpdater = class {
9228
9564
  * Returns all .md spec files sorted newest-first.
9229
9565
  */
9230
9566
  static async findLatestSpec(specsDir) {
9231
- if (!await fs20.pathExists(specsDir)) return null;
9232
- const files = await fs20.readdir(specsDir);
9567
+ if (!await fs22.pathExists(specsDir)) return null;
9568
+ const files = await fs22.readdir(specsDir);
9233
9569
  const pattern = /^feature-(.+)-v(\d+)\.md$/;
9234
9570
  let latest = null;
9235
9571
  for (const file of files) {
@@ -9237,8 +9573,8 @@ var SpecUpdater = class {
9237
9573
  if (!m) continue;
9238
9574
  const version = parseInt(m[2], 10);
9239
9575
  if (!latest || version > latest.version) {
9240
- const filePath = path19.join(specsDir, file);
9241
- const content = await fs20.readFile(filePath, "utf-8");
9576
+ const filePath = path21.join(specsDir, file);
9577
+ const content = await fs22.readFile(filePath, "utf-8");
9242
9578
  latest = { filePath, version, slug: m[1], content };
9243
9579
  }
9244
9580
  }
@@ -9249,16 +9585,16 @@ var SpecUpdater = class {
9249
9585
  * Generates a new version of the spec, re-extracts the DSL, and identifies affected files.
9250
9586
  */
9251
9587
  async update(changeRequest, existingSpecPath, projectDir, context, opts = {}) {
9252
- const existingSpec = await fs20.readFile(existingSpecPath, "utf-8");
9588
+ const existingSpec = await fs22.readFile(existingSpecPath, "utf-8");
9253
9589
  let existingDsl = null;
9254
9590
  const dslFile = await findLatestDslFile(projectDir);
9255
9591
  if (dslFile) {
9256
9592
  try {
9257
- existingDsl = await fs20.readJson(dslFile);
9593
+ existingDsl = await fs22.readJson(dslFile);
9258
9594
  } catch {
9259
9595
  }
9260
9596
  }
9261
- console.log(chalk16.blue(" [1/3] Generating updated spec..."));
9597
+ console.log(chalk18.blue(" [1/3] Generating updated spec..."));
9262
9598
  const updatePrompt = buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context);
9263
9599
  let updatedSpecContent;
9264
9600
  try {
@@ -9267,15 +9603,15 @@ var SpecUpdater = class {
9267
9603
  } catch (err) {
9268
9604
  throw new Error(`Spec update generation failed: ${err.message}`);
9269
9605
  }
9270
- const specBasename = path19.basename(existingSpecPath);
9606
+ const specBasename = path21.basename(existingSpecPath);
9271
9607
  const slugMatch = specBasename.match(/^feature-(.+)-v\d+\.md$/);
9272
9608
  const slug = slugMatch ? slugMatch[1] : "feature";
9273
- const specsDir = path19.dirname(existingSpecPath);
9609
+ const specsDir = path21.dirname(existingSpecPath);
9274
9610
  const { filePath: newSpecPath, version: newVersion } = await nextVersionPath(specsDir, slug);
9275
- await fs20.ensureDir(specsDir);
9276
- await fs20.writeFile(newSpecPath, updatedSpecContent, "utf-8");
9277
- console.log(chalk16.green(` \u2714 New spec written: ${path19.relative(projectDir, newSpecPath)}`));
9278
- console.log(chalk16.blue(" [2/3] Updating DSL..."));
9611
+ await fs22.ensureDir(specsDir);
9612
+ await fs22.writeFile(newSpecPath, updatedSpecContent, "utf-8");
9613
+ console.log(chalk18.green(` \u2714 New spec written: ${path21.relative(projectDir, newSpecPath)}`));
9614
+ console.log(chalk18.blue(" [2/3] Updating DSL..."));
9279
9615
  let updatedDsl = null;
9280
9616
  let newDslPath = null;
9281
9617
  if (existingDsl) {
@@ -9287,7 +9623,7 @@ var SpecUpdater = class {
9287
9623
  updatedDsl = parsed;
9288
9624
  }
9289
9625
  } catch {
9290
- console.log(chalk16.gray(" Targeted DSL update failed \u2014 falling back to full extraction."));
9626
+ console.log(chalk18.gray(" Targeted DSL update failed \u2014 falling back to full extraction."));
9291
9627
  }
9292
9628
  }
9293
9629
  if (!updatedDsl) {
@@ -9296,15 +9632,15 @@ var SpecUpdater = class {
9296
9632
  }
9297
9633
  if (updatedDsl) {
9298
9634
  const dslPath = newSpecPath.replace(/\.md$/, ".dsl.json");
9299
- await fs20.writeJson(dslPath, updatedDsl, { spaces: 2 });
9635
+ await fs22.writeJson(dslPath, updatedDsl, { spaces: 2 });
9300
9636
  newDslPath = dslPath;
9301
- console.log(chalk16.green(` \u2714 DSL updated: ${path19.relative(projectDir, dslPath)}`));
9637
+ console.log(chalk18.green(` \u2714 DSL updated: ${path21.relative(projectDir, dslPath)}`));
9302
9638
  } else {
9303
- console.log(chalk16.yellow(" \u26A0 DSL update failed \u2014 continuing without DSL."));
9639
+ console.log(chalk18.yellow(" \u26A0 DSL update failed \u2014 continuing without DSL."));
9304
9640
  }
9305
9641
  let affectedFiles = [];
9306
9642
  if (!opts.skipAffectedFiles && updatedDsl && existingDsl && context) {
9307
- console.log(chalk16.blue(" [3/3] Identifying affected files..."));
9643
+ console.log(chalk18.blue(" [3/3] Identifying affected files..."));
9308
9644
  const systemPrompt = getCodeGenSystemPrompt(opts.repoType);
9309
9645
  const affectedPrompt = buildAffectedFilesPrompt(
9310
9646
  changeRequest,
@@ -9315,9 +9651,9 @@ var SpecUpdater = class {
9315
9651
  try {
9316
9652
  const affectedRaw = await this.provider.generate(affectedPrompt, systemPrompt);
9317
9653
  affectedFiles = parseAffectedFiles(affectedRaw);
9318
- console.log(chalk16.green(` \u2714 ${affectedFiles.length} file(s) identified for update`));
9654
+ console.log(chalk18.green(` \u2714 ${affectedFiles.length} file(s) identified for update`));
9319
9655
  } catch {
9320
- console.log(chalk16.gray(" Could not identify affected files \u2014 use manual selection."));
9656
+ console.log(chalk18.gray(" Could not identify affected files \u2014 use manual selection."));
9321
9657
  }
9322
9658
  }
9323
9659
  return { newSpecPath, newVersion, newDslPath, affectedFiles, updatedDsl };
@@ -9325,8 +9661,8 @@ var SpecUpdater = class {
9325
9661
  };
9326
9662
 
9327
9663
  // core/openapi-exporter.ts
9328
- import * as path20 from "path";
9329
- import * as fs21 from "fs-extra";
9664
+ import * as path22 from "path";
9665
+ import * as fs23 from "fs-extra";
9330
9666
  function dslTypeToOASchema(typeDesc, fieldName = "") {
9331
9667
  const t = typeDesc.toLowerCase();
9332
9668
  if (t === "string" || t.includes("string")) {
@@ -9555,7 +9891,7 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
9555
9891
  const format = opts.format ?? "yaml";
9556
9892
  const serverUrl = opts.serverUrl ?? "http://localhost:3000";
9557
9893
  const defaultName = `openapi.${format}`;
9558
- const outputPath = opts.outputPath ? path20.isAbsolute(opts.outputPath) ? opts.outputPath : path20.join(projectDir, opts.outputPath) : path20.join(projectDir, defaultName);
9894
+ const outputPath = opts.outputPath ? path22.isAbsolute(opts.outputPath) ? opts.outputPath : path22.join(projectDir, opts.outputPath) : path22.join(projectDir, defaultName);
9559
9895
  const doc = dslToOpenApi(dsl, serverUrl);
9560
9896
  let content;
9561
9897
  if (format === "json") {
@@ -9563,8 +9899,8 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
9563
9899
  } else {
9564
9900
  content = buildYamlDoc(doc);
9565
9901
  }
9566
- await fs21.ensureDir(path20.dirname(outputPath));
9567
- await fs21.writeFile(outputPath, content, "utf-8");
9902
+ await fs23.ensureDir(path22.dirname(outputPath));
9903
+ await fs23.writeFile(outputPath, content, "utf-8");
9568
9904
  return outputPath;
9569
9905
  }
9570
9906
 
@@ -9572,9 +9908,9 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
9572
9908
  dotenv.config();
9573
9909
  var CONFIG_FILE = ".ai-spec.json";
9574
9910
  async function loadConfig(dir) {
9575
- const p = path21.join(dir, CONFIG_FILE);
9576
- if (await fs22.pathExists(p)) {
9577
- return fs22.readJson(p);
9911
+ const p = path23.join(dir, CONFIG_FILE);
9912
+ if (await fs24.pathExists(p)) {
9913
+ return fs24.readJson(p);
9578
9914
  }
9579
9915
  return {};
9580
9916
  }
@@ -9599,20 +9935,20 @@ async function resolveApiKey(providerName, cliKey) {
9599
9935
  validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
9600
9936
  });
9601
9937
  await saveKey(providerName, newKey.trim());
9602
- console.log(chalk17.gray(` Key saved to ${KEY_STORE_FILE}`));
9938
+ console.log(chalk19.gray(` Key saved to ${KEY_STORE_FILE}`));
9603
9939
  return newKey.trim();
9604
9940
  }
9605
9941
  function printBanner(opts) {
9606
- console.log(chalk17.blue("\n" + "\u2500".repeat(52)));
9607
- console.log(chalk17.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
9608
- console.log(chalk17.blue("\u2500".repeat(52)));
9609
- console.log(chalk17.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
9942
+ console.log(chalk19.blue("\n" + "\u2500".repeat(52)));
9943
+ console.log(chalk19.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
9944
+ console.log(chalk19.blue("\u2500".repeat(52)));
9945
+ console.log(chalk19.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
9610
9946
  console.log(
9611
- chalk17.gray(
9947
+ chalk19.gray(
9612
9948
  ` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
9613
9949
  )
9614
9950
  );
9615
- console.log(chalk17.blue("\u2500".repeat(52) + "\n"));
9951
+ console.log(chalk19.blue("\u2500".repeat(52) + "\n"));
9616
9952
  }
9617
9953
  var program = new Command();
9618
9954
  program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
@@ -9639,42 +9975,42 @@ program.command("create").description("Generate a feature spec and kick off code
9639
9975
  const workspaceLoader = new WorkspaceLoader(currentDir);
9640
9976
  const workspaceConfig = await workspaceLoader.load();
9641
9977
  if (workspaceConfig) {
9642
- console.log(chalk17.cyan(`
9978
+ console.log(chalk19.cyan(`
9643
9979
  [Workspace] Detected workspace: ${workspaceConfig.name}`));
9644
- console.log(chalk17.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
9980
+ console.log(chalk19.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
9645
9981
  const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
9646
9982
  if (opts.serve) {
9647
- console.log(chalk17.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
9983
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
9648
9984
  const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
9649
9985
  const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
9650
9986
  if (!backendResult) {
9651
- console.log(chalk17.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
9987
+ console.log(chalk19.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
9652
9988
  } else {
9653
9989
  const mockPort = 3001;
9654
9990
  const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
9655
- const serverJsPath = path21.join(backendResult.repoAbsPath, "mock", "server.js");
9656
- console.log(chalk17.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
9991
+ const serverJsPath = path23.join(backendResult.repoAbsPath, "mock", "server.js");
9992
+ console.log(chalk19.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
9657
9993
  const pid = startMockServerBackground(serverJsPath, mockPort);
9658
- console.log(chalk17.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
9994
+ console.log(chalk19.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
9659
9995
  if (frontendResult) {
9660
9996
  const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
9661
9997
  await saveMockServerPid(frontendResult.repoAbsPath, pid);
9662
9998
  if (proxyResult.applied) {
9663
- console.log(chalk17.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
9664
- console.log(chalk17.bold.cyan(`
9999
+ console.log(chalk19.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
10000
+ console.log(chalk19.bold.cyan(`
9665
10001
  Ready! Run your frontend dev server:`));
9666
- console.log(chalk17.white(` cd ${frontendResult.repoAbsPath}`));
9667
- console.log(chalk17.white(` ${proxyResult.devCommand}`));
9668
- console.log(chalk17.gray(`
10002
+ console.log(chalk19.white(` cd ${frontendResult.repoAbsPath}`));
10003
+ console.log(chalk19.white(` ${proxyResult.devCommand}`));
10004
+ console.log(chalk19.gray(`
9669
10005
  When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
9670
10006
  } else {
9671
- console.log(chalk17.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
9672
- if (proxyResult.note) console.log(chalk17.gray(` ${proxyResult.note}`));
9673
- console.log(chalk17.gray(` Mock server: http://localhost:${mockPort}`));
10007
+ console.log(chalk19.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
10008
+ if (proxyResult.note) console.log(chalk19.gray(` ${proxyResult.note}`));
10009
+ console.log(chalk19.gray(` Mock server: http://localhost:${mockPort}`));
9674
10010
  }
9675
10011
  } else {
9676
- console.log(chalk17.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
9677
- console.log(chalk17.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
10012
+ console.log(chalk19.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
10013
+ console.log(chalk19.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
9678
10014
  }
9679
10015
  }
9680
10016
  }
@@ -9694,23 +10030,32 @@ program.command("create").description("Generate a feature spec and kick off code
9694
10030
  codegenProvider: codegenProviderName,
9695
10031
  codegenModel: codegenModelName
9696
10032
  });
9697
- console.log(chalk17.blue("[1/6] Loading project context..."));
10033
+ const runId = generateRunId();
10034
+ console.log(chalk19.gray(` Run ID: ${runId}`));
10035
+ const runSnapshot = new RunSnapshot(currentDir, runId);
10036
+ setActiveSnapshot(runSnapshot);
10037
+ const runLogger = new RunLogger(currentDir, runId, {
10038
+ provider: specProviderName,
10039
+ model: specModelName
10040
+ });
10041
+ setActiveLogger(runLogger);
10042
+ console.log(chalk19.blue("[1/6] Loading project context..."));
9698
10043
  const loader = new ContextLoader(currentDir);
9699
10044
  const context = await loader.loadProjectContext();
9700
10045
  const { type: detectedRepoType } = await detectRepoType(currentDir);
9701
- console.log(chalk17.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
9702
- console.log(chalk17.gray(` Dependencies: ${context.dependencies.length} packages`));
9703
- console.log(chalk17.gray(` API files : ${context.apiStructure.length} files`));
10046
+ console.log(chalk19.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10047
+ console.log(chalk19.gray(` Dependencies: ${context.dependencies.length} packages`));
10048
+ console.log(chalk19.gray(` API files : ${context.apiStructure.length} files`));
9704
10049
  if (context.schema) {
9705
- console.log(chalk17.gray(` Prisma schema: found`));
10050
+ console.log(chalk19.gray(` Prisma schema: found`));
9706
10051
  }
9707
10052
  if (context.constitution) {
9708
- console.log(chalk17.green(` Constitution : found (.ai-spec-constitution.md)`));
10053
+ console.log(chalk19.green(` Constitution : found (.ai-spec-constitution.md)`));
9709
10054
  if (context.constitution.length > 6e3) {
9710
- console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10055
+ console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
9711
10056
  }
9712
10057
  } else {
9713
- console.log(chalk17.yellow(" Constitution : not found \u2014 auto-generating..."));
10058
+ console.log(chalk19.yellow(" Constitution : not found \u2014 auto-generating..."));
9714
10059
  try {
9715
10060
  const constitutionGen = new ConstitutionGenerator(
9716
10061
  createProvider(specProviderName, specApiKey, specModelName)
@@ -9718,12 +10063,12 @@ program.command("create").description("Generate a feature spec and kick off code
9718
10063
  const constitutionContent = await constitutionGen.generate(currentDir);
9719
10064
  await constitutionGen.saveConstitution(currentDir, constitutionContent);
9720
10065
  context.constitution = constitutionContent;
9721
- console.log(chalk17.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
10066
+ console.log(chalk19.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
9722
10067
  } catch (err) {
9723
- console.log(chalk17.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
10068
+ console.log(chalk19.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
9724
10069
  }
9725
10070
  }
9726
- console.log(chalk17.blue(`
10071
+ console.log(chalk19.blue(`
9727
10072
  [2/6] Generating spec with ${specProviderName}/${specModelName}...`));
9728
10073
  const specProvider = createProvider(specProviderName, specApiKey, specModelName);
9729
10074
  let initialSpec;
@@ -9732,28 +10077,28 @@ program.command("create").description("Generate a feature spec and kick off code
9732
10077
  if (opts.skipTasks) {
9733
10078
  const generator = new SpecGenerator(specProvider);
9734
10079
  initialSpec = await generator.generateSpec(idea, context);
9735
- console.log(chalk17.green(" \u2714 Spec generated."));
10080
+ console.log(chalk19.green(" \u2714 Spec generated."));
9736
10081
  } else {
9737
10082
  const result = await generateSpecWithTasks(specProvider, idea, context);
9738
10083
  initialSpec = result.spec;
9739
10084
  initialTasks = result.tasks;
9740
- console.log(chalk17.green(` \u2714 Spec generated.`));
10085
+ console.log(chalk19.green(` \u2714 Spec generated.`));
9741
10086
  if (initialTasks.length > 0) {
9742
- console.log(chalk17.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
10087
+ console.log(chalk19.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
9743
10088
  } else {
9744
- console.log(chalk17.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
10089
+ console.log(chalk19.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
9745
10090
  }
9746
10091
  }
9747
10092
  } catch (err) {
9748
- console.error(chalk17.red(" \u2718 Spec generation failed:"), err);
10093
+ console.error(chalk19.red(" \u2718 Spec generation failed:"), err);
9749
10094
  process.exit(1);
9750
10095
  }
9751
10096
  let finalSpec;
9752
10097
  if (opts.fast) {
9753
- console.log(chalk17.gray("\n[3/6] Skipping refinement (--fast)."));
10098
+ console.log(chalk19.gray("\n[3/6] Skipping refinement (--fast)."));
9754
10099
  finalSpec = initialSpec;
9755
10100
  } else {
9756
- console.log(chalk17.blue("\n[3/6] Interactive spec refinement..."));
10101
+ console.log(chalk19.blue("\n[3/6] Interactive spec refinement..."));
9757
10102
  const refiner = new SpecRefiner(specProvider);
9758
10103
  finalSpec = await refiner.refineLoop(initialSpec);
9759
10104
  }
@@ -9762,48 +10107,48 @@ program.command("create").description("Generate a feature spec and kick off code
9762
10107
  const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
9763
10108
  if (shouldRunAssessment) {
9764
10109
  if (!opts.auto) {
9765
- console.log(chalk17.blue("\n[3.4/6] Spec quality assessment..."));
10110
+ console.log(chalk19.blue("\n[3.4/6] Spec quality assessment..."));
9766
10111
  }
9767
10112
  const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
9768
10113
  if (assessment) {
9769
10114
  if (!opts.auto) printSpecAssessment(assessment);
9770
10115
  if (minScore > 0 && assessment.overallScore < minScore) {
9771
10116
  if (opts.force) {
9772
- console.log(chalk17.yellow(`
10117
+ console.log(chalk19.yellow(`
9773
10118
  \u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
9774
10119
  } else {
9775
- console.log(chalk17.red(`
10120
+ console.log(chalk19.red(`
9776
10121
  \u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
9777
10122
  if (!opts.auto) {
9778
- console.log(chalk17.gray(` Address the issues above and re-run, or use --force to bypass.`));
10123
+ console.log(chalk19.gray(` Address the issues above and re-run, or use --force to bypass.`));
9779
10124
  } else {
9780
- console.log(chalk17.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
10125
+ console.log(chalk19.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
9781
10126
  }
9782
- console.log(chalk17.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
10127
+ console.log(chalk19.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
9783
10128
  process.exit(1);
9784
10129
  }
9785
10130
  }
9786
10131
  } else if (!opts.auto) {
9787
- console.log(chalk17.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
10132
+ console.log(chalk19.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
9788
10133
  }
9789
10134
  }
9790
10135
  if (!opts.auto) {
9791
- console.log(chalk17.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
10136
+ console.log(chalk19.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
9792
10137
  const specLines = finalSpec.split("\n").length;
9793
10138
  const specWords = finalSpec.split(/\s+/).length;
9794
10139
  const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
9795
- console.log(chalk17.gray(` Spec length : ${specLines} lines / ${specWords} words`));
9796
- if (taskCountHint) console.log(chalk17.gray(taskCountHint));
9797
- const previewSpecsDir = path21.join(currentDir, "specs");
10140
+ console.log(chalk19.gray(` Spec length : ${specLines} lines / ${specWords} words`));
10141
+ if (taskCountHint) console.log(chalk19.gray(taskCountHint));
10142
+ const previewSpecsDir = path23.join(currentDir, "specs");
9798
10143
  const slug = featureSlug;
9799
10144
  const prevVersion = await findLatestVersion(previewSpecsDir, slug);
9800
10145
  if (prevVersion) {
9801
- console.log(chalk17.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
10146
+ console.log(chalk19.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
9802
10147
  const diff = computeDiff(prevVersion.content, finalSpec);
9803
- console.log(chalk17.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10148
+ console.log(chalk19.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
9804
10149
  printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
9805
10150
  printDiff(diff);
9806
- console.log(chalk17.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"));
10151
+ console.log(chalk19.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"));
9807
10152
  }
9808
10153
  const gate = await select3({
9809
10154
  message: "Ready to proceed to code generation?",
@@ -9814,9 +10159,9 @@ program.command("create").description("Generate a feature spec and kick off code
9814
10159
  ]
9815
10160
  });
9816
10161
  if (gate === "view") {
9817
- console.log(chalk17.cyan("\n" + "\u2500".repeat(52)));
10162
+ console.log(chalk19.cyan("\n" + "\u2500".repeat(52)));
9818
10163
  console.log(finalSpec);
9819
- console.log(chalk17.cyan("\u2500".repeat(52) + "\n"));
10164
+ console.log(chalk19.cyan("\u2500".repeat(52) + "\n"));
9820
10165
  const confirm22 = await select3({
9821
10166
  message: "Proceed to code generation?",
9822
10167
  choices: [
@@ -9825,88 +10170,88 @@ program.command("create").description("Generate a feature spec and kick off code
9825
10170
  ]
9826
10171
  });
9827
10172
  if (confirm22 === "abort") {
9828
- console.log(chalk17.yellow(" Aborted. Spec was NOT saved."));
10173
+ console.log(chalk19.yellow(" Aborted. Spec was NOT saved."));
9829
10174
  process.exit(0);
9830
10175
  }
9831
10176
  } else if (gate === "abort") {
9832
- console.log(chalk17.yellow(" Aborted. Spec was NOT saved."));
10177
+ console.log(chalk19.yellow(" Aborted. Spec was NOT saved."));
9833
10178
  process.exit(0);
9834
10179
  }
9835
- console.log(chalk17.green(" \u2714 Approved \u2014 continuing to code generation."));
10180
+ console.log(chalk19.green(" \u2714 Approved \u2014 continuing to code generation."));
9836
10181
  } else {
9837
- console.log(chalk17.gray("[3.5/6] Approval Gate: skipped (--auto)."));
10182
+ console.log(chalk19.gray("[3.5/6] Approval Gate: skipped (--auto)."));
9838
10183
  }
9839
10184
  let extractedDsl = null;
9840
10185
  if (opts.skipDsl) {
9841
- console.log(chalk17.gray("\n[DSL] Skipped (--skip-dsl)."));
10186
+ console.log(chalk19.gray("\n[DSL] Skipped (--skip-dsl)."));
9842
10187
  } else {
9843
- console.log(chalk17.blue("\n[DSL] Extracting structured DSL from spec..."));
9844
- console.log(chalk17.gray(` Provider: ${specProviderName}/${specModelName}`));
10188
+ console.log(chalk19.blue("\n[DSL] Extracting structured DSL from spec..."));
10189
+ console.log(chalk19.gray(` Provider: ${specProviderName}/${specModelName}`));
9845
10190
  try {
9846
10191
  const isFrontend = isFrontendDeps(context.dependencies);
9847
- if (isFrontend) console.log(chalk17.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
10192
+ if (isFrontend) console.log(chalk19.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
9848
10193
  const dslExtractor = new DslExtractor(specProvider);
9849
10194
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
9850
10195
  if (extractedDsl) {
9851
- console.log(chalk17.green(" \u2714 DSL extracted and validated."));
10196
+ console.log(chalk19.green(" \u2714 DSL extracted and validated."));
9852
10197
  } else {
9853
- console.log(chalk17.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
10198
+ console.log(chalk19.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
9854
10199
  }
9855
10200
  } catch (err) {
9856
- console.log(chalk17.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
10201
+ console.log(chalk19.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
9857
10202
  }
9858
10203
  }
9859
10204
  const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
9860
10205
  const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
9861
10206
  let workingDir = currentDir;
9862
10207
  if (!skipWorktree) {
9863
- console.log(chalk17.blue("\n[4/6] Setting up git worktree..."));
10208
+ console.log(chalk19.blue("\n[4/6] Setting up git worktree..."));
9864
10209
  const worktreeManager = new GitWorktreeManager(currentDir);
9865
10210
  const worktreePath = await worktreeManager.createWorktree(idea);
9866
10211
  if (worktreePath) workingDir = worktreePath;
9867
10212
  } else {
9868
10213
  const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
9869
- console.log(chalk17.gray(`[4/6] Skipping worktree${reason}.`));
10214
+ console.log(chalk19.gray(`[4/6] Skipping worktree${reason}.`));
9870
10215
  }
9871
- const specsDir = path21.join(workingDir, "specs");
9872
- await fs22.ensureDir(specsDir);
10216
+ const specsDir = path23.join(workingDir, "specs");
10217
+ await fs24.ensureDir(specsDir);
9873
10218
  const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
9874
- await fs22.writeFile(specFile, finalSpec, "utf-8");
9875
- console.log(chalk17.green(`
9876
- [5/6] \u2714 Spec saved: ${specFile}`) + chalk17.gray(` (v${specVersion})`));
10219
+ await fs24.writeFile(specFile, finalSpec, "utf-8");
10220
+ console.log(chalk19.green(`
10221
+ [5/6] \u2714 Spec saved: ${specFile}`) + chalk19.gray(` (v${specVersion})`));
9877
10222
  let savedDslFile = null;
9878
10223
  if (extractedDsl) {
9879
10224
  const dslExtractor = new DslExtractor(specProvider);
9880
10225
  savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
9881
- console.log(chalk17.green(` \u2714 DSL saved : ${savedDslFile}`));
10226
+ console.log(chalk19.green(` \u2714 DSL saved : ${savedDslFile}`));
9882
10227
  }
9883
10228
  if (!opts.skipTasks) {
9884
10229
  const taskGen = new TaskGenerator(specProvider);
9885
10230
  let tasksToSave = initialTasks;
9886
10231
  if (tasksToSave.length === 0) {
9887
- console.log(chalk17.blue(`
10232
+ console.log(chalk19.blue(`
9888
10233
  Generating tasks (separate call)...`));
9889
10234
  try {
9890
10235
  tasksToSave = await taskGen.generateTasks(finalSpec, context);
9891
10236
  } catch (err) {
9892
- console.log(chalk17.yellow(` \u26A0 Task generation failed: ${err.message}`));
10237
+ console.log(chalk19.yellow(` \u26A0 Task generation failed: ${err.message}`));
9893
10238
  }
9894
10239
  }
9895
10240
  if (tasksToSave.length > 0) {
9896
10241
  const sorted = taskGen.sortByLayer(tasksToSave);
9897
10242
  const tasksFile = await taskGen.saveTasks(sorted, specFile);
9898
10243
  printTasks(sorted);
9899
- console.log(chalk17.green(` \u2714 Tasks saved: ${tasksFile}`));
10244
+ console.log(chalk19.green(` \u2714 Tasks saved: ${tasksFile}`));
9900
10245
  } else {
9901
- console.log(chalk17.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
10246
+ console.log(chalk19.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
9902
10247
  }
9903
10248
  }
9904
- console.log(chalk17.blue(`
10249
+ console.log(chalk19.blue(`
9905
10250
  [6/6] Code generation (mode: ${codegenMode})...`));
9906
10251
  const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
9907
10252
  let generatedTestFiles = [];
9908
10253
  if (opts.tdd && extractedDsl) {
9909
- console.log(chalk17.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
10254
+ console.log(chalk19.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
9910
10255
  const testGen = new TestGenerator(codegenProvider);
9911
10256
  generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
9912
10257
  }
@@ -9918,22 +10263,22 @@ program.command("create").description("Generate a feature spec and kick off code
9918
10263
  repoType: detectedRepoType
9919
10264
  });
9920
10265
  if (opts.tdd) {
9921
- console.log(chalk17.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
10266
+ console.log(chalk19.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
9922
10267
  } else if (opts.skipTests) {
9923
- console.log(chalk17.gray("\n[7/9] Skipping test generation (--skip-tests)."));
10268
+ console.log(chalk19.gray("\n[7/9] Skipping test generation (--skip-tests)."));
9924
10269
  } else if (!extractedDsl) {
9925
- console.log(chalk17.gray("\n[7/9] Skipping test generation (no DSL available)."));
10270
+ console.log(chalk19.gray("\n[7/9] Skipping test generation (no DSL available)."));
9926
10271
  } else {
9927
- console.log(chalk17.blue(`
10272
+ console.log(chalk19.blue(`
9928
10273
  [7/9] Test skeleton generation...`));
9929
10274
  const testGen = new TestGenerator(codegenProvider);
9930
10275
  generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
9931
10276
  }
9932
10277
  if (opts.skipErrorFeedback) {
9933
- console.log(chalk17.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
10278
+ console.log(chalk19.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
9934
10279
  } else {
9935
10280
  if (opts.tdd) {
9936
- console.log(chalk17.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
10281
+ console.log(chalk19.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
9937
10282
  }
9938
10283
  await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
9939
10284
  maxCycles: opts.tdd ? 3 : 2
@@ -9942,9 +10287,9 @@ program.command("create").description("Generate a feature spec and kick off code
9942
10287
  }
9943
10288
  let reviewResult = "";
9944
10289
  if (!opts.skipReview) {
9945
- console.log(chalk17.blue("\n[9/9] Automated code review (2-pass: architecture + implementation)..."));
10290
+ console.log(chalk19.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
9946
10291
  const reviewer = new CodeReviewer(specProvider, currentDir);
9947
- const savedSpec = await fs22.readFile(specFile, "utf-8");
10292
+ const savedSpec = await fs24.readFile(specFile, "utf-8");
9948
10293
  if (codegenMode === "api" && generatedFiles.length > 0) {
9949
10294
  reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
9950
10295
  } else {
@@ -9958,15 +10303,20 @@ program.command("create").description("Generate a feature spec and kick off code
9958
10303
  }
9959
10304
  await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
9960
10305
  }
9961
- console.log(chalk17.bold.green("\n\u2714 All done!"));
9962
- console.log(chalk17.gray(` Spec : ${specFile}`));
9963
- if (savedDslFile) console.log(chalk17.gray(` DSL : ${savedDslFile}`));
10306
+ runLogger.finish();
10307
+ console.log(chalk19.bold.green("\n\u2714 All done!"));
10308
+ console.log(chalk19.gray(` Spec : ${specFile}`));
10309
+ if (savedDslFile) console.log(chalk19.gray(` DSL : ${savedDslFile}`));
9964
10310
  if (generatedTestFiles.length > 0) {
9965
- console.log(chalk17.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
10311
+ console.log(chalk19.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
9966
10312
  }
9967
- console.log(chalk17.gray(` Working dir : ${workingDir}`));
10313
+ console.log(chalk19.gray(` Working dir : ${workingDir}`));
9968
10314
  if (workingDir !== currentDir) {
9969
- console.log(chalk17.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10315
+ console.log(chalk19.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10316
+ }
10317
+ runLogger.printSummary();
10318
+ if (runSnapshot.fileCount > 0) {
10319
+ console.log(chalk19.gray(` To undo changes: ai-spec restore ${runId}`));
9970
10320
  }
9971
10321
  });
9972
10322
  program.command("review").description("Run AI code review on current git diff against a spec").argument("[specFile]", "Path to spec file (auto-detects latest in specs/ if omitted)").option(
@@ -9983,24 +10333,24 @@ program.command("review").description("Run AI code review on current git diff ag
9983
10333
  const reviewer = new CodeReviewer(provider, currentDir);
9984
10334
  let specContent = "";
9985
10335
  let resolvedSpecFile;
9986
- if (specFile && await fs22.pathExists(specFile)) {
9987
- specContent = await fs22.readFile(specFile, "utf-8");
10336
+ if (specFile && await fs24.pathExists(specFile)) {
10337
+ specContent = await fs24.readFile(specFile, "utf-8");
9988
10338
  resolvedSpecFile = specFile;
9989
- console.log(chalk17.gray(`Using spec: ${specFile}`));
10339
+ console.log(chalk19.gray(`Using spec: ${specFile}`));
9990
10340
  } else {
9991
- const specsDir = path21.join(currentDir, "specs");
9992
- if (await fs22.pathExists(specsDir)) {
9993
- const files = (await fs22.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
10341
+ const specsDir = path23.join(currentDir, "specs");
10342
+ if (await fs24.pathExists(specsDir)) {
10343
+ const files = (await fs24.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
9994
10344
  if (files.length > 0) {
9995
- const latest = path21.join(specsDir, files[0]);
9996
- specContent = await fs22.readFile(latest, "utf-8");
10345
+ const latest = path23.join(specsDir, files[0]);
10346
+ specContent = await fs24.readFile(latest, "utf-8");
9997
10347
  resolvedSpecFile = latest;
9998
- console.log(chalk17.gray(`Auto-detected spec: specs/${files[0]}`));
10348
+ console.log(chalk19.gray(`Auto-detected spec: specs/${files[0]}`));
9999
10349
  }
10000
10350
  }
10001
10351
  }
10002
10352
  if (!specContent) {
10003
- console.log(chalk17.yellow("No spec file found. Running review without spec context."));
10353
+ console.log(chalk19.yellow("No spec file found. Running review without spec context."));
10004
10354
  }
10005
10355
  await reviewer.reviewCode(specContent, resolvedSpecFile);
10006
10356
  await reviewer.printScoreTrend();
@@ -10027,15 +10377,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
10027
10377
  auto: opts.auto
10028
10378
  });
10029
10379
  if (result.written) {
10030
- console.log(chalk17.blue("\n Summary:"));
10031
- console.log(chalk17.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
10032
- console.log(chalk17.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
10380
+ console.log(chalk19.blue("\n Summary:"));
10381
+ console.log(chalk19.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
10382
+ console.log(chalk19.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
10033
10383
  if (result.backupPath) {
10034
- console.log(chalk17.gray(` Backup: ${path21.basename(result.backupPath)}`));
10384
+ console.log(chalk19.gray(` Backup: ${path23.basename(result.backupPath)}`));
10035
10385
  }
10036
10386
  }
10037
10387
  } catch (err) {
10038
- console.error(chalk17.red(` \u2718 Consolidation failed: ${err.message}`));
10388
+ console.error(chalk19.red(` \u2718 Consolidation failed: ${err.message}`));
10039
10389
  process.exit(1);
10040
10390
  }
10041
10391
  return;
@@ -10043,75 +10393,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
10043
10393
  if (opts.global) {
10044
10394
  const existing = await loadGlobalConstitution([currentDir]);
10045
10395
  if (existing && !opts.force) {
10046
- console.log(chalk17.yellow(`
10396
+ console.log(chalk19.yellow(`
10047
10397
  Global constitution already exists at: ${existing.source}`));
10048
- console.log(chalk17.gray(" Use --force to overwrite it."));
10398
+ console.log(chalk19.gray(" Use --force to overwrite it."));
10049
10399
  return;
10050
10400
  }
10051
- console.log(chalk17.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10052
- console.log(chalk17.gray(` Provider: ${providerName}/${modelName}`));
10053
- console.log(chalk17.gray(" Scanning repos in workspace..."));
10401
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10402
+ console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
10403
+ console.log(chalk19.gray(" Scanning repos in workspace..."));
10054
10404
  const loader = new ContextLoader(currentDir);
10055
10405
  const ctx = await loader.loadProjectContext();
10056
10406
  const summary = [
10057
10407
  `Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
10058
10408
  `Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
10059
10409
  ].join("\n");
10060
- const prompt = buildGlobalConstitutionPrompt([{ name: path21.basename(currentDir), summary }]);
10410
+ const prompt = buildGlobalConstitutionPrompt([{ name: path23.basename(currentDir), summary }]);
10061
10411
  let globalConstitution;
10062
10412
  try {
10063
10413
  globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
10064
10414
  } catch (err) {
10065
- console.error(chalk17.red(" \u2718 Failed to generate global constitution:"), err);
10415
+ console.error(chalk19.red(" \u2718 Failed to generate global constitution:"), err);
10066
10416
  process.exit(1);
10067
10417
  }
10068
10418
  const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
10069
- console.log(chalk17.green(`
10419
+ console.log(chalk19.green(`
10070
10420
  \u2714 Global constitution saved: ${saved2}`));
10071
- console.log(chalk17.gray(" This will be automatically merged into all project constitutions in this workspace."));
10072
- console.log(chalk17.gray(" Project-level rules always override global rules.\n"));
10073
- console.log(chalk17.bold(" Preview:"));
10074
- console.log(chalk17.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
10421
+ console.log(chalk19.gray(" This will be automatically merged into all project constitutions in this workspace."));
10422
+ console.log(chalk19.gray(" Project-level rules always override global rules.\n"));
10423
+ console.log(chalk19.bold(" Preview:"));
10424
+ console.log(chalk19.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
10075
10425
  if (globalConstitution.split("\n").length > 12) {
10076
- console.log(chalk17.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
10426
+ console.log(chalk19.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
10077
10427
  }
10078
10428
  return;
10079
10429
  }
10080
- const constitutionPath = path21.join(currentDir, CONSTITUTION_FILE);
10081
- if (!opts.force && await fs22.pathExists(constitutionPath)) {
10082
- console.log(chalk17.yellow(`
10430
+ const constitutionPath = path23.join(currentDir, CONSTITUTION_FILE);
10431
+ if (!opts.force && await fs24.pathExists(constitutionPath)) {
10432
+ console.log(chalk19.yellow(`
10083
10433
  ${CONSTITUTION_FILE} already exists.`));
10084
- console.log(chalk17.gray(" Use --force to overwrite it."));
10085
- console.log(chalk17.gray(` Or edit it directly: ${constitutionPath}`));
10434
+ console.log(chalk19.gray(" Use --force to overwrite it."));
10435
+ console.log(chalk19.gray(` Or edit it directly: ${constitutionPath}`));
10086
10436
  return;
10087
10437
  }
10088
- console.log(chalk17.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10089
- console.log(chalk17.gray(` Provider: ${providerName}/${modelName}`));
10090
- console.log(chalk17.gray(" Analyzing codebase..."));
10438
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10439
+ console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
10440
+ console.log(chalk19.gray(" Analyzing codebase..."));
10091
10441
  const generator = new ConstitutionGenerator(provider);
10092
10442
  let constitution;
10093
10443
  try {
10094
10444
  constitution = await generator.generate(currentDir);
10095
10445
  } catch (err) {
10096
- console.error(chalk17.red(" \u2718 Failed to generate constitution:"), err);
10446
+ console.error(chalk19.red(" \u2718 Failed to generate constitution:"), err);
10097
10447
  process.exit(1);
10098
10448
  }
10099
10449
  const saved = await generator.saveConstitution(currentDir, constitution);
10100
- const globalResult = await loadGlobalConstitution([path21.dirname(currentDir)]);
10450
+ const globalResult = await loadGlobalConstitution([path23.dirname(currentDir)]);
10101
10451
  if (globalResult) {
10102
- console.log(chalk17.cyan(`
10452
+ console.log(chalk19.cyan(`
10103
10453
  \u2139 Global constitution detected: ${globalResult.source}`));
10104
- console.log(chalk17.gray(" It will be merged with this project constitution at runtime."));
10105
- console.log(chalk17.gray(" Project rules take priority over global rules."));
10454
+ console.log(chalk19.gray(" It will be merged with this project constitution at runtime."));
10455
+ console.log(chalk19.gray(" Project rules take priority over global rules."));
10106
10456
  }
10107
- console.log(chalk17.green(`
10457
+ console.log(chalk19.green(`
10108
10458
  \u2714 Constitution saved: ${saved}`));
10109
- console.log(chalk17.gray(" This file will be automatically used in all future `ai-spec create` runs."));
10110
- console.log(chalk17.gray(" Edit it to add custom rules or red lines for your project.\n"));
10111
- console.log(chalk17.bold(" Preview:"));
10112
- console.log(chalk17.gray(constitution.split("\n").slice(0, 15).join("\n")));
10459
+ console.log(chalk19.gray(" This file will be automatically used in all future `ai-spec create` runs."));
10460
+ console.log(chalk19.gray(" Edit it to add custom rules or red lines for your project.\n"));
10461
+ console.log(chalk19.bold(" Preview:"));
10462
+ console.log(chalk19.gray(constitution.split("\n").slice(0, 15).join("\n")));
10113
10463
  if (constitution.split("\n").length > 15) {
10114
- console.log(chalk17.gray(` ... (${constitution.split("\n").length} lines total)`));
10464
+ console.log(chalk19.gray(` ... (${constitution.split("\n").length} lines total)`));
10115
10465
  }
10116
10466
  });
10117
10467
  program.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option(
@@ -10119,44 +10469,44 @@ program.command("config").description(`Set default configuration for this projec
10119
10469
  "Default code generation mode (claude-code|api|plan)"
10120
10470
  ).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
10121
10471
  const currentDir = process.cwd();
10122
- const configPath = path21.join(currentDir, CONFIG_FILE);
10472
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10123
10473
  if (opts.clearKeys) {
10124
10474
  await clearAllKeys();
10125
- console.log(chalk17.green(`\u2714 All saved API keys cleared.`));
10475
+ console.log(chalk19.green(`\u2714 All saved API keys cleared.`));
10126
10476
  return;
10127
10477
  }
10128
10478
  if (opts.clearKey) {
10129
10479
  await clearKey(opts.clearKey);
10130
- console.log(chalk17.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
10480
+ console.log(chalk19.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
10131
10481
  return;
10132
10482
  }
10133
10483
  if (opts.listKeys) {
10134
- const store = await fs22.readJson(KEY_STORE_FILE).catch(() => ({}));
10484
+ const store = await fs24.readJson(KEY_STORE_FILE).catch(() => ({}));
10135
10485
  const providers = Object.keys(store);
10136
10486
  if (providers.length === 0) {
10137
- console.log(chalk17.gray("No saved API keys."));
10487
+ console.log(chalk19.gray("No saved API keys."));
10138
10488
  } else {
10139
- console.log(chalk17.bold("Saved API keys:"));
10489
+ console.log(chalk19.bold("Saved API keys:"));
10140
10490
  for (const p of providers) {
10141
10491
  const k2 = store[p];
10142
- console.log(chalk17.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
10492
+ console.log(chalk19.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
10143
10493
  }
10144
- console.log(chalk17.gray(`
10494
+ console.log(chalk19.gray(`
10145
10495
  File: ${KEY_STORE_FILE}`));
10146
10496
  }
10147
10497
  return;
10148
10498
  }
10149
10499
  if (opts.reset) {
10150
- await fs22.writeJson(configPath, {}, { spaces: 2 });
10151
- console.log(chalk17.green(`\u2714 Config reset: ${configPath}`));
10500
+ await fs24.writeJson(configPath, {}, { spaces: 2 });
10501
+ console.log(chalk19.green(`\u2714 Config reset: ${configPath}`));
10152
10502
  return;
10153
10503
  }
10154
10504
  const existing = await loadConfig(currentDir);
10155
10505
  if (opts.show) {
10156
10506
  if (Object.keys(existing).length === 0) {
10157
- console.log(chalk17.gray("No config file found. Using built-in defaults."));
10507
+ console.log(chalk19.gray("No config file found. Using built-in defaults."));
10158
10508
  } else {
10159
- console.log(chalk17.bold(`${configPath}:`));
10509
+ console.log(chalk19.bold(`${configPath}:`));
10160
10510
  console.log(JSON.stringify(existing, null, 2));
10161
10511
  }
10162
10512
  return;
@@ -10170,27 +10520,27 @@ File: ${KEY_STORE_FILE}`));
10170
10520
  if (opts.minSpecScore !== void 0) {
10171
10521
  const score = parseInt(opts.minSpecScore, 10);
10172
10522
  if (isNaN(score) || score < 0 || score > 10) {
10173
- console.error(chalk17.red(" --min-spec-score must be a number between 0 and 10"));
10523
+ console.error(chalk19.red(" --min-spec-score must be a number between 0 and 10"));
10174
10524
  process.exit(1);
10175
10525
  }
10176
10526
  updated.minSpecScore = score;
10177
10527
  }
10178
- await fs22.writeJson(configPath, updated, { spaces: 2 });
10179
- console.log(chalk17.green(`\u2714 Config saved to ${configPath}`));
10528
+ await fs24.writeJson(configPath, updated, { spaces: 2 });
10529
+ console.log(chalk19.green(`\u2714 Config saved to ${configPath}`));
10180
10530
  console.log(JSON.stringify(updated, null, 2));
10181
10531
  });
10182
10532
  program.command("model").description("Interactively switch the active AI provider/model and save to .ai-spec.json").option("--list", "List all available providers and models").action(async (opts) => {
10183
10533
  const currentDir = process.cwd();
10184
- const configPath = path21.join(currentDir, CONFIG_FILE);
10534
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10185
10535
  if (opts.list) {
10186
- console.log(chalk17.bold("\nAvailable providers & models:\n"));
10536
+ console.log(chalk19.bold("\nAvailable providers & models:\n"));
10187
10537
  for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
10188
10538
  console.log(
10189
- ` ${chalk17.bold.cyan(key.padEnd(10))} ${chalk17.white(meta.displayName)}`
10539
+ ` ${chalk19.bold.cyan(key.padEnd(10))} ${chalk19.white(meta.displayName)}`
10190
10540
  );
10191
- console.log(chalk17.gray(` ${meta.description}`));
10541
+ console.log(chalk19.gray(` ${meta.description}`));
10192
10542
  console.log(
10193
- chalk17.gray(
10543
+ chalk19.gray(
10194
10544
  ` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
10195
10545
  )
10196
10546
  );
@@ -10199,10 +10549,10 @@ program.command("model").description("Interactively switch the active AI provide
10199
10549
  return;
10200
10550
  }
10201
10551
  const existing = await loadConfig(currentDir);
10202
- console.log(chalk17.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10552
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10203
10553
  if (Object.keys(existing).length > 0) {
10204
10554
  console.log(
10205
- chalk17.gray(
10555
+ chalk19.gray(
10206
10556
  ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
10207
10557
  )
10208
10558
  );
@@ -10220,7 +10570,7 @@ program.command("model").description("Interactively switch the active AI provide
10220
10570
  const providerKey = await select3({
10221
10571
  message: `${label} \u2014 select provider:`,
10222
10572
  choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
10223
- name: `${meta2.displayName.padEnd(22)} ${chalk17.gray(meta2.description)}`,
10573
+ name: `${meta2.displayName.padEnd(22)} ${chalk19.gray(meta2.description)}`,
10224
10574
  value: key,
10225
10575
  short: meta2.displayName
10226
10576
  }))
@@ -10228,7 +10578,7 @@ program.command("model").description("Interactively switch the active AI provide
10228
10578
  const meta = PROVIDER_CATALOG[providerKey];
10229
10579
  const modelChoices = [
10230
10580
  ...meta.models.map((m) => ({ name: m, value: m })),
10231
- { name: chalk17.italic("\u270E Enter custom model name..."), value: "__custom__" }
10581
+ { name: chalk19.italic("\u270E Enter custom model name..."), value: "__custom__" }
10232
10582
  ];
10233
10583
  let chosenModel = await select3({
10234
10584
  message: `${label} \u2014 select model (${meta.displayName}):`,
@@ -10262,37 +10612,37 @@ program.command("model").description("Interactively switch the active AI provide
10262
10612
  if (!updated.codegen || updated.codegen === "claude-code") {
10263
10613
  updated.codegen = "api";
10264
10614
  console.log(
10265
- chalk17.yellow(
10615
+ chalk19.yellow(
10266
10616
  `
10267
10617
  \u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
10268
10618
  )
10269
10619
  );
10270
- console.log(chalk17.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
10620
+ console.log(chalk19.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
10271
10621
  }
10272
10622
  }
10273
10623
  }
10274
- console.log(chalk17.blue("\n Preview:"));
10275
- console.log(chalk17.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
10624
+ console.log(chalk19.blue("\n Preview:"));
10625
+ console.log(chalk19.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
10276
10626
  if (updated.codegenProvider) {
10277
10627
  console.log(
10278
- chalk17.gray(
10628
+ chalk19.gray(
10279
10629
  ` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
10280
10630
  )
10281
10631
  );
10282
10632
  }
10283
10633
  const ok = await confirm2({ message: "Save to .ai-spec.json?", default: true });
10284
10634
  if (!ok) {
10285
- console.log(chalk17.gray(" Cancelled."));
10635
+ console.log(chalk19.gray(" Cancelled."));
10286
10636
  return;
10287
10637
  }
10288
- await fs22.writeJson(configPath, updated, { spaces: 2 });
10289
- console.log(chalk17.green(`
10638
+ await fs24.writeJson(configPath, updated, { spaces: 2 });
10639
+ console.log(chalk19.green(`
10290
10640
  \u2714 Saved to ${configPath}`));
10291
10641
  const providerToCheck = updated.provider ?? "gemini";
10292
10642
  const envKey = ENV_KEY_MAP[providerToCheck];
10293
10643
  if (envKey && !process.env[envKey]) {
10294
10644
  console.log(
10295
- chalk17.yellow(
10645
+ chalk19.yellow(
10296
10646
  ` \u26A0 Remember to set ${envKey} in your environment or .env file.`
10297
10647
  )
10298
10648
  );
@@ -10311,29 +10661,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10311
10661
  cliOpts,
10312
10662
  contractContextSection
10313
10663
  } = opts;
10314
- console.log(chalk17.blue(`
10664
+ console.log(chalk19.blue(`
10315
10665
  [${repoName}] Loading project context...`));
10316
10666
  const loader = new ContextLoader(repoAbsPath);
10317
10667
  let context = await loader.loadProjectContext();
10318
10668
  const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
10319
- console.log(chalk17.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10320
- console.log(chalk17.gray(` Dependencies: ${context.dependencies.length} packages`));
10669
+ console.log(chalk19.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10670
+ console.log(chalk19.gray(` Dependencies: ${context.dependencies.length} packages`));
10321
10671
  if (context.constitution && context.constitution.length > 6e3) {
10322
- console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10672
+ console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10323
10673
  }
10324
10674
  if (!context.constitution) {
10325
- console.log(chalk17.yellow(` Constitution: not found \u2014 auto-generating...`));
10675
+ console.log(chalk19.yellow(` Constitution: not found \u2014 auto-generating...`));
10326
10676
  try {
10327
10677
  const constitutionGen = new ConstitutionGenerator(specProvider);
10328
10678
  const constitutionContent = await constitutionGen.generate(repoAbsPath);
10329
10679
  await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
10330
10680
  context.constitution = constitutionContent;
10331
- console.log(chalk17.green(` Constitution: generated`));
10681
+ console.log(chalk19.green(` Constitution: generated`));
10332
10682
  } catch (err) {
10333
- console.log(chalk17.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
10683
+ console.log(chalk19.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
10334
10684
  }
10335
10685
  } else {
10336
- console.log(chalk17.green(` Constitution: found`));
10686
+ console.log(chalk19.green(` Constitution: found`));
10337
10687
  }
10338
10688
  let fullIdea = idea;
10339
10689
  if (contractContextSection) {
@@ -10341,58 +10691,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10341
10691
 
10342
10692
  ${contractContextSection}`;
10343
10693
  }
10344
- console.log(chalk17.blue(` [${repoName}] Generating spec...`));
10694
+ console.log(chalk19.blue(` [${repoName}] Generating spec...`));
10345
10695
  let finalSpec;
10346
10696
  try {
10347
10697
  const result = await generateSpecWithTasks(specProvider, fullIdea, context);
10348
10698
  finalSpec = result.spec;
10349
- console.log(chalk17.green(` Spec generated.`));
10699
+ console.log(chalk19.green(` Spec generated.`));
10350
10700
  } catch (err) {
10351
- console.error(chalk17.red(` Spec generation failed: ${err.message}`));
10701
+ console.error(chalk19.red(` Spec generation failed: ${err.message}`));
10352
10702
  return { dsl: null, specFile: null };
10353
10703
  }
10354
10704
  let extractedDsl = null;
10355
10705
  if (!cliOpts.skipDsl) {
10356
- console.log(chalk17.blue(` [${repoName}] Extracting DSL...`));
10706
+ console.log(chalk19.blue(` [${repoName}] Extracting DSL...`));
10357
10707
  try {
10358
10708
  const dslExtractor = new DslExtractor(specProvider);
10359
10709
  const repoIsFrontend = isFrontendDeps(context.dependencies);
10360
10710
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
10361
10711
  if (extractedDsl) {
10362
- console.log(chalk17.green(` DSL extracted.`));
10712
+ console.log(chalk19.green(` DSL extracted.`));
10363
10713
  }
10364
10714
  } catch (err) {
10365
- console.log(chalk17.yellow(` DSL extraction failed: ${err.message}`));
10715
+ console.log(chalk19.yellow(` DSL extraction failed: ${err.message}`));
10366
10716
  }
10367
10717
  }
10368
10718
  const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
10369
10719
  const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
10370
10720
  let workingDir = repoAbsPath;
10371
10721
  if (!skipWorktreeForRepo) {
10372
- console.log(chalk17.blue(` [${repoName}] Setting up git worktree...`));
10722
+ console.log(chalk19.blue(` [${repoName}] Setting up git worktree...`));
10373
10723
  try {
10374
10724
  const worktreeManager = new GitWorktreeManager(repoAbsPath);
10375
10725
  const worktreePath = await worktreeManager.createWorktree(idea);
10376
10726
  if (worktreePath) workingDir = worktreePath;
10377
10727
  } catch (err) {
10378
- console.log(chalk17.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
10728
+ console.log(chalk19.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
10379
10729
  }
10380
10730
  } else {
10381
- console.log(chalk17.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
10731
+ console.log(chalk19.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
10382
10732
  }
10383
- const specsDir = path21.join(workingDir, "specs");
10384
- await fs22.ensureDir(specsDir);
10733
+ const specsDir = path23.join(workingDir, "specs");
10734
+ await fs24.ensureDir(specsDir);
10385
10735
  const featureSlug = slugify(idea);
10386
10736
  const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
10387
- await fs22.writeFile(specFile, finalSpec, "utf-8");
10388
- console.log(chalk17.green(` Spec saved: ${path21.relative(repoAbsPath, specFile)}`));
10737
+ await fs24.writeFile(specFile, finalSpec, "utf-8");
10738
+ console.log(chalk19.green(` Spec saved: ${path23.relative(repoAbsPath, specFile)}`));
10389
10739
  let savedDslFile = null;
10390
10740
  if (extractedDsl) {
10391
10741
  const dslExtractorForSave = new DslExtractor(specProvider);
10392
10742
  savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
10393
- console.log(chalk17.green(` DSL saved: ${path21.relative(repoAbsPath, savedDslFile)}`));
10743
+ console.log(chalk19.green(` DSL saved: ${path23.relative(repoAbsPath, savedDslFile)}`));
10394
10744
  }
10395
- console.log(chalk17.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
10745
+ console.log(chalk19.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
10396
10746
  try {
10397
10747
  const codegen = new CodeGenerator(codegenProvider, codegenMode);
10398
10748
  await codegen.generateCode(specFile, workingDir, context, {
@@ -10400,36 +10750,43 @@ ${contractContextSection}`;
10400
10750
  dslFilePath: savedDslFile ?? void 0,
10401
10751
  repoType: detectedRepoType
10402
10752
  });
10403
- console.log(chalk17.green(` Code generation complete.`));
10753
+ console.log(chalk19.green(` Code generation complete.`));
10404
10754
  } catch (err) {
10405
- console.log(chalk17.yellow(` Code generation failed: ${err.message}`));
10755
+ console.log(chalk19.yellow(` Code generation failed: ${err.message}`));
10406
10756
  }
10407
10757
  if (!cliOpts.skipTests && extractedDsl) {
10408
- console.log(chalk17.blue(` [${repoName}] Generating test skeletons...`));
10758
+ console.log(chalk19.blue(` [${repoName}] Generating test skeletons...`));
10409
10759
  try {
10410
10760
  const testGen = new TestGenerator(codegenProvider);
10411
10761
  const testFiles = await testGen.generate(extractedDsl, workingDir);
10412
- console.log(chalk17.green(` ${testFiles.length} test file(s) generated.`));
10762
+ console.log(chalk19.green(` ${testFiles.length} test file(s) generated.`));
10413
10763
  } catch (err) {
10414
- console.log(chalk17.yellow(` Test generation failed: ${err.message}`));
10764
+ console.log(chalk19.yellow(` Test generation failed: ${err.message}`));
10415
10765
  }
10416
10766
  }
10417
10767
  if (!cliOpts.skipErrorFeedback) {
10418
10768
  try {
10419
10769
  await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
10420
10770
  } catch (err) {
10421
- console.log(chalk17.yellow(` Error feedback failed: ${err.message}`));
10771
+ console.log(chalk19.yellow(` Error feedback failed: ${err.message}`));
10422
10772
  }
10423
10773
  }
10424
10774
  if (!cliOpts.skipReview) {
10425
- console.log(chalk17.blue(` [${repoName}] Running code review...`));
10775
+ console.log(chalk19.blue(` [${repoName}] Running code review...`));
10426
10776
  try {
10427
10777
  const reviewer = new CodeReviewer(specProvider);
10428
- const reviewResult = await reviewer.reviewCode(finalSpec);
10778
+ const originalDir = process.cwd();
10779
+ let reviewResult;
10780
+ try {
10781
+ process.chdir(workingDir);
10782
+ reviewResult = await reviewer.reviewCode(finalSpec);
10783
+ } finally {
10784
+ process.chdir(originalDir);
10785
+ }
10429
10786
  await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
10430
- console.log(chalk17.green(` Code review complete.`));
10787
+ console.log(chalk19.green(` Code review complete.`));
10431
10788
  } catch (err) {
10432
- console.log(chalk17.yellow(` Code review failed: ${err.message}`));
10789
+ console.log(chalk19.yellow(` Code review failed: ${err.message}`));
10433
10790
  }
10434
10791
  }
10435
10792
  return { dsl: extractedDsl, specFile };
@@ -10452,7 +10809,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10452
10809
  codegenModel: codegenModelName
10453
10810
  });
10454
10811
  const workspaceLoader = new WorkspaceLoader(currentDir);
10455
- console.log(chalk17.blue("\n[W1] Loading per-repo contexts..."));
10812
+ console.log(chalk19.blue("\n[W1] Loading per-repo contexts..."));
10456
10813
  const contexts = /* @__PURE__ */ new Map();
10457
10814
  const frontendContexts = /* @__PURE__ */ new Map();
10458
10815
  for (const repo of workspace.repos) {
@@ -10464,27 +10821,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10464
10821
  if (repo.role === "frontend" || repo.role === "mobile") {
10465
10822
  const fctx = await loadFrontendContext(repoAbsPath);
10466
10823
  frontendContexts.set(repo.name, fctx);
10467
- console.log(chalk17.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
10824
+ console.log(chalk19.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
10468
10825
  } else {
10469
- console.log(chalk17.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
10826
+ console.log(chalk19.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
10470
10827
  }
10471
10828
  } catch (err) {
10472
- console.log(chalk17.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
10829
+ console.log(chalk19.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
10473
10830
  }
10474
10831
  }
10475
- console.log(chalk17.blue("\n[W2] Decomposing requirement across repos..."));
10832
+ console.log(chalk19.blue("\n[W2] Decomposing requirement across repos..."));
10476
10833
  const decomposer = new RequirementDecomposer(specProvider);
10477
10834
  let decomposition;
10478
10835
  try {
10479
10836
  decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
10480
- console.log(chalk17.green(` Summary: ${decomposition.summary}`));
10481
- console.log(chalk17.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
10837
+ console.log(chalk19.green(` Summary: ${decomposition.summary}`));
10838
+ console.log(chalk19.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
10482
10839
  if (decomposition.coordinationNotes) {
10483
- console.log(chalk17.gray(` Coordination: ${decomposition.coordinationNotes}`));
10840
+ console.log(chalk19.gray(` Coordination: ${decomposition.coordinationNotes}`));
10484
10841
  }
10485
10842
  } catch (err) {
10486
- console.error(chalk17.red(` Decomposition failed: ${err.message}`));
10487
- console.log(chalk17.yellow(" Falling back to running all repos independently."));
10843
+ console.error(chalk19.red(` Decomposition failed: ${err.message}`));
10844
+ console.log(chalk19.yellow(" Falling back to running all repos independently."));
10488
10845
  decomposition = {
10489
10846
  originalRequirement: idea,
10490
10847
  summary: idea,
@@ -10500,11 +10857,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10500
10857
  };
10501
10858
  }
10502
10859
  if (!opts.auto) {
10503
- console.log(chalk17.cyan("\n[W3] Decomposition Preview:"));
10504
- console.log(chalk17.cyan("\u2500".repeat(52)));
10860
+ console.log(chalk19.cyan("\n[W3] Decomposition Preview:"));
10861
+ console.log(chalk19.cyan("\u2500".repeat(52)));
10505
10862
  for (const r of decomposition.repos) {
10506
- console.log(chalk17.bold(` ${r.repoName} (${r.role})`));
10507
- console.log(chalk17.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
10863
+ console.log(chalk19.bold(` ${r.repoName} (${r.role})`));
10864
+ console.log(chalk19.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
10508
10865
  if (r.uxDecisions) {
10509
10866
  const ux = r.uxDecisions;
10510
10867
  const uxSummary = [
@@ -10513,13 +10870,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10513
10870
  ux.optimisticUpdate ? "optimistic-update" : "",
10514
10871
  ux.errorRollback ? "rollback" : ""
10515
10872
  ].filter(Boolean).join(", ");
10516
- if (uxSummary) console.log(chalk17.cyan(` UX: ${uxSummary}`));
10873
+ if (uxSummary) console.log(chalk19.cyan(` UX: ${uxSummary}`));
10517
10874
  }
10518
10875
  if (r.dependsOnRepos.length > 0) {
10519
- console.log(chalk17.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
10876
+ console.log(chalk19.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
10520
10877
  }
10521
10878
  }
10522
- console.log(chalk17.cyan("\u2500".repeat(52)));
10879
+ console.log(chalk19.cyan("\u2500".repeat(52)));
10523
10880
  const gate = await select3({
10524
10881
  message: "Proceed with multi-repo pipeline?",
10525
10882
  choices: [
@@ -10528,24 +10885,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10528
10885
  ]
10529
10886
  });
10530
10887
  if (gate === "abort") {
10531
- console.log(chalk17.yellow(" Aborted."));
10888
+ console.log(chalk19.yellow(" Aborted."));
10532
10889
  process.exit(0);
10533
10890
  }
10534
10891
  }
10535
10892
  const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
10536
10893
  const contractDsls = /* @__PURE__ */ new Map();
10537
- console.log(chalk17.blue(`
10894
+ console.log(chalk19.blue(`
10538
10895
  [W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
10539
10896
  const results = [];
10540
10897
  for (const repoReq of sortedRepoRequirements) {
10541
10898
  const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
10542
10899
  if (!repoConfig) {
10543
- console.log(chalk17.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
10900
+ console.log(chalk19.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
10544
10901
  results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
10545
10902
  continue;
10546
10903
  }
10547
10904
  const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
10548
- console.log(chalk17.bold.blue(`
10905
+ console.log(chalk19.bold.blue(`
10549
10906
  \u2500\u2500 ${repoReq.repoName} (${repoReq.role}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
10550
10907
  let contractContextSection;
10551
10908
  if (repoReq.dependsOnRepos.length > 0) {
@@ -10553,7 +10910,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10553
10910
  for (const depName of repoReq.dependsOnRepos) {
10554
10911
  const depDsl = contractDsls.get(depName);
10555
10912
  if (depDsl) {
10556
- console.log(chalk17.gray(` Using API contract from: ${depName}`));
10913
+ console.log(chalk19.gray(` Using API contract from: ${depName}`));
10557
10914
  const contract = buildFrontendApiContract(depDsl);
10558
10915
  contractParts.push(buildContractContextSection(contract));
10559
10916
  }
@@ -10573,7 +10930,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10573
10930
  frontendContext: frontendCtx
10574
10931
  });
10575
10932
  contractContextSection = void 0;
10576
- console.log(chalk17.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
10933
+ console.log(chalk19.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
10577
10934
  }
10578
10935
  try {
10579
10936
  const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
@@ -10590,22 +10947,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10590
10947
  });
10591
10948
  if (repoReq.isContractProvider && dsl) {
10592
10949
  contractDsls.set(repoReq.repoName, dsl);
10593
- console.log(chalk17.green(` Contract stored for downstream repos.`));
10950
+ console.log(chalk19.green(` Contract stored for downstream repos.`));
10594
10951
  }
10595
10952
  results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
10596
- console.log(chalk17.green(` \u2714 ${repoReq.repoName} complete`));
10953
+ console.log(chalk19.green(` \u2714 ${repoReq.repoName} complete`));
10597
10954
  } catch (err) {
10598
- console.error(chalk17.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
10955
+ console.error(chalk19.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
10599
10956
  results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
10600
10957
  }
10601
10958
  }
10602
- console.log(chalk17.bold.green("\n\u2714 Multi-repo pipeline complete!"));
10603
- console.log(chalk17.gray(` Workspace: ${workspace.name}`));
10604
- console.log(chalk17.gray(` Requirement: ${idea}`));
10959
+ console.log(chalk19.bold.green("\n\u2714 Multi-repo pipeline complete!"));
10960
+ console.log(chalk19.gray(` Workspace: ${workspace.name}`));
10961
+ console.log(chalk19.gray(` Requirement: ${idea}`));
10605
10962
  console.log();
10606
10963
  for (const r of results) {
10607
- const icon = r.status === "success" ? chalk17.green("\u2714") : r.status === "failed" ? chalk17.red("\u2718") : chalk17.gray("\u2212");
10608
- const specInfo = r.specFile ? chalk17.gray(` \u2192 ${r.specFile}`) : "";
10964
+ const icon = r.status === "success" ? chalk19.green("\u2714") : r.status === "failed" ? chalk19.red("\u2718") : chalk19.gray("\u2212");
10965
+ const specInfo = r.specFile ? chalk19.gray(` \u2192 ${r.specFile}`) : "";
10609
10966
  console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
10610
10967
  }
10611
10968
  return results;
@@ -10613,18 +10970,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10613
10970
  var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
10614
10971
  workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
10615
10972
  const currentDir = process.cwd();
10616
- const configPath = path21.join(currentDir, WORKSPACE_CONFIG_FILE);
10617
- if (await fs22.pathExists(configPath)) {
10973
+ const configPath = path23.join(currentDir, WORKSPACE_CONFIG_FILE);
10974
+ if (await fs24.pathExists(configPath)) {
10618
10975
  const overwrite = await confirm2({
10619
10976
  message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
10620
10977
  default: false
10621
10978
  });
10622
10979
  if (!overwrite) {
10623
- console.log(chalk17.gray(" Cancelled."));
10980
+ console.log(chalk19.gray(" Cancelled."));
10624
10981
  return;
10625
10982
  }
10626
10983
  }
10627
- console.log(chalk17.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10984
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10628
10985
  const workspaceName = await input({
10629
10986
  message: "Workspace name:",
10630
10987
  validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
@@ -10638,11 +10995,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
10638
10995
  const workspaceLoader = new WorkspaceLoader(currentDir);
10639
10996
  const detected = await workspaceLoader.autoDetect();
10640
10997
  if (detected.length === 0) {
10641
- console.log(chalk17.yellow(" No recognizable repos found in sibling directories."));
10998
+ console.log(chalk19.yellow(" No recognizable repos found in sibling directories."));
10642
10999
  } else {
10643
- console.log(chalk17.cyan("\n Detected repos:"));
11000
+ console.log(chalk19.cyan("\n Detected repos:"));
10644
11001
  for (const r of detected) {
10645
- console.log(chalk17.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11002
+ console.log(chalk19.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
10646
11003
  }
10647
11004
  const keepAll = await confirm2({
10648
11005
  message: `Include all ${detected.length} detected repo(s)?`,
@@ -10659,7 +11016,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
10659
11016
  if (keep) repos.push(r);
10660
11017
  }
10661
11018
  }
10662
- console.log(chalk17.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
11019
+ console.log(chalk19.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
10663
11020
  }
10664
11021
  }
10665
11022
  const repoTypeChoices = [
@@ -10681,7 +11038,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
10681
11038
  default: repos.length === 0
10682
11039
  });
10683
11040
  while (addMore) {
10684
- console.log(chalk17.cyan(`
11041
+ console.log(chalk19.cyan(`
10685
11042
  Adding repo #${repos.length + 1}`));
10686
11043
  const repoName = await input({
10687
11044
  message: "Repo name (e.g. api, web, app):",
@@ -10695,16 +11052,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
10695
11052
  message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
10696
11053
  default: `./${repoName}`
10697
11054
  });
10698
- const absPath = path21.resolve(currentDir, repoPath);
11055
+ const absPath = path23.resolve(currentDir, repoPath);
10699
11056
  let detectedType = "unknown";
10700
11057
  let detectedRole = "shared";
10701
- if (await fs22.pathExists(absPath)) {
11058
+ if (await fs24.pathExists(absPath)) {
10702
11059
  const { type, role } = await detectRepoType(absPath);
10703
11060
  detectedType = type;
10704
11061
  detectedRole = role;
10705
- console.log(chalk17.gray(` Auto-detected: type=${type}, role=${role}`));
11062
+ console.log(chalk19.gray(` Auto-detected: type=${type}, role=${role}`));
10706
11063
  } else {
10707
- console.log(chalk17.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
11064
+ console.log(chalk19.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
10708
11065
  }
10709
11066
  const repoType = await select3({
10710
11067
  message: `Repo type for "${repoName}":`,
@@ -10727,53 +11084,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
10727
11084
  type: repoType,
10728
11085
  role: repoRole
10729
11086
  });
10730
- console.log(chalk17.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
11087
+ console.log(chalk19.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
10731
11088
  addMore = await confirm2({
10732
11089
  message: "Add another repo?",
10733
11090
  default: false
10734
11091
  });
10735
11092
  }
10736
11093
  const workspaceConfig = { name: workspaceName, repos };
10737
- console.log(chalk17.cyan("\n Workspace summary:"));
10738
- console.log(chalk17.gray(` Name: ${workspaceName}`));
11094
+ console.log(chalk19.cyan("\n Workspace summary:"));
11095
+ console.log(chalk19.gray(` Name: ${workspaceName}`));
10739
11096
  for (const r of repos) {
10740
- console.log(chalk17.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11097
+ console.log(chalk19.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
10741
11098
  }
10742
11099
  const ok = await confirm2({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
10743
11100
  if (!ok) {
10744
- console.log(chalk17.gray(" Cancelled."));
11101
+ console.log(chalk19.gray(" Cancelled."));
10745
11102
  return;
10746
11103
  }
10747
11104
  const loader = new WorkspaceLoader(currentDir);
10748
11105
  const saved = await loader.save(workspaceConfig);
10749
- console.log(chalk17.green(`
11106
+ console.log(chalk19.green(`
10750
11107
  \u2714 Workspace saved: ${saved}`));
10751
- console.log(chalk17.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
11108
+ console.log(chalk19.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
10752
11109
  });
10753
11110
  workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
10754
11111
  const currentDir = process.cwd();
10755
11112
  const loader = new WorkspaceLoader(currentDir);
10756
11113
  const config2 = await loader.load();
10757
11114
  if (!config2) {
10758
- console.log(chalk17.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
10759
- console.log(chalk17.gray(" Run `ai-spec workspace init` to create one."));
11115
+ console.log(chalk19.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
11116
+ console.log(chalk19.gray(" Run `ai-spec workspace init` to create one."));
10760
11117
  return;
10761
11118
  }
10762
- console.log(chalk17.bold(`
11119
+ console.log(chalk19.bold(`
10763
11120
  Workspace: ${config2.name}`));
10764
- console.log(chalk17.gray(` Config: ${path21.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
10765
- console.log(chalk17.gray(` Repos (${config2.repos.length}):
11121
+ console.log(chalk19.gray(` Config: ${path23.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
11122
+ console.log(chalk19.gray(` Repos (${config2.repos.length}):
10766
11123
  `));
10767
11124
  for (const repo of config2.repos) {
10768
11125
  const absPath = loader.resolveAbsPath(repo);
10769
- const exists = await fs22.pathExists(absPath);
10770
- const status = exists ? chalk17.green("found") : chalk17.red("not found");
11126
+ const exists = await fs24.pathExists(absPath);
11127
+ const status = exists ? chalk19.green("found") : chalk19.red("not found");
10771
11128
  console.log(
10772
- ` ${chalk17.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
11129
+ ` ${chalk19.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
10773
11130
  );
10774
- console.log(chalk17.gray(` path: ${absPath}`));
11131
+ console.log(chalk19.gray(` path: ${absPath}`));
10775
11132
  if (repo.constitution) {
10776
- console.log(chalk17.green(` constitution: found`));
11133
+ console.log(chalk19.green(` constitution: found`));
10777
11134
  }
10778
11135
  }
10779
11136
  });
@@ -10790,24 +11147,24 @@ program.command("update").description("Update an existing spec with a change req
10790
11147
  const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
10791
11148
  const apiKey = await resolveApiKey(providerName, opts.key);
10792
11149
  const provider = createProvider(providerName, apiKey, modelName);
10793
- console.log(chalk17.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10794
- console.log(chalk17.gray(` Provider: ${providerName}/${modelName}`));
11150
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11151
+ console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
10795
11152
  let specPath = opts.spec ?? null;
10796
11153
  if (!specPath) {
10797
- const specsDir = path21.join(currentDir, "specs");
11154
+ const specsDir = path23.join(currentDir, "specs");
10798
11155
  const latest = await SpecUpdater.findLatestSpec(specsDir);
10799
11156
  if (!latest) {
10800
- console.error(chalk17.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
11157
+ console.error(chalk19.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
10801
11158
  process.exit(1);
10802
11159
  }
10803
11160
  specPath = latest.filePath;
10804
- console.log(chalk17.gray(` Using spec: ${path21.relative(currentDir, specPath)} (v${latest.version})`));
11161
+ console.log(chalk19.gray(` Using spec: ${path23.relative(currentDir, specPath)} (v${latest.version})`));
10805
11162
  }
10806
- console.log(chalk17.gray(" Loading project context..."));
11163
+ console.log(chalk19.gray(" Loading project context..."));
10807
11164
  const loader = new ContextLoader(currentDir);
10808
11165
  const context = await loader.loadProjectContext();
10809
11166
  if (context.constitution && context.constitution.length > 6e3) {
10810
- console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
11167
+ console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10811
11168
  }
10812
11169
  const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
10813
11170
  const { type: repoType } = await _detectRepoType(currentDir);
@@ -10819,19 +11176,19 @@ program.command("update").description("Update an existing spec with a change req
10819
11176
  repoType
10820
11177
  });
10821
11178
  } catch (err) {
10822
- console.error(chalk17.red(` Update failed: ${err.message}`));
11179
+ console.error(chalk19.red(` Update failed: ${err.message}`));
10823
11180
  process.exit(1);
10824
11181
  }
10825
- console.log(chalk17.green(`
10826
- \u2714 Spec updated \u2192 v${result.newVersion}: ${path21.relative(currentDir, result.newSpecPath)}`));
11182
+ console.log(chalk19.green(`
11183
+ \u2714 Spec updated \u2192 v${result.newVersion}: ${path23.relative(currentDir, result.newSpecPath)}`));
10827
11184
  if (result.newDslPath) {
10828
- console.log(chalk17.green(` \u2714 DSL updated: ${path21.relative(currentDir, result.newDslPath)}`));
11185
+ console.log(chalk19.green(` \u2714 DSL updated: ${path23.relative(currentDir, result.newDslPath)}`));
10829
11186
  }
10830
11187
  if (result.affectedFiles.length > 0) {
10831
- console.log(chalk17.cyan("\n Affected files:"));
11188
+ console.log(chalk19.cyan("\n Affected files:"));
10832
11189
  for (const f of result.affectedFiles) {
10833
- const icon = f.action === "create" ? chalk17.green("+") : chalk17.yellow("~");
10834
- console.log(` ${icon} ${f.file}: ${chalk17.gray(f.description)}`);
11190
+ const icon = f.action === "create" ? chalk19.green("+") : chalk19.yellow("~");
11191
+ console.log(` ${icon} ${f.file}: ${chalk19.gray(f.description)}`);
10835
11192
  }
10836
11193
  }
10837
11194
  if (opts.codegen && result.affectedFiles.length > 0) {
@@ -10839,9 +11196,9 @@ program.command("update").description("Update an existing spec with a change req
10839
11196
  const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
10840
11197
  const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
10841
11198
  const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
10842
- console.log(chalk17.blue("\n Regenerating affected files..."));
11199
+ console.log(chalk19.blue("\n Regenerating affected files..."));
10843
11200
  const codeGenerator = new CodeGenerator(codegenProvider, "api");
10844
- const specContent = await fs22.readFile(result.newSpecPath, "utf-8");
11201
+ const specContent = await fs24.readFile(result.newSpecPath, "utf-8");
10845
11202
  const constitutionSection = context.constitution ? `
10846
11203
  === Project Constitution (MUST follow) ===
10847
11204
  ${context.constitution}
@@ -10851,10 +11208,10 @@ ${context.constitution}
10851
11208
  ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
10852
11209
  ` : "";
10853
11210
  for (const affected of result.affectedFiles) {
10854
- const fullPath = path21.join(currentDir, affected.file);
11211
+ const fullPath = path23.join(currentDir, affected.file);
10855
11212
  let existing = "";
10856
11213
  try {
10857
- existing = await fs22.readFile(fullPath, "utf-8");
11214
+ existing = await fs24.readFile(fullPath, "utf-8");
10858
11215
  } catch {
10859
11216
  }
10860
11217
  const codePrompt = `Apply this change to the file.
@@ -10868,24 +11225,24 @@ ${specContent}
10868
11225
  ${constitutionSection}${dslSection}
10869
11226
  === ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
10870
11227
  ${existing || "Create from scratch."}`;
10871
- process.stdout.write(` ${existing ? chalk17.yellow("~") : chalk17.green("+")} ${affected.file}... `);
11228
+ process.stdout.write(` ${existing ? chalk19.yellow("~") : chalk19.green("+")} ${affected.file}... `);
10872
11229
  try {
10873
11230
  const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
10874
11231
  const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
10875
11232
  const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
10876
- await fs22.ensureDir(path21.dirname(fullPath));
10877
- await fs22.writeFile(fullPath, content, "utf-8");
10878
- console.log(chalk17.green("\u2714"));
11233
+ await fs24.ensureDir(path23.dirname(fullPath));
11234
+ await fs24.writeFile(fullPath, content, "utf-8");
11235
+ console.log(chalk19.green("\u2714"));
10879
11236
  } catch (err) {
10880
- console.log(chalk17.red(`\u2718 ${err.message}`));
11237
+ console.log(chalk19.red(`\u2718 ${err.message}`));
10881
11238
  }
10882
11239
  }
10883
11240
  }
10884
11241
  if (!opts.codegen && result.affectedFiles.length > 0) {
10885
- console.log(chalk17.blue("\n Next steps:"));
10886
- console.log(chalk17.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
10887
- console.log(chalk17.gray(` \u2022 Or update files manually based on the affected files list above`));
10888
- console.log(chalk17.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
11242
+ console.log(chalk19.blue("\n Next steps:"));
11243
+ console.log(chalk19.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
11244
+ console.log(chalk19.gray(` \u2022 Or update files manually based on the affected files list above`));
11245
+ console.log(chalk19.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
10889
11246
  }
10890
11247
  });
10891
11248
  program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (YAML or JSON)").option("--openapi", "Export as OpenAPI 3.1.0 (default behaviour)").option("--format <fmt>", "Output format: yaml | json (default: yaml)", "yaml").option("--output <path>", "Output file path (default: openapi.yaml)").option("--server <url>", "API server URL in the OpenAPI document (default: http://localhost:3000)").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").action(async (opts) => {
@@ -10894,19 +11251,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
10894
11251
  if (!dslPath) {
10895
11252
  dslPath = await findLatestDslFile(currentDir);
10896
11253
  if (!dslPath) {
10897
- console.error(chalk17.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
11254
+ console.error(chalk19.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
10898
11255
  process.exit(1);
10899
11256
  }
10900
- console.log(chalk17.gray(` Using DSL: ${path21.relative(currentDir, dslPath)}`));
11257
+ console.log(chalk19.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
10901
11258
  }
10902
11259
  let dsl;
10903
11260
  try {
10904
- dsl = await fs22.readJson(dslPath);
11261
+ dsl = await fs24.readJson(dslPath);
10905
11262
  } catch (err) {
10906
- console.error(chalk17.red(` Failed to read DSL: ${err.message}`));
11263
+ console.error(chalk19.red(` Failed to read DSL: ${err.message}`));
10907
11264
  process.exit(1);
10908
11265
  }
10909
- console.log(chalk17.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11266
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10910
11267
  const format = opts.format === "json" ? "json" : "yaml";
10911
11268
  const serverUrl = opts.server || "http://localhost:3000";
10912
11269
  try {
@@ -10915,31 +11272,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
10915
11272
  serverUrl,
10916
11273
  outputPath: opts.output
10917
11274
  });
10918
- const rel = path21.relative(currentDir, outputPath);
10919
- console.log(chalk17.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
10920
- console.log(chalk17.gray(` Feature : ${dsl.feature.title}`));
10921
- console.log(chalk17.gray(` Endpoints: ${dsl.endpoints.length}`));
10922
- console.log(chalk17.gray(` Models : ${dsl.models.length}`));
10923
- console.log(chalk17.gray(` Server : ${serverUrl}`));
10924
- console.log(chalk17.blue("\n Next steps:"));
10925
- console.log(chalk17.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
10926
- console.log(chalk17.gray(` \u2022 Use openapi-generator to generate client SDKs`));
11275
+ const rel = path23.relative(currentDir, outputPath);
11276
+ console.log(chalk19.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
11277
+ console.log(chalk19.gray(` Feature : ${dsl.feature.title}`));
11278
+ console.log(chalk19.gray(` Endpoints: ${dsl.endpoints.length}`));
11279
+ console.log(chalk19.gray(` Models : ${dsl.models.length}`));
11280
+ console.log(chalk19.gray(` Server : ${serverUrl}`));
11281
+ console.log(chalk19.blue("\n Next steps:"));
11282
+ console.log(chalk19.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
11283
+ console.log(chalk19.gray(` \u2022 Use openapi-generator to generate client SDKs`));
10927
11284
  } catch (err) {
10928
- console.error(chalk17.red(` Export failed: ${err.message}`));
11285
+ console.error(chalk19.red(` Export failed: ${err.message}`));
10929
11286
  process.exit(1);
10930
11287
  }
10931
11288
  });
10932
11289
  program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option("--workspace", "Generate mock assets for all backend repos in the workspace").option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)").option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)").option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)").action(async (opts) => {
10933
11290
  const currentDir = process.cwd();
10934
11291
  const port = parseInt(opts.port, 10) || 3001;
10935
- console.log(chalk17.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11292
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10936
11293
  if (opts.restore) {
10937
- const frontendDir = opts.frontend ? path21.resolve(opts.frontend) : currentDir;
11294
+ const frontendDir = opts.frontend ? path23.resolve(opts.frontend) : currentDir;
10938
11295
  const r = await restoreMockProxy(frontendDir);
10939
11296
  if (r.restored) {
10940
- console.log(chalk17.green(" \u2714 Proxy restored and mock server stopped."));
11297
+ console.log(chalk19.green(" \u2714 Proxy restored and mock server stopped."));
10941
11298
  } else {
10942
- console.log(chalk17.yellow(` ${r.note ?? "Nothing to restore."}`));
11299
+ console.log(chalk19.yellow(` ${r.note ?? "Nothing to restore."}`));
10943
11300
  }
10944
11301
  return;
10945
11302
  }
@@ -10947,32 +11304,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
10947
11304
  const workspaceLoader = new WorkspaceLoader(currentDir);
10948
11305
  const workspaceConfig = await workspaceLoader.load();
10949
11306
  if (!workspaceConfig) {
10950
- console.error(chalk17.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
11307
+ console.error(chalk19.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
10951
11308
  process.exit(1);
10952
11309
  }
10953
11310
  const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
10954
11311
  if (backendRepos.length === 0) {
10955
- console.log(chalk17.yellow(" No backend repos found in workspace."));
11312
+ console.log(chalk19.yellow(" No backend repos found in workspace."));
10956
11313
  return;
10957
11314
  }
10958
11315
  for (const repo of backendRepos) {
10959
11316
  const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
10960
- console.log(chalk17.cyan(`
11317
+ console.log(chalk19.cyan(`
10961
11318
  Repo: ${repo.name} (${repoAbsPath})`));
10962
11319
  const dslFile = await findLatestDslFile(repoAbsPath);
10963
11320
  if (!dslFile) {
10964
- console.log(chalk17.yellow(` No DSL file found \u2014 skipping.`));
11321
+ console.log(chalk19.yellow(` No DSL file found \u2014 skipping.`));
10965
11322
  continue;
10966
11323
  }
10967
- const dsl2 = await fs22.readJson(dslFile);
11324
+ const dsl2 = await fs24.readJson(dslFile);
10968
11325
  const result2 = await generateMockAssets(dsl2, repoAbsPath, {
10969
11326
  port,
10970
11327
  msw: opts.msw,
10971
11328
  proxy: opts.proxy
10972
11329
  });
10973
11330
  for (const f of result2.files) {
10974
- console.log(chalk17.green(` \u2714 ${f.path}`));
10975
- console.log(chalk17.gray(` ${f.description}`));
11331
+ console.log(chalk19.green(` \u2714 ${f.path}`));
11332
+ console.log(chalk19.gray(` ${f.description}`));
10976
11333
  }
10977
11334
  }
10978
11335
  return;
@@ -10982,19 +11339,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
10982
11339
  dslPath = await findLatestDslFile(currentDir);
10983
11340
  if (!dslPath) {
10984
11341
  console.error(
10985
- chalk17.red(
11342
+ chalk19.red(
10986
11343
  " No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
10987
11344
  )
10988
11345
  );
10989
11346
  process.exit(1);
10990
11347
  }
10991
- console.log(chalk17.gray(` Using DSL: ${path21.relative(currentDir, dslPath)}`));
11348
+ console.log(chalk19.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
10992
11349
  }
10993
11350
  let dsl;
10994
11351
  try {
10995
- dsl = await fs22.readJson(dslPath);
11352
+ dsl = await fs24.readJson(dslPath);
10996
11353
  } catch (err) {
10997
- console.error(chalk17.red(` Failed to read DSL file: ${err.message}`));
11354
+ console.error(chalk19.red(` Failed to read DSL file: ${err.message}`));
10998
11355
  process.exit(1);
10999
11356
  }
11000
11357
  const result = await generateMockAssets(dsl, currentDir, {
@@ -11002,58 +11359,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11002
11359
  msw: opts.msw,
11003
11360
  proxy: opts.proxy
11004
11361
  });
11005
- console.log(chalk17.green(`
11362
+ console.log(chalk19.green(`
11006
11363
  \u2714 Mock assets generated (${result.files.length} file(s)):`));
11007
11364
  for (const f of result.files) {
11008
- console.log(chalk17.green(` ${f.path}`));
11009
- console.log(chalk17.gray(` ${f.description}`));
11365
+ console.log(chalk19.green(` ${f.path}`));
11366
+ console.log(chalk19.gray(` ${f.description}`));
11010
11367
  }
11011
11368
  if (opts.serve) {
11012
- const serverJsPath = path21.join(currentDir, "mock", "server.js");
11013
- if (!await fs22.pathExists(serverJsPath)) {
11014
- console.error(chalk17.red(" mock/server.js not found \u2014 generation may have failed."));
11369
+ const serverJsPath = path23.join(currentDir, "mock", "server.js");
11370
+ if (!await fs24.pathExists(serverJsPath)) {
11371
+ console.error(chalk19.red(" mock/server.js not found \u2014 generation may have failed."));
11015
11372
  process.exit(1);
11016
11373
  }
11017
11374
  const pid = startMockServerBackground(serverJsPath, port);
11018
- console.log(chalk17.green(`
11375
+ console.log(chalk19.green(`
11019
11376
  \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
11020
11377
  if (opts.frontend) {
11021
- const frontendDir = path21.resolve(opts.frontend);
11378
+ const frontendDir = path23.resolve(opts.frontend);
11022
11379
  const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
11023
11380
  await saveMockServerPid(frontendDir, pid);
11024
11381
  if (proxyResult.applied) {
11025
- console.log(chalk17.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11026
- console.log(chalk17.bold.cyan(`
11382
+ console.log(chalk19.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11383
+ console.log(chalk19.bold.cyan(`
11027
11384
  Ready! Open a new terminal and run:`));
11028
- console.log(chalk17.white(` cd ${frontendDir}`));
11029
- console.log(chalk17.white(` ${proxyResult.devCommand}`));
11030
- console.log(chalk17.gray(`
11385
+ console.log(chalk19.white(` cd ${frontendDir}`));
11386
+ console.log(chalk19.white(` ${proxyResult.devCommand}`));
11387
+ console.log(chalk19.gray(`
11031
11388
  When done: ai-spec mock --restore --frontend ${frontendDir}`));
11032
11389
  } else {
11033
- console.log(chalk17.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11034
- if (proxyResult.note) console.log(chalk17.gray(` ${proxyResult.note}`));
11390
+ console.log(chalk19.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11391
+ if (proxyResult.note) console.log(chalk19.gray(` ${proxyResult.note}`));
11035
11392
  }
11036
11393
  } else {
11037
- console.log(chalk17.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
11038
- console.log(chalk17.gray(` Mock server: http://localhost:${port}`));
11394
+ console.log(chalk19.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
11395
+ console.log(chalk19.gray(` Mock server: http://localhost:${port}`));
11039
11396
  }
11040
11397
  return;
11041
11398
  }
11042
- console.log(chalk17.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11043
- console.log(chalk17.white(` 1. Install express (if not already):`));
11044
- console.log(chalk17.gray(` npm install --save-dev express`));
11045
- console.log(chalk17.white(` 2. Start mock server:`));
11046
- console.log(chalk17.gray(` node mock/server.js`));
11047
- console.log(chalk17.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
11048
- console.log(chalk17.white(` 3. Configure your frontend to proxy API calls to:`));
11049
- console.log(chalk17.gray(` http://localhost:${port}`));
11399
+ console.log(chalk19.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11400
+ console.log(chalk19.white(` 1. Install express (if not already):`));
11401
+ console.log(chalk19.gray(` npm install --save-dev express`));
11402
+ console.log(chalk19.white(` 2. Start mock server:`));
11403
+ console.log(chalk19.gray(` node mock/server.js`));
11404
+ console.log(chalk19.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
11405
+ console.log(chalk19.white(` 3. Configure your frontend to proxy API calls to:`));
11406
+ console.log(chalk19.gray(` http://localhost:${port}`));
11050
11407
  if (opts.proxy) {
11051
- console.log(chalk17.gray(` (See the generated proxy config file for framework-specific instructions)`));
11408
+ console.log(chalk19.gray(` (See the generated proxy config file for framework-specific instructions)`));
11052
11409
  }
11053
11410
  if (opts.msw) {
11054
- console.log(chalk17.white(` 4. MSW: import and start the worker in your app entry:`));
11055
- console.log(chalk17.gray(` import { worker } from './mocks/browser';`));
11056
- console.log(chalk17.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
11411
+ console.log(chalk19.white(` 4. MSW: import and start the worker in your app entry:`));
11412
+ console.log(chalk19.gray(` import { worker } from './mocks/browser';`));
11413
+ console.log(chalk19.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
11057
11414
  }
11058
11415
  });
11059
11416
  program.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
@@ -11069,14 +11426,27 @@ program.command("learn").description("Append a lesson or engineering decision di
11069
11426
  }
11070
11427
  const result = await appendDirectLesson(currentDir, lesson.trim());
11071
11428
  if (result.appended) {
11072
- console.log(chalk17.green(`
11429
+ console.log(chalk19.green(`
11073
11430
  \u2714 Lesson appended to constitution \xA79`));
11074
- console.log(chalk17.gray(` File: .ai-spec-constitution.md`));
11431
+ console.log(chalk19.gray(` File: .ai-spec-constitution.md`));
11075
11432
  } else {
11076
- console.log(chalk17.yellow(`
11433
+ console.log(chalk19.yellow(`
11077
11434
  \u26A0 Not appended: ${result.reason}`));
11078
11435
  }
11079
11436
  });
11437
+ program.command("restore").description("Restore files modified by a previous run").argument("<runId>", "Run ID shown at the end of a create / generate run").action(async (runId) => {
11438
+ const currentDir = process.cwd();
11439
+ const snapshot = new RunSnapshot(currentDir, runId);
11440
+ console.log(chalk19.blue(`Restoring run: ${runId}...`));
11441
+ const restored = await snapshot.restore();
11442
+ if (restored.length === 0) {
11443
+ console.log(chalk19.yellow(" No backup found for this run ID."));
11444
+ } else {
11445
+ restored.forEach((f) => console.log(chalk19.green(` \u2714 restored: ${f}`)));
11446
+ console.log(chalk19.bold.green(`
11447
+ \u2714 ${restored.length} file(s) restored.`));
11448
+ }
11449
+ });
11080
11450
  if (process.argv.length <= 2) {
11081
11451
  (async () => {
11082
11452
  const currentDir = process.cwd();