ai-spec-dev 0.38.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/RELEASE_LOG.md +231 -0
  2. package/cli/commands/create.ts +9 -1176
  3. package/cli/commands/dashboard.ts +1 -1
  4. package/cli/pipeline/helpers.ts +34 -0
  5. package/cli/pipeline/multi-repo.ts +483 -0
  6. package/cli/pipeline/single-repo.ts +755 -0
  7. package/cli/utils.ts +2 -0
  8. package/core/code-generator.ts +52 -341
  9. package/core/codegen/helpers.ts +219 -0
  10. package/core/codegen/topo-sort.ts +98 -0
  11. package/core/constitution-consolidator.ts +2 -2
  12. package/core/dsl-coverage-checker.ts +298 -0
  13. package/core/dsl-extractor.ts +19 -46
  14. package/core/dsl-feedback.ts +1 -1
  15. package/core/dsl-validator.ts +74 -0
  16. package/core/error-feedback.ts +95 -11
  17. package/core/frontend-context-loader.ts +27 -5
  18. package/core/knowledge-memory.ts +52 -0
  19. package/core/mock/fixtures.ts +89 -0
  20. package/core/mock/proxy.ts +380 -0
  21. package/core/mock-server-generator.ts +12 -460
  22. package/core/requirement-decomposer.ts +4 -28
  23. package/core/reviewer.ts +1 -1
  24. package/core/safe-json.ts +76 -0
  25. package/core/spec-updater.ts +5 -21
  26. package/core/token-budget.ts +124 -0
  27. package/core/vcr.ts +20 -1
  28. package/dist/cli/index.js +4110 -3534
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/index.mjs +4237 -3661
  31. package/dist/cli/index.mjs.map +1 -1
  32. package/dist/index.d.mts +18 -16
  33. package/dist/index.d.ts +18 -16
  34. package/dist/index.js +310 -182
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +308 -180
  37. package/dist/index.mjs.map +1 -1
  38. package/package.json +2 -2
  39. package/purpose.md +173 -33
  40. package/tests/auto-consolidation.test.ts +109 -0
  41. package/tests/combined-generator.test.ts +81 -0
  42. package/tests/constitution-consolidator.test.ts +161 -0
  43. package/tests/constitution-generator.test.ts +94 -0
  44. package/tests/contract-bridge.test.ts +201 -0
  45. package/tests/design-dialogue.test.ts +108 -0
  46. package/tests/dsl-coverage-checker.test.ts +230 -0
  47. package/tests/dsl-feedback.test.ts +45 -0
  48. package/tests/dsl-validator-xref.test.ts +99 -0
  49. package/tests/error-feedback-repair.test.ts +319 -0
  50. package/tests/error-feedback-validation.test.ts +91 -0
  51. package/tests/frontend-context-loader.test.ts +609 -0
  52. package/tests/global-constitution.test.ts +110 -0
  53. package/tests/key-store.test.ts +73 -0
  54. package/tests/knowledge-memory.test.ts +327 -0
  55. package/tests/project-index.test.ts +206 -0
  56. package/tests/prompt-hasher.test.ts +19 -0
  57. package/tests/requirement-decomposer.test.ts +171 -0
  58. package/tests/reviewer.test.ts +4 -1
  59. package/tests/run-logger.test.ts +289 -0
  60. package/tests/run-snapshot.test.ts +113 -0
  61. package/tests/safe-json.test.ts +63 -0
  62. package/tests/spec-updater.test.ts +161 -0
  63. package/tests/test-generator.test.ts +146 -0
  64. package/tests/token-budget.test.ts +124 -0
  65. package/tests/vcr-hash.test.ts +101 -0
  66. package/tests/workspace-loader.test.ts +277 -0
package/dist/index.mjs CHANGED
@@ -4107,8 +4107,8 @@ ${currentSpec}`,
4107
4107
  };
4108
4108
 
4109
4109
  // core/code-generator.ts
4110
- import chalk8 from "chalk";
4111
- import { execSync, spawnSync } from "child_process";
4110
+ import chalk10 from "chalk";
4111
+ import { execSync as execSync2, spawnSync } from "child_process";
4112
4112
  import * as path6 from "path";
4113
4113
  import * as fs10 from "fs-extra";
4114
4114
 
@@ -4685,7 +4685,7 @@ async function updateTaskStatus(specFilePath, taskId, status) {
4685
4685
  }
4686
4686
 
4687
4687
  // core/dsl-extractor.ts
4688
- import chalk6 from "chalk";
4688
+ import chalk7 from "chalk";
4689
4689
  import * as fs6 from "fs-extra";
4690
4690
  import * as path4 from "path";
4691
4691
  import { select as select2 } from "@inquirer/prompts";
@@ -4774,6 +4774,7 @@ function validateDsl(raw) {
4774
4774
  }
4775
4775
  }
4776
4776
  }
4777
+ crossReferenceChecks(obj, errors);
4777
4778
  if (errors.length > 0) {
4778
4779
  return { valid: false, errors };
4779
4780
  }
@@ -5005,6 +5006,64 @@ function validateComponent(raw, path10, errors) {
5005
5006
  }
5006
5007
  }
5007
5008
  }
5009
+ function crossReferenceChecks(obj, errors) {
5010
+ const models = Array.isArray(obj["models"]) ? obj["models"] : [];
5011
+ const endpoints = Array.isArray(obj["endpoints"]) ? obj["endpoints"] : [];
5012
+ const components = Array.isArray(obj["components"]) ? obj["components"] : [];
5013
+ const seenRoutes = /* @__PURE__ */ new Map();
5014
+ for (let i = 0; i < endpoints.length; i++) {
5015
+ const ep = endpoints[i];
5016
+ if (typeof ep?.["method"] === "string" && typeof ep?.["path"] === "string") {
5017
+ const route = `${ep["method"].toUpperCase()} ${ep["path"]}`;
5018
+ if (seenRoutes.has(route)) {
5019
+ errors.push({
5020
+ path: `endpoints[${i}]`,
5021
+ message: `Duplicate route "${route}" \u2014 also defined at endpoints[${seenRoutes.get(route)}]`
5022
+ });
5023
+ } else {
5024
+ seenRoutes.set(route, i);
5025
+ }
5026
+ }
5027
+ }
5028
+ const modelNames = new Set(
5029
+ models.filter((m) => typeof m?.["name"] === "string").map((m) => m["name"])
5030
+ );
5031
+ for (let i = 0; i < models.length; i++) {
5032
+ const m = models[i];
5033
+ if (!Array.isArray(m?.["relations"])) continue;
5034
+ for (const rel of m["relations"]) {
5035
+ if (typeof rel !== "string") continue;
5036
+ const refMatch = rel.match(/(?:hasMany|hasOne|belongsTo|manyToMany)\s+(\w+)/i);
5037
+ if (refMatch) {
5038
+ const refName = refMatch[1];
5039
+ if (!modelNames.has(refName)) {
5040
+ errors.push({
5041
+ path: `models[${i}].relations`,
5042
+ message: `Relation references model "${refName}" which is not defined in models[]`
5043
+ });
5044
+ }
5045
+ }
5046
+ }
5047
+ }
5048
+ const endpointIds = new Set(
5049
+ endpoints.filter((e) => typeof e?.["id"] === "string").map((e) => e["id"])
5050
+ );
5051
+ if (endpointIds.size > 0) {
5052
+ for (let i = 0; i < components.length; i++) {
5053
+ const c = components[i];
5054
+ if (!Array.isArray(c?.["apiCalls"])) continue;
5055
+ for (const call of c["apiCalls"]) {
5056
+ if (typeof call !== "string") continue;
5057
+ if (!endpointIds.has(call)) {
5058
+ errors.push({
5059
+ path: `components[${i}].apiCalls`,
5060
+ message: `References endpoint "${call}" which is not defined in endpoints[]`
5061
+ });
5062
+ }
5063
+ }
5064
+ }
5065
+ }
5066
+ }
5008
5067
  function requireNonEmptyString(v2, path10, errors) {
5009
5068
  if (typeof v2 !== "string" || v2.trim().length === 0) {
5010
5069
  errors.push({
@@ -5019,6 +5078,26 @@ function typeLabel(v2) {
5019
5078
  return typeof v2;
5020
5079
  }
5021
5080
 
5081
+ // core/token-budget.ts
5082
+ import chalk6 from "chalk";
5083
+ var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5084
+ function estimateTokens(text) {
5085
+ if (!text) return 0;
5086
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5087
+ const nonCjkLength = text.length - cjkCount;
5088
+ return Math.ceil(cjkCount + nonCjkLength / 4);
5089
+ }
5090
+ var DEFAULT_TOKEN_BUDGETS = {
5091
+ gemini: 9e5,
5092
+ claude: 18e4,
5093
+ openai: 12e4,
5094
+ deepseek: 6e4,
5095
+ default: 1e5
5096
+ };
5097
+ function getDefaultBudget(providerName) {
5098
+ return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
5099
+ }
5100
+
5022
5101
  // core/dsl-extractor.ts
5023
5102
  function dslFilePath(specFilePath) {
5024
5103
  const dir = path4.dirname(specFilePath);
@@ -5177,7 +5256,9 @@ var ROUTING_LIBS = [
5177
5256
  ["@tanstack/react-router", "tanstack-router"],
5178
5257
  ["react-navigation", "react-navigation"],
5179
5258
  ["expo-router", "expo-router"],
5180
- ["vue-router", "vue-router"]
5259
+ ["vue-router", "vue-router"],
5260
+ ["@solidjs/router", "solid-router"],
5261
+ ["@builder.io/qwik-city", "qwik-city"]
5181
5262
  ];
5182
5263
  async function loadFrontendContext(projectRoot) {
5183
5264
  const ctx = {
@@ -5209,6 +5290,18 @@ async function loadFrontendContext(projectRoot) {
5209
5290
  const has = (name) => depKeys.includes(name);
5210
5291
  if (has("react-native") || has("expo")) {
5211
5292
  ctx.framework = "react-native";
5293
+ } else if (has("@sveltejs/kit")) {
5294
+ ctx.framework = "sveltekit";
5295
+ } else if (has("svelte")) {
5296
+ ctx.framework = "svelte";
5297
+ } else if (has("@builder.io/qwik")) {
5298
+ ctx.framework = "qwik";
5299
+ } else if (has("@remix-run/react") || has("@remix-run/node")) {
5300
+ ctx.framework = "remix";
5301
+ } else if (has("astro")) {
5302
+ ctx.framework = "astro";
5303
+ } else if (has("solid-js")) {
5304
+ ctx.framework = "solid";
5212
5305
  } else if (has("next")) {
5213
5306
  ctx.framework = "next";
5214
5307
  } else if (has("react")) {
@@ -5235,6 +5328,12 @@ async function loadFrontendContext(projectRoot) {
5235
5328
  if (ctx.framework === "next") {
5236
5329
  const hasAppDir = await fs7.pathExists(path5.join(projectRoot, "app"));
5237
5330
  ctx.routingPattern = hasAppDir ? "next-app-router" : "next-pages-router";
5331
+ } else if (ctx.framework === "sveltekit") {
5332
+ ctx.routingPattern = "sveltekit-file-router";
5333
+ } else if (ctx.framework === "remix") {
5334
+ ctx.routingPattern = "remix-file-router";
5335
+ } else if (ctx.framework === "astro") {
5336
+ ctx.routingPattern = "astro-file-router";
5238
5337
  } else {
5239
5338
  for (const [lib, label] of ROUTING_LIBS) {
5240
5339
  if (has(lib)) {
@@ -5620,13 +5719,14 @@ function getActiveSnapshot() {
5620
5719
 
5621
5720
  // core/run-logger.ts
5622
5721
  import * as fs9 from "fs-extra";
5623
- import chalk7 from "chalk";
5722
+ import chalk8 from "chalk";
5624
5723
  var _activeLogger = null;
5625
5724
  function getActiveLogger() {
5626
5725
  return _activeLogger;
5627
5726
  }
5628
5727
 
5629
- // core/code-generator.ts
5728
+ // core/codegen/helpers.ts
5729
+ import { execSync } from "child_process";
5630
5730
  function buildSharedConfigSection(context) {
5631
5731
  if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
5632
5732
  const lines = [
@@ -5758,7 +5858,7 @@ function buildGeneratedFilesSection(cache) {
5758
5858
  }
5759
5859
  function isRtkAvailable() {
5760
5860
  try {
5761
- execSync("rtk --version", { stdio: "ignore" });
5861
+ execSync("rtk --version", { stdio: "ignore", timeout: 1e4 });
5762
5862
  return true;
5763
5863
  } catch {
5764
5864
  return false;
@@ -5782,6 +5882,73 @@ function parseJsonArray(text) {
5782
5882
  }
5783
5883
  return [];
5784
5884
  }
5885
+
5886
+ // core/codegen/topo-sort.ts
5887
+ import chalk9 from "chalk";
5888
+ function topoSortLayerTasks(tasks) {
5889
+ if (tasks.length <= 1) return [tasks];
5890
+ const idSet = new Set(tasks.map((t) => t.id));
5891
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
5892
+ const inDegree = /* @__PURE__ */ new Map();
5893
+ const dependents = /* @__PURE__ */ new Map();
5894
+ for (const task of tasks) {
5895
+ inDegree.set(task.id, 0);
5896
+ dependents.set(task.id, []);
5897
+ }
5898
+ for (const task of tasks) {
5899
+ const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
5900
+ inDegree.set(task.id, intraDeps.length);
5901
+ for (const dep of intraDeps) {
5902
+ dependents.get(dep).push(task.id);
5903
+ }
5904
+ }
5905
+ const batches = [];
5906
+ const remaining = new Set(tasks.map((t) => t.id));
5907
+ while (remaining.size > 0) {
5908
+ const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
5909
+ if (batch.length === 0) {
5910
+ batches.push([...remaining].map((id) => taskById.get(id)));
5911
+ break;
5912
+ }
5913
+ batches.push(batch);
5914
+ for (const task of batch) {
5915
+ remaining.delete(task.id);
5916
+ for (const dependent of dependents.get(task.id)) {
5917
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
5918
+ }
5919
+ }
5920
+ }
5921
+ return batches;
5922
+ }
5923
+ var LAYER_ICONS = {
5924
+ data: "\u{1F4BE}",
5925
+ infra: "\u2699\uFE0F ",
5926
+ service: "\u{1F527}",
5927
+ api: "\u{1F310}",
5928
+ view: "\u{1F5A5}\uFE0F ",
5929
+ route: "\u{1F5FA}\uFE0F ",
5930
+ test: "\u{1F9EA}"
5931
+ };
5932
+ function printTaskProgress(completed, total, task, mode) {
5933
+ const pct = total > 0 ? Math.round(completed / total * 100) : 0;
5934
+ const barWidth = 20;
5935
+ const filled = Math.round(pct / 100 * barWidth);
5936
+ const bar = chalk9.green("\u2588".repeat(filled)) + chalk9.gray("\u2591".repeat(barWidth - filled));
5937
+ const icon = LAYER_ICONS[task.layer] ?? " ";
5938
+ if (mode === "skip") {
5939
+ console.log(
5940
+ chalk9.gray(`
5941
+ [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
5942
+ );
5943
+ } else {
5944
+ console.log(
5945
+ chalk9.bold(`
5946
+ [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
5947
+ );
5948
+ }
5949
+ }
5950
+
5951
+ // core/code-generator.ts
5785
5952
  var CodeGenerator = class {
5786
5953
  constructor(provider, mode = "claude-code") {
5787
5954
  this.provider = provider;
@@ -5792,13 +5959,13 @@ var CodeGenerator = class {
5792
5959
  let effectiveMode = this.mode;
5793
5960
  if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
5794
5961
  console.log(
5795
- chalk8.yellow(
5962
+ chalk10.yellow(
5796
5963
  `
5797
5964
  \u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
5798
5965
  )
5799
5966
  );
5800
- 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`));
5801
- console.log(chalk8.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5967
+ console.log(chalk10.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
5968
+ console.log(chalk10.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5802
5969
  `));
5803
5970
  effectiveMode = "api";
5804
5971
  }
@@ -5816,23 +5983,23 @@ var CodeGenerator = class {
5816
5983
  // ── Mode: claude-code ──────────────────────────────────────────────────────
5817
5984
  isClaudeCLIAvailable() {
5818
5985
  try {
5819
- execSync("claude --version", { stdio: "ignore" });
5986
+ execSync2("claude --version", { stdio: "ignore", timeout: 1e4 });
5820
5987
  return true;
5821
5988
  } catch {
5822
5989
  return false;
5823
5990
  }
5824
5991
  }
5825
5992
  async runClaudeCode(specFilePath, workingDir, options = {}) {
5826
- console.log(chalk8.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5993
+ console.log(chalk10.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5827
5994
  if (!this.isClaudeCLIAvailable()) {
5828
- console.log(chalk8.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5829
- console.log(chalk8.gray(" Install: npm install -g @anthropic-ai/claude-code"));
5995
+ console.log(chalk10.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5996
+ console.log(chalk10.gray(" Install: npm install -g @anthropic-ai/claude-code"));
5830
5997
  return this.runPlanMode(specFilePath);
5831
5998
  }
5832
5999
  const rtkAvailable = isRtkAvailable();
5833
6000
  const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
5834
6001
  if (rtkAvailable) {
5835
- console.log(chalk8.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
6002
+ console.log(chalk10.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
5836
6003
  }
5837
6004
  const tasks = await loadTasksForSpec(specFilePath);
5838
6005
  if (options.auto && tasks && tasks.length > 0) {
@@ -5848,28 +6015,28 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5848
6015
  const promptFile = path6.join(workingDir, ".claude-prompt.txt");
5849
6016
  await fs10.writeFile(promptFile, promptContent, "utf-8");
5850
6017
  if (options.auto) {
5851
- console.log(chalk8.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5852
- console.log(chalk8.gray(` Spec: ${specFilePath}`));
6018
+ console.log(chalk10.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
6019
+ console.log(chalk10.gray(` Spec: ${specFilePath}`));
5853
6020
  try {
5854
6021
  spawnSync(claudeCmd, ["-p", promptContent], {
5855
6022
  cwd: workingDir,
5856
6023
  stdio: "inherit",
5857
6024
  shell: false
5858
6025
  });
5859
- console.log(chalk8.green("\n \u2714 Claude Code completed."));
6026
+ console.log(chalk10.green("\n \u2714 Claude Code completed."));
5860
6027
  } catch {
5861
- console.log(chalk8.yellow("\n Claude Code exited. Check output above."));
6028
+ console.log(chalk10.yellow("\n Claude Code exited. Check output above."));
5862
6029
  }
5863
6030
  } else {
5864
- console.log(chalk8.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
5865
- console.log(chalk8.gray(` Spec: ${specFilePath}`));
5866
- if (tasks) console.log(chalk8.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
5867
- console.log(chalk8.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
6031
+ console.log(chalk10.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
6032
+ console.log(chalk10.gray(` Spec: ${specFilePath}`));
6033
+ if (tasks) console.log(chalk10.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
6034
+ console.log(chalk10.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
5868
6035
  try {
5869
- execSync(claudeCmd, { cwd: workingDir, stdio: "inherit" });
5870
- console.log(chalk8.green("\n \u2714 Claude Code session completed."));
6036
+ execSync2(claudeCmd, { cwd: workingDir, stdio: "inherit" });
6037
+ console.log(chalk10.green("\n \u2714 Claude Code session completed."));
5871
6038
  } catch {
5872
- console.log(chalk8.yellow("\n Claude Code session ended. Continuing workflow."));
6039
+ console.log(chalk10.yellow("\n Claude Code session ended. Continuing workflow."));
5873
6040
  }
5874
6041
  }
5875
6042
  }
@@ -5882,10 +6049,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5882
6049
  const pending = tasks.filter((t) => t.status !== "done");
5883
6050
  const doneCount = tasks.length - pending.length;
5884
6051
  if (options.resume && doneCount > 0) {
5885
- console.log(chalk8.cyan(`
6052
+ console.log(chalk10.cyan(`
5886
6053
  Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
5887
6054
  } else {
5888
- console.log(chalk8.cyan(`
6055
+ console.log(chalk10.cyan(`
5889
6056
  Incremental mode: ${tasks.length} tasks`));
5890
6057
  }
5891
6058
  let completed = doneCount;
@@ -5914,33 +6081,33 @@ Implement ONLY this task. Do not implement other tasks.`;
5914
6081
  completed++;
5915
6082
  } catch {
5916
6083
  taskStatus = "failed";
5917
- console.log(chalk8.yellow(`
6084
+ console.log(chalk10.yellow(`
5918
6085
  \u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
5919
6086
  }
5920
6087
  await updateTaskStatus(specFilePath, task.id, taskStatus);
5921
6088
  }
5922
6089
  const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
5923
6090
  console.log(
5924
- chalk8.bold(
6091
+ chalk10.bold(
5925
6092
  `
5926
- ${successCount === tasks.length ? chalk8.green("\u2714") : chalk8.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
6093
+ ${successCount === tasks.length ? chalk10.green("\u2714") : chalk10.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
5927
6094
  )
5928
6095
  );
5929
6096
  }
5930
6097
  // ── Mode: api ─────────────────────────────────────────────────────────────
5931
6098
  async runApiMode(specFilePath, workingDir, context, options = {}) {
5932
6099
  console.log(
5933
- chalk8.blue(
6100
+ chalk10.blue(
5934
6101
  `
5935
6102
  \u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
5936
6103
  )
5937
6104
  );
5938
6105
  const systemPrompt = getCodeGenSystemPrompt(options.repoType);
5939
6106
  if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
5940
- console.log(chalk8.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
6107
+ console.log(chalk10.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
5941
6108
  }
5942
6109
  const spec = await fs10.readFile(specFilePath, "utf-8");
5943
- const constitutionSection = context?.constitution ? `
6110
+ let constitutionSection = context?.constitution ? `
5944
6111
  === Project Constitution (MUST follow) ===
5945
6112
  ${context.constitution}
5946
6113
  ` : "";
@@ -5955,7 +6122,7 @@ ${buildDslContextSection(dsl)}
5955
6122
  if (dsl) {
5956
6123
  const cmpCount = dsl.components?.length ?? 0;
5957
6124
  const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
5958
- console.log(chalk8.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
6125
+ console.log(chalk10.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
5959
6126
  }
5960
6127
  const isFrontend = isFrontendDeps(context?.dependencies ?? []);
5961
6128
  let frontendSection = "";
@@ -5964,13 +6131,30 @@ ${buildDslContextSection(dsl)}
5964
6131
  frontendSection = `
5965
6132
  ${buildFrontendContextSection(fctx)}
5966
6133
  `;
5967
- console.log(chalk8.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6134
+ console.log(chalk10.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6135
+ }
6136
+ const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
6137
+ const estimatedTokenCount = estimateTokens(allContextText);
6138
+ const budget = getDefaultBudget(this.provider.providerName);
6139
+ if (estimatedTokenCount > budget * 0.7) {
6140
+ console.log(
6141
+ chalk10.yellow(
6142
+ ` \u26A0 Context size: ~${Math.round(estimatedTokenCount / 1e3)}K tokens (budget: ${Math.round(budget / 1e3)}K for ${this.provider.providerName})`
6143
+ )
6144
+ );
6145
+ if (constitutionSection.length > 4e3) {
6146
+ const s9Start = constitutionSection.indexOf("## 9.");
6147
+ if (s9Start > 0) {
6148
+ constitutionSection = constitutionSection.slice(0, s9Start) + "## 9. \u79EF\u7D2F\u6559\u8BAD (Accumulated Lessons)\n[Trimmed for context budget \u2014 run `ai-spec init --consolidate` to prune]\n";
6149
+ console.log(chalk10.gray(" \u2192 \xA79 trimmed from constitution to save tokens."));
6150
+ }
6151
+ }
5968
6152
  }
5969
6153
  const tasks = await loadTasksForSpec(specFilePath);
5970
6154
  if (tasks && tasks.length > 0) {
5971
6155
  return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
5972
6156
  }
5973
- console.log(chalk8.gray(" [1/2] Planning implementation files..."));
6157
+ console.log(chalk10.gray(" [1/2] Planning implementation files..."));
5974
6158
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
5975
6159
 
5976
6160
  IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
@@ -5993,18 +6177,18 @@ Output ONLY a valid JSON array:
5993
6177
  const planResponse = await this.provider.generate(planPrompt, systemPrompt);
5994
6178
  filePlan = parseJsonArray(planResponse);
5995
6179
  } catch (err) {
5996
- console.error(chalk8.red(" Failed to generate file plan:"), err);
6180
+ console.error(chalk10.red(" Failed to generate file plan:"), err);
5997
6181
  }
5998
6182
  if (filePlan.length === 0) {
5999
- console.log(chalk8.yellow(" Could not determine file plan. Falling back to plan mode."));
6183
+ console.log(chalk10.yellow(" Could not determine file plan. Falling back to plan mode."));
6000
6184
  await this.runPlanMode(specFilePath);
6001
6185
  return [];
6002
6186
  }
6003
- console.log(chalk8.cyan(`
6187
+ console.log(chalk10.cyan(`
6004
6188
  Plan: ${filePlan.length} file(s) to process`));
6005
6189
  filePlan.forEach((item) => {
6006
- const icon = item.action === "create" ? chalk8.green("+") : chalk8.yellow("~");
6007
- console.log(` ${icon} ${item.file}: ${chalk8.gray(item.description)}`);
6190
+ const icon = item.action === "create" ? chalk10.green("+") : chalk10.yellow("~");
6191
+ console.log(` ${icon} ${item.file}: ${chalk10.gray(item.description)}`);
6008
6192
  });
6009
6193
  const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
6010
6194
  return files;
@@ -6013,13 +6197,13 @@ Output ONLY a valid JSON array:
6013
6197
  const pendingTasks = tasks.filter((t) => t.status !== "done");
6014
6198
  const doneCount = tasks.length - pendingTasks.length;
6015
6199
  if (options.resume && doneCount > 0) {
6016
- console.log(chalk8.cyan(`
6017
- Task-based generation (resume): ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, skipping)`));
6200
+ console.log(chalk10.cyan(`
6201
+ Task-based generation (resume): ${tasks.length} tasks (${chalk10.green(doneCount + " already done")}, skipping)`));
6018
6202
  } else if (doneCount > 0) {
6019
- console.log(chalk8.cyan(`
6020
- Task-based generation: ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, resuming from checkpoint)`));
6203
+ console.log(chalk10.cyan(`
6204
+ Task-based generation: ${tasks.length} tasks (${chalk10.green(doneCount + " already done")}, resuming from checkpoint)`));
6021
6205
  } else {
6022
- console.log(chalk8.cyan(`
6206
+ console.log(chalk10.cyan(`
6023
6207
  Task-based generation: ${tasks.length} tasks`));
6024
6208
  }
6025
6209
  const sharedConfigPaths = new Set(
@@ -6051,9 +6235,9 @@ Output ONLY a valid JSON array:
6051
6235
  const pct = Math.round(completedTasks / tasks.length * 100);
6052
6236
  const barWidth = 20;
6053
6237
  const filled = Math.round(pct / 100 * barWidth);
6054
- const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
6238
+ const bar = chalk10.green("\u2588".repeat(filled)) + chalk10.gray("\u2591".repeat(barWidth - filled));
6055
6239
  console.log(
6056
- chalk8.bold(`
6240
+ chalk10.bold(`
6057
6241
  [${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
6058
6242
  );
6059
6243
  } else {
@@ -6061,7 +6245,7 @@ Output ONLY a valid JSON array:
6061
6245
  }
6062
6246
  const executeTask = async (task, batchIsParallel) => {
6063
6247
  if (task.filesToTouch.length === 0) {
6064
- if (!batchIsParallel) console.log(chalk8.gray(" No files specified, skipping."));
6248
+ if (!batchIsParallel) console.log(chalk10.gray(" No files specified, skipping."));
6065
6249
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6066
6250
  }
6067
6251
  const filePlan = await Promise.all(
@@ -6118,13 +6302,19 @@ ${taskContext}`,
6118
6302
  const layerResults = [];
6119
6303
  for (const batch of taskBatches) {
6120
6304
  const batchIsParallel = batch.length > 1;
6121
- const batchResultPromises = batch.map(
6122
- (task) => executeTask(task, batchIsParallel).catch((err) => {
6123
- console.log(chalk8.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
6124
- return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6125
- })
6126
- );
6127
- const batchResults = await Promise.all(batchResultPromises);
6305
+ const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
6306
+ const settled = await Promise.allSettled(batchResultPromises);
6307
+ const batchResults = [];
6308
+ for (let i = 0; i < settled.length; i++) {
6309
+ const outcome = settled[i];
6310
+ if (outcome.status === "fulfilled") {
6311
+ batchResults.push(outcome.value);
6312
+ } else {
6313
+ const task = batch[i];
6314
+ console.log(chalk10.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
6315
+ batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
6316
+ }
6317
+ }
6128
6318
  layerResults.push(...batchResults);
6129
6319
  await updateCacheFromBatch(batchResults);
6130
6320
  }
@@ -6136,14 +6326,14 @@ ${taskContext}`,
6136
6326
  totalFiles += result.total;
6137
6327
  allGeneratedFiles.push(...result.files);
6138
6328
  if (isParallel) {
6139
- const icon = result.success === result.total ? chalk8.green("\u2714") : chalk8.yellow("!");
6329
+ const icon = result.success === result.total ? chalk10.green("\u2714") : chalk10.yellow("!");
6140
6330
  const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
6141
6331
  console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
6142
6332
  }
6143
6333
  const taskStatus = result.success === result.total ? "done" : "failed";
6144
6334
  await updateTaskStatus(specFilePath, result.task.id, taskStatus);
6145
6335
  if (taskStatus === "failed") {
6146
- console.log(chalk8.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
6336
+ console.log(chalk10.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
6147
6337
  }
6148
6338
  }
6149
6339
  completedTasks += layerTasks.length;
@@ -6158,7 +6348,7 @@ ${taskContext}`,
6158
6348
  if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
6159
6349
  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.`;
6160
6350
  }
6161
- console.log(chalk8.gray(`
6351
+ console.log(chalk10.gray(`
6162
6352
  + updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
6163
6353
  const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
6164
6354
  await this.generateFiles(
@@ -6176,17 +6366,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
6176
6366
  }
6177
6367
  }
6178
6368
  console.log(
6179
- chalk8.bold(
6369
+ chalk10.bold(
6180
6370
  `
6181
- ${totalSuccess === totalFiles ? chalk8.green("\u2714") : chalk8.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
6371
+ ${totalSuccess === totalFiles ? chalk10.green("\u2714") : chalk10.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
6182
6372
  )
6183
6373
  );
6184
6374
  return allGeneratedFiles;
6185
6375
  }
6186
6376
  async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
6187
- const prefix = taskLabel ? ` [${chalk8.cyan(taskLabel)}] ` : " ";
6377
+ const prefix = taskLabel ? ` [${chalk10.cyan(taskLabel)}] ` : " ";
6188
6378
  if (!taskLabel) {
6189
- console.log(chalk8.gray(`
6379
+ console.log(chalk10.gray(`
6190
6380
  Generating ${filePlan.length} file(s)...`));
6191
6381
  }
6192
6382
  let successCount = 0;
@@ -6214,17 +6404,17 @@ ${existingContent || "Output only the complete file content."}`;
6214
6404
  await fs10.ensureDir(path6.dirname(fullPath));
6215
6405
  await fs10.writeFile(fullPath, fileContent, "utf-8");
6216
6406
  getActiveLogger()?.fileWritten(item.file);
6217
- console.log(`${prefix}${existingContent ? chalk8.yellow("~") : chalk8.green("+")} ${chalk8.bold(item.file)} ${chalk8.green("\u2714")}`);
6407
+ console.log(`${prefix}${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)} ${chalk10.green("\u2714")}`);
6218
6408
  successCount++;
6219
6409
  writtenFiles.push(item.file);
6220
6410
  } catch (err) {
6221
- console.log(`${prefix}${chalk8.red("\u2718")} ${chalk8.bold(item.file)} \u2014 ${chalk8.red(err.message)}`);
6411
+ console.log(`${prefix}${chalk10.red("\u2718")} ${chalk10.bold(item.file)} \u2014 ${chalk10.red(err.message)}`);
6222
6412
  }
6223
6413
  }
6224
6414
  if (!taskLabel) {
6225
6415
  console.log(
6226
- chalk8.bold(
6227
- ` ${successCount === filePlan.length ? chalk8.green("\u2714") : chalk8.yellow("!")} ${successCount}/${filePlan.length} files written.`
6416
+ chalk10.bold(
6417
+ ` ${successCount === filePlan.length ? chalk10.green("\u2714") : chalk10.yellow("!")} ${successCount}/${filePlan.length} files written.`
6228
6418
  )
6229
6419
  );
6230
6420
  }
@@ -6232,7 +6422,7 @@ ${existingContent || "Output only the complete file content."}`;
6232
6422
  }
6233
6423
  // ── Mode: plan ─────────────────────────────────────────────────────────────
6234
6424
  async runPlanMode(specFilePath) {
6235
- 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"));
6425
+ console.log(chalk10.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"));
6236
6426
  const spec = await fs10.readFile(specFilePath, "utf-8");
6237
6427
  const plan = await this.provider.generate(
6238
6428
  `Create a detailed, step-by-step implementation plan for the following feature spec.
@@ -6245,80 +6435,18 @@ Be specific about:
6245
6435
  ${spec}`,
6246
6436
  "You are a senior developer creating an actionable implementation guide."
6247
6437
  );
6248
- console.log(chalk8.cyan("\n") + plan);
6438
+ console.log(chalk10.cyan("\n") + plan);
6249
6439
  }
6250
6440
  };
6251
- function topoSortLayerTasks(tasks) {
6252
- if (tasks.length <= 1) return [tasks];
6253
- const idSet = new Set(tasks.map((t) => t.id));
6254
- const taskById = new Map(tasks.map((t) => [t.id, t]));
6255
- const inDegree = /* @__PURE__ */ new Map();
6256
- const dependents = /* @__PURE__ */ new Map();
6257
- for (const task of tasks) {
6258
- inDegree.set(task.id, 0);
6259
- dependents.set(task.id, []);
6260
- }
6261
- for (const task of tasks) {
6262
- const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
6263
- inDegree.set(task.id, intraDeps.length);
6264
- for (const dep of intraDeps) {
6265
- dependents.get(dep).push(task.id);
6266
- }
6267
- }
6268
- const batches = [];
6269
- const remaining = new Set(tasks.map((t) => t.id));
6270
- while (remaining.size > 0) {
6271
- const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
6272
- if (batch.length === 0) {
6273
- batches.push([...remaining].map((id) => taskById.get(id)));
6274
- break;
6275
- }
6276
- batches.push(batch);
6277
- for (const task of batch) {
6278
- remaining.delete(task.id);
6279
- for (const dependent of dependents.get(task.id)) {
6280
- inDegree.set(dependent, inDegree.get(dependent) - 1);
6281
- }
6282
- }
6283
- }
6284
- return batches;
6285
- }
6286
- var LAYER_ICONS = {
6287
- data: "\u{1F4BE}",
6288
- infra: "\u2699\uFE0F ",
6289
- service: "\u{1F527}",
6290
- api: "\u{1F310}",
6291
- view: "\u{1F5A5}\uFE0F ",
6292
- route: "\u{1F5FA}\uFE0F ",
6293
- test: "\u{1F9EA}"
6294
- };
6295
- function printTaskProgress(completed, total, task, mode) {
6296
- const pct = total > 0 ? Math.round(completed / total * 100) : 0;
6297
- const barWidth = 20;
6298
- const filled = Math.round(pct / 100 * barWidth);
6299
- const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
6300
- const icon = LAYER_ICONS[task.layer] ?? " ";
6301
- if (mode === "skip") {
6302
- console.log(
6303
- chalk8.gray(`
6304
- [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
6305
- );
6306
- } else {
6307
- console.log(
6308
- chalk8.bold(`
6309
- [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
6310
- );
6311
- }
6312
- }
6313
6441
 
6314
6442
  // core/reviewer.ts
6315
- import chalk10 from "chalk";
6316
- import { execSync as execSync2 } from "child_process";
6443
+ import chalk12 from "chalk";
6444
+ import { execSync as execSync3 } from "child_process";
6317
6445
  import * as path8 from "path";
6318
6446
  import * as fs12 from "fs-extra";
6319
6447
 
6320
6448
  // core/constitution-generator.ts
6321
- import chalk9 from "chalk";
6449
+ import chalk11 from "chalk";
6322
6450
  import * as fs11 from "fs-extra";
6323
6451
  import * as path7 from "path";
6324
6452
 
@@ -6468,7 +6596,7 @@ async function loadConstitution(projectRoot) {
6468
6596
  function printConstitutionHint(exists) {
6469
6597
  if (!exists) {
6470
6598
  console.log(
6471
- chalk9.yellow(
6599
+ chalk11.yellow(
6472
6600
  " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6473
6601
  )
6474
6602
  );
@@ -6552,16 +6680,16 @@ var CodeReviewer = class {
6552
6680
  this.projectRoot = projectRoot;
6553
6681
  }
6554
6682
  getGitDiff() {
6555
- const silent = { encoding: "utf-8", stdio: "pipe" };
6683
+ const silent = { encoding: "utf-8", stdio: "pipe", cwd: this.projectRoot, timeout: 3e4 };
6556
6684
  try {
6557
- execSync2("git rev-parse --is-inside-work-tree", silent);
6685
+ execSync3("git rev-parse --is-inside-work-tree", silent);
6558
6686
  } catch {
6559
6687
  return "";
6560
6688
  }
6561
6689
  try {
6562
- let diff = execSync2("git diff --cached", silent);
6563
- if (!diff.trim()) diff = execSync2("git diff HEAD", silent);
6564
- if (!diff.trim()) diff = execSync2("git diff", silent);
6690
+ let diff = execSync3("git diff --cached", silent);
6691
+ if (!diff.trim()) diff = execSync3("git diff HEAD", silent);
6692
+ if (!diff.trim()) diff = execSync3("git diff", silent);
6565
6693
  return diff;
6566
6694
  } catch {
6567
6695
  return "";
@@ -6586,7 +6714,7 @@ var CodeReviewer = class {
6586
6714
  async runThreePassReview(specContent, codeContext, specFile) {
6587
6715
  let complianceReview = "";
6588
6716
  if (specContent && specContent.trim() && specContent !== "(No spec \u2014 review for general code quality)") {
6589
- console.log(chalk10.gray(" Pass 0/3: Spec compliance check..."));
6717
+ console.log(chalk12.gray(" Pass 0/3: Spec compliance check..."));
6590
6718
  const compliancePrompt = `Check whether the implementation covers every requirement in the spec.
6591
6719
 
6592
6720
  === Feature Spec ===
@@ -6598,13 +6726,13 @@ ${codeContext}`;
6598
6726
  const complianceScore2 = extractComplianceScore(complianceReview);
6599
6727
  const missingCount = extractMissingCount(complianceReview);
6600
6728
  if (complianceScore2 > 0) {
6601
- const scoreColor = complianceScore2 >= 8 ? chalk10.green : complianceScore2 >= 6 ? chalk10.yellow : chalk10.red;
6729
+ const scoreColor = complianceScore2 >= 8 ? chalk12.green : complianceScore2 >= 6 ? chalk12.yellow : chalk12.red;
6602
6730
  console.log(
6603
- chalk10.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? chalk10.red(` \xB7 ${missingCount} missing requirement(s)`) : chalk10.green(" \xB7 all requirements covered"))
6731
+ chalk12.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? chalk12.red(` \xB7 ${missingCount} missing requirement(s)`) : chalk12.green(" \xB7 all requirements covered"))
6604
6732
  );
6605
6733
  }
6606
6734
  }
6607
- console.log(chalk10.gray(` Pass 1/3: Architecture review...`));
6735
+ console.log(chalk12.gray(` Pass 1/3: Architecture review...`));
6608
6736
  const accumulatedLessons = await loadAccumulatedLessons(this.projectRoot);
6609
6737
  const archPrompt = `Review the architecture of this change.
6610
6738
  ${complianceReview ? `
@@ -6621,7 +6749,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6621
6749
  === Code ===
6622
6750
  ${codeContext}`;
6623
6751
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6624
- console.log(chalk10.gray(" Pass 2/3: Implementation review..."));
6752
+ console.log(chalk12.gray(" Pass 2/3: Implementation review..."));
6625
6753
  const history = await loadReviewHistory(this.projectRoot);
6626
6754
  const historyContext = buildHistoryContext(history);
6627
6755
  const implPrompt = `Review the implementation details of this change.
@@ -6636,7 +6764,7 @@ ${codeContext}
6636
6764
  ${archReview}
6637
6765
  ${historyContext}`;
6638
6766
  const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
6639
- console.log(chalk10.gray(" Pass 3/3: Impact & complexity assessment..."));
6767
+ console.log(chalk12.gray(" Pass 3/3: Impact & complexity assessment..."));
6640
6768
  const impactPrompt = `Assess the impact and complexity of this change.
6641
6769
 
6642
6770
  === Feature Spec ===
@@ -6677,37 +6805,37 @@ ${sep}
6677
6805
  return combined;
6678
6806
  }
6679
6807
  async reviewCode(specContent, specFile) {
6680
- console.log(chalk10.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"));
6808
+ console.log(chalk12.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"));
6681
6809
  const diff = this.getGitDiff();
6682
6810
  if (!diff.trim()) {
6683
6811
  console.log(
6684
- chalk10.yellow(" No git diff found. Stage or commit changes first, then run review.")
6812
+ chalk12.yellow(" No git diff found. Stage or commit changes first, then run review.")
6685
6813
  );
6686
- console.log(chalk10.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6814
+ console.log(chalk12.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6687
6815
  return "No changes";
6688
6816
  }
6689
6817
  const { files, added, removed } = this.getDiffStats(diff);
6690
6818
  console.log(
6691
- chalk10.gray(` Diff: ${files} file(s), ${chalk10.green("+" + added)} ${chalk10.red("-" + removed)}`)
6819
+ chalk12.gray(` Diff: ${files} file(s), ${chalk12.green("+" + added)} ${chalk12.red("-" + removed)}`)
6692
6820
  );
6693
6821
  console.log(
6694
- chalk10.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6822
+ chalk12.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6695
6823
  );
6696
6824
  const codeContext = diff.slice(0, 1e4);
6697
6825
  const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
6698
- console.log(chalk10.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"));
6826
+ console.log(chalk12.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"));
6699
6827
  console.log(reviewResult);
6700
- console.log(chalk10.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"));
6828
+ console.log(chalk12.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"));
6701
6829
  return reviewResult;
6702
6830
  }
6703
6831
  /**
6704
6832
  * Review directly from generated file contents (for api mode where git diff is empty).
6705
6833
  */
6706
6834
  async reviewFiles(specContent, filePaths, workingDir, specFile) {
6707
- console.log(chalk10.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6708
- console.log(chalk10.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6835
+ console.log(chalk12.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6836
+ console.log(chalk12.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6709
6837
  console.log(
6710
- chalk10.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6838
+ chalk12.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6711
6839
  );
6712
6840
  let filesSection = "";
6713
6841
  for (const filePath of filePaths) {
@@ -6728,28 +6856,28 @@ ${content.slice(0, 3e3)}`;
6728
6856
  }
6729
6857
  }
6730
6858
  const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
6731
- console.log(chalk10.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"));
6859
+ console.log(chalk12.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"));
6732
6860
  console.log(reviewResult);
6733
- console.log(chalk10.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"));
6861
+ console.log(chalk12.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"));
6734
6862
  return reviewResult;
6735
6863
  }
6736
6864
  /** Print score trend from history (last N reviews) */
6737
6865
  async printScoreTrend(limit = 5) {
6738
6866
  const history = await loadReviewHistory(this.projectRoot);
6739
6867
  if (history.length === 0) {
6740
- console.log(chalk10.gray(" No review history yet."));
6868
+ console.log(chalk12.gray(" No review history yet."));
6741
6869
  return;
6742
6870
  }
6743
6871
  const recent = history.slice(-limit);
6744
- console.log(chalk10.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"));
6872
+ console.log(chalk12.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"));
6745
6873
  for (const entry of recent) {
6746
6874
  const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
6747
- const color = entry.score >= 8 ? chalk10.green : entry.score >= 6 ? chalk10.yellow : chalk10.red;
6748
- const impactTag = entry.impactLevel ? chalk10.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk10.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk10.yellow(entry.impactLevel) : chalk10.green(entry.impactLevel)}`) : "";
6749
- const complexityTag = entry.complexityLevel ? chalk10.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk10.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk10.yellow(entry.complexityLevel) : chalk10.green(entry.complexityLevel)}`) : "";
6875
+ const color = entry.score >= 8 ? chalk12.green : entry.score >= 6 ? chalk12.yellow : chalk12.red;
6876
+ const impactTag = entry.impactLevel ? chalk12.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk12.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk12.yellow(entry.impactLevel) : chalk12.green(entry.impactLevel)}`) : "";
6877
+ const complexityTag = entry.complexityLevel ? chalk12.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk12.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk12.yellow(entry.complexityLevel) : chalk12.green(entry.complexityLevel)}`) : "";
6750
6878
  console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
6751
6879
  }
6752
- console.log(chalk10.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"));
6880
+ console.log(chalk12.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"));
6753
6881
  }
6754
6882
  };
6755
6883
 
@@ -6799,17 +6927,17 @@ function parseSpecAndTasks(raw) {
6799
6927
  }
6800
6928
 
6801
6929
  // git/worktree.ts
6802
- import { execSync as execSync3 } from "child_process";
6930
+ import { execSync as execSync4 } from "child_process";
6803
6931
  import * as path9 from "path";
6804
6932
  import * as fs13 from "fs-extra";
6805
- import chalk11 from "chalk";
6933
+ import chalk13 from "chalk";
6806
6934
  var GitWorktreeManager = class {
6807
6935
  constructor(baseDir) {
6808
6936
  this.baseDir = baseDir;
6809
6937
  }
6810
6938
  isGitRepo() {
6811
6939
  try {
6812
- execSync3("git rev-parse --is-inside-work-tree", { cwd: this.baseDir, stdio: "ignore" });
6940
+ execSync4("git rev-parse --is-inside-work-tree", { cwd: this.baseDir, stdio: "ignore" });
6813
6941
  return true;
6814
6942
  } catch {
6815
6943
  return false;
@@ -6833,58 +6961,58 @@ var GitWorktreeManager = class {
6833
6961
  if (await fs13.pathExists(dest)) continue;
6834
6962
  try {
6835
6963
  await fs13.ensureSymlink(src, dest, "dir");
6836
- console.log(chalk11.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6964
+ console.log(chalk13.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6837
6965
  } catch (err) {
6838
- console.log(chalk11.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6839
- console.log(chalk11.yellow(` Run \`npm install\` inside the worktree manually.`));
6966
+ console.log(chalk13.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6967
+ console.log(chalk13.yellow(` Run \`npm install\` inside the worktree manually.`));
6840
6968
  }
6841
6969
  }
6842
6970
  }
6843
6971
  async createWorktree(idea) {
6844
6972
  if (!this.isGitRepo()) {
6845
- console.log(chalk11.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6973
+ console.log(chalk13.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6846
6974
  return null;
6847
6975
  }
6848
6976
  const featureName = this.sanitizeFeatureName(idea);
6849
6977
  const branchName = `feature/${featureName}`;
6850
6978
  const repoName = path9.basename(this.baseDir);
6851
6979
  const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
6852
- console.log(chalk11.cyan(`
6980
+ console.log(chalk13.cyan(`
6853
6981
  --- Setting up Git Worktree ---`));
6854
6982
  if (await fs13.pathExists(worktreePath)) {
6855
- console.log(chalk11.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6983
+ console.log(chalk13.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6856
6984
  await this.linkDependencies(worktreePath);
6857
6985
  return worktreePath;
6858
6986
  }
6859
6987
  try {
6860
6988
  let branchExists = false;
6861
6989
  try {
6862
- execSync3(`git show-ref --verify refs/heads/${branchName}`, {
6990
+ execSync4(`git show-ref --verify refs/heads/${branchName}`, {
6863
6991
  cwd: this.baseDir,
6864
6992
  stdio: "ignore"
6865
6993
  });
6866
6994
  branchExists = true;
6867
6995
  } catch {
6868
6996
  }
6869
- console.log(chalk11.gray(`Creating worktree at: ${worktreePath}`));
6997
+ console.log(chalk13.gray(`Creating worktree at: ${worktreePath}`));
6870
6998
  if (branchExists) {
6871
- execSync3(`git worktree add "${worktreePath}" ${branchName}`, {
6999
+ execSync4(`git worktree add "${worktreePath}" ${branchName}`, {
6872
7000
  cwd: this.baseDir,
6873
7001
  stdio: "inherit"
6874
7002
  });
6875
7003
  } else {
6876
- execSync3(`git worktree add -b ${branchName} "${worktreePath}"`, {
7004
+ execSync4(`git worktree add -b ${branchName} "${worktreePath}"`, {
6877
7005
  cwd: this.baseDir,
6878
7006
  stdio: "inherit"
6879
7007
  });
6880
7008
  }
6881
7009
  console.log(
6882
- chalk11.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
7010
+ chalk13.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6883
7011
  );
6884
7012
  await this.linkDependencies(worktreePath);
6885
7013
  return worktreePath;
6886
7014
  } catch (error) {
6887
- console.error(chalk11.red("Failed to create git worktree:"), error);
7015
+ console.error(chalk13.red("Failed to create git worktree:"), error);
6888
7016
  return null;
6889
7017
  }
6890
7018
  }