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.js CHANGED
@@ -4173,8 +4173,8 @@ ${currentSpec}`,
4173
4173
  };
4174
4174
 
4175
4175
  // core/code-generator.ts
4176
- var import_chalk8 = __toESM(require("chalk"));
4177
- var import_child_process = require("child_process");
4176
+ var import_chalk10 = __toESM(require("chalk"));
4177
+ var import_child_process2 = require("child_process");
4178
4178
  var path6 = __toESM(require("path"));
4179
4179
  var fs10 = __toESM(require("fs-extra"));
4180
4180
 
@@ -4751,7 +4751,7 @@ async function updateTaskStatus(specFilePath, taskId, status) {
4751
4751
  }
4752
4752
 
4753
4753
  // core/dsl-extractor.ts
4754
- var import_chalk6 = __toESM(require("chalk"));
4754
+ var import_chalk7 = __toESM(require("chalk"));
4755
4755
  var fs6 = __toESM(require("fs-extra"));
4756
4756
  var path4 = __toESM(require("path"));
4757
4757
  var import_prompts2 = require("@inquirer/prompts");
@@ -4840,6 +4840,7 @@ function validateDsl(raw) {
4840
4840
  }
4841
4841
  }
4842
4842
  }
4843
+ crossReferenceChecks(obj, errors);
4843
4844
  if (errors.length > 0) {
4844
4845
  return { valid: false, errors };
4845
4846
  }
@@ -5071,6 +5072,64 @@ function validateComponent(raw, path10, errors) {
5071
5072
  }
5072
5073
  }
5073
5074
  }
5075
+ function crossReferenceChecks(obj, errors) {
5076
+ const models = Array.isArray(obj["models"]) ? obj["models"] : [];
5077
+ const endpoints = Array.isArray(obj["endpoints"]) ? obj["endpoints"] : [];
5078
+ const components = Array.isArray(obj["components"]) ? obj["components"] : [];
5079
+ const seenRoutes = /* @__PURE__ */ new Map();
5080
+ for (let i = 0; i < endpoints.length; i++) {
5081
+ const ep = endpoints[i];
5082
+ if (typeof ep?.["method"] === "string" && typeof ep?.["path"] === "string") {
5083
+ const route = `${ep["method"].toUpperCase()} ${ep["path"]}`;
5084
+ if (seenRoutes.has(route)) {
5085
+ errors.push({
5086
+ path: `endpoints[${i}]`,
5087
+ message: `Duplicate route "${route}" \u2014 also defined at endpoints[${seenRoutes.get(route)}]`
5088
+ });
5089
+ } else {
5090
+ seenRoutes.set(route, i);
5091
+ }
5092
+ }
5093
+ }
5094
+ const modelNames = new Set(
5095
+ models.filter((m) => typeof m?.["name"] === "string").map((m) => m["name"])
5096
+ );
5097
+ for (let i = 0; i < models.length; i++) {
5098
+ const m = models[i];
5099
+ if (!Array.isArray(m?.["relations"])) continue;
5100
+ for (const rel of m["relations"]) {
5101
+ if (typeof rel !== "string") continue;
5102
+ const refMatch = rel.match(/(?:hasMany|hasOne|belongsTo|manyToMany)\s+(\w+)/i);
5103
+ if (refMatch) {
5104
+ const refName = refMatch[1];
5105
+ if (!modelNames.has(refName)) {
5106
+ errors.push({
5107
+ path: `models[${i}].relations`,
5108
+ message: `Relation references model "${refName}" which is not defined in models[]`
5109
+ });
5110
+ }
5111
+ }
5112
+ }
5113
+ }
5114
+ const endpointIds = new Set(
5115
+ endpoints.filter((e) => typeof e?.["id"] === "string").map((e) => e["id"])
5116
+ );
5117
+ if (endpointIds.size > 0) {
5118
+ for (let i = 0; i < components.length; i++) {
5119
+ const c = components[i];
5120
+ if (!Array.isArray(c?.["apiCalls"])) continue;
5121
+ for (const call of c["apiCalls"]) {
5122
+ if (typeof call !== "string") continue;
5123
+ if (!endpointIds.has(call)) {
5124
+ errors.push({
5125
+ path: `components[${i}].apiCalls`,
5126
+ message: `References endpoint "${call}" which is not defined in endpoints[]`
5127
+ });
5128
+ }
5129
+ }
5130
+ }
5131
+ }
5132
+ }
5074
5133
  function requireNonEmptyString(v2, path10, errors) {
5075
5134
  if (typeof v2 !== "string" || v2.trim().length === 0) {
5076
5135
  errors.push({
@@ -5085,6 +5144,26 @@ function typeLabel(v2) {
5085
5144
  return typeof v2;
5086
5145
  }
5087
5146
 
5147
+ // core/token-budget.ts
5148
+ var import_chalk6 = __toESM(require("chalk"));
5149
+ var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5150
+ function estimateTokens(text) {
5151
+ if (!text) return 0;
5152
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5153
+ const nonCjkLength = text.length - cjkCount;
5154
+ return Math.ceil(cjkCount + nonCjkLength / 4);
5155
+ }
5156
+ var DEFAULT_TOKEN_BUDGETS = {
5157
+ gemini: 9e5,
5158
+ claude: 18e4,
5159
+ openai: 12e4,
5160
+ deepseek: 6e4,
5161
+ default: 1e5
5162
+ };
5163
+ function getDefaultBudget(providerName) {
5164
+ return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
5165
+ }
5166
+
5088
5167
  // core/dsl-extractor.ts
5089
5168
  function dslFilePath(specFilePath) {
5090
5169
  const dir = path4.dirname(specFilePath);
@@ -5243,7 +5322,9 @@ var ROUTING_LIBS = [
5243
5322
  ["@tanstack/react-router", "tanstack-router"],
5244
5323
  ["react-navigation", "react-navigation"],
5245
5324
  ["expo-router", "expo-router"],
5246
- ["vue-router", "vue-router"]
5325
+ ["vue-router", "vue-router"],
5326
+ ["@solidjs/router", "solid-router"],
5327
+ ["@builder.io/qwik-city", "qwik-city"]
5247
5328
  ];
5248
5329
  async function loadFrontendContext(projectRoot) {
5249
5330
  const ctx = {
@@ -5275,6 +5356,18 @@ async function loadFrontendContext(projectRoot) {
5275
5356
  const has = (name) => depKeys.includes(name);
5276
5357
  if (has("react-native") || has("expo")) {
5277
5358
  ctx.framework = "react-native";
5359
+ } else if (has("@sveltejs/kit")) {
5360
+ ctx.framework = "sveltekit";
5361
+ } else if (has("svelte")) {
5362
+ ctx.framework = "svelte";
5363
+ } else if (has("@builder.io/qwik")) {
5364
+ ctx.framework = "qwik";
5365
+ } else if (has("@remix-run/react") || has("@remix-run/node")) {
5366
+ ctx.framework = "remix";
5367
+ } else if (has("astro")) {
5368
+ ctx.framework = "astro";
5369
+ } else if (has("solid-js")) {
5370
+ ctx.framework = "solid";
5278
5371
  } else if (has("next")) {
5279
5372
  ctx.framework = "next";
5280
5373
  } else if (has("react")) {
@@ -5301,6 +5394,12 @@ async function loadFrontendContext(projectRoot) {
5301
5394
  if (ctx.framework === "next") {
5302
5395
  const hasAppDir = await fs7.pathExists(path5.join(projectRoot, "app"));
5303
5396
  ctx.routingPattern = hasAppDir ? "next-app-router" : "next-pages-router";
5397
+ } else if (ctx.framework === "sveltekit") {
5398
+ ctx.routingPattern = "sveltekit-file-router";
5399
+ } else if (ctx.framework === "remix") {
5400
+ ctx.routingPattern = "remix-file-router";
5401
+ } else if (ctx.framework === "astro") {
5402
+ ctx.routingPattern = "astro-file-router";
5304
5403
  } else {
5305
5404
  for (const [lib, label] of ROUTING_LIBS) {
5306
5405
  if (has(lib)) {
@@ -5686,13 +5785,14 @@ function getActiveSnapshot() {
5686
5785
 
5687
5786
  // core/run-logger.ts
5688
5787
  var fs9 = __toESM(require("fs-extra"));
5689
- var import_chalk7 = __toESM(require("chalk"));
5788
+ var import_chalk8 = __toESM(require("chalk"));
5690
5789
  var _activeLogger = null;
5691
5790
  function getActiveLogger() {
5692
5791
  return _activeLogger;
5693
5792
  }
5694
5793
 
5695
- // core/code-generator.ts
5794
+ // core/codegen/helpers.ts
5795
+ var import_child_process = require("child_process");
5696
5796
  function buildSharedConfigSection(context) {
5697
5797
  if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
5698
5798
  const lines = [
@@ -5824,7 +5924,7 @@ function buildGeneratedFilesSection(cache) {
5824
5924
  }
5825
5925
  function isRtkAvailable() {
5826
5926
  try {
5827
- (0, import_child_process.execSync)("rtk --version", { stdio: "ignore" });
5927
+ (0, import_child_process.execSync)("rtk --version", { stdio: "ignore", timeout: 1e4 });
5828
5928
  return true;
5829
5929
  } catch {
5830
5930
  return false;
@@ -5848,6 +5948,73 @@ function parseJsonArray(text) {
5848
5948
  }
5849
5949
  return [];
5850
5950
  }
5951
+
5952
+ // core/codegen/topo-sort.ts
5953
+ var import_chalk9 = __toESM(require("chalk"));
5954
+ function topoSortLayerTasks(tasks) {
5955
+ if (tasks.length <= 1) return [tasks];
5956
+ const idSet = new Set(tasks.map((t) => t.id));
5957
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
5958
+ const inDegree = /* @__PURE__ */ new Map();
5959
+ const dependents = /* @__PURE__ */ new Map();
5960
+ for (const task of tasks) {
5961
+ inDegree.set(task.id, 0);
5962
+ dependents.set(task.id, []);
5963
+ }
5964
+ for (const task of tasks) {
5965
+ const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
5966
+ inDegree.set(task.id, intraDeps.length);
5967
+ for (const dep of intraDeps) {
5968
+ dependents.get(dep).push(task.id);
5969
+ }
5970
+ }
5971
+ const batches = [];
5972
+ const remaining = new Set(tasks.map((t) => t.id));
5973
+ while (remaining.size > 0) {
5974
+ const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
5975
+ if (batch.length === 0) {
5976
+ batches.push([...remaining].map((id) => taskById.get(id)));
5977
+ break;
5978
+ }
5979
+ batches.push(batch);
5980
+ for (const task of batch) {
5981
+ remaining.delete(task.id);
5982
+ for (const dependent of dependents.get(task.id)) {
5983
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
5984
+ }
5985
+ }
5986
+ }
5987
+ return batches;
5988
+ }
5989
+ var LAYER_ICONS = {
5990
+ data: "\u{1F4BE}",
5991
+ infra: "\u2699\uFE0F ",
5992
+ service: "\u{1F527}",
5993
+ api: "\u{1F310}",
5994
+ view: "\u{1F5A5}\uFE0F ",
5995
+ route: "\u{1F5FA}\uFE0F ",
5996
+ test: "\u{1F9EA}"
5997
+ };
5998
+ function printTaskProgress(completed, total, task, mode) {
5999
+ const pct = total > 0 ? Math.round(completed / total * 100) : 0;
6000
+ const barWidth = 20;
6001
+ const filled = Math.round(pct / 100 * barWidth);
6002
+ const bar = import_chalk9.default.green("\u2588".repeat(filled)) + import_chalk9.default.gray("\u2591".repeat(barWidth - filled));
6003
+ const icon = LAYER_ICONS[task.layer] ?? " ";
6004
+ if (mode === "skip") {
6005
+ console.log(
6006
+ import_chalk9.default.gray(`
6007
+ [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
6008
+ );
6009
+ } else {
6010
+ console.log(
6011
+ import_chalk9.default.bold(`
6012
+ [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
6013
+ );
6014
+ }
6015
+ }
6016
+
6017
+ // core/code-generator.ts
5851
6018
  var CodeGenerator = class {
5852
6019
  constructor(provider, mode = "claude-code") {
5853
6020
  this.provider = provider;
@@ -5858,13 +6025,13 @@ var CodeGenerator = class {
5858
6025
  let effectiveMode = this.mode;
5859
6026
  if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
5860
6027
  console.log(
5861
- import_chalk8.default.yellow(
6028
+ import_chalk10.default.yellow(
5862
6029
  `
5863
6030
  \u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
5864
6031
  )
5865
6032
  );
5866
- console.log(import_chalk8.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
5867
- console.log(import_chalk8.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
6033
+ console.log(import_chalk10.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
6034
+ console.log(import_chalk10.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
5868
6035
  `));
5869
6036
  effectiveMode = "api";
5870
6037
  }
@@ -5882,23 +6049,23 @@ var CodeGenerator = class {
5882
6049
  // ── Mode: claude-code ──────────────────────────────────────────────────────
5883
6050
  isClaudeCLIAvailable() {
5884
6051
  try {
5885
- (0, import_child_process.execSync)("claude --version", { stdio: "ignore" });
6052
+ (0, import_child_process2.execSync)("claude --version", { stdio: "ignore", timeout: 1e4 });
5886
6053
  return true;
5887
6054
  } catch {
5888
6055
  return false;
5889
6056
  }
5890
6057
  }
5891
6058
  async runClaudeCode(specFilePath, workingDir, options = {}) {
5892
- console.log(import_chalk8.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6059
+ console.log(import_chalk10.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5893
6060
  if (!this.isClaudeCLIAvailable()) {
5894
- console.log(import_chalk8.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
5895
- console.log(import_chalk8.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
6061
+ console.log(import_chalk10.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
6062
+ console.log(import_chalk10.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
5896
6063
  return this.runPlanMode(specFilePath);
5897
6064
  }
5898
6065
  const rtkAvailable = isRtkAvailable();
5899
6066
  const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
5900
6067
  if (rtkAvailable) {
5901
- console.log(import_chalk8.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
6068
+ console.log(import_chalk10.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
5902
6069
  }
5903
6070
  const tasks = await loadTasksForSpec(specFilePath);
5904
6071
  if (options.auto && tasks && tasks.length > 0) {
@@ -5914,28 +6081,28 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5914
6081
  const promptFile = path6.join(workingDir, ".claude-prompt.txt");
5915
6082
  await fs10.writeFile(promptFile, promptContent, "utf-8");
5916
6083
  if (options.auto) {
5917
- console.log(import_chalk8.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5918
- console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
6084
+ console.log(import_chalk10.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
6085
+ console.log(import_chalk10.default.gray(` Spec: ${specFilePath}`));
5919
6086
  try {
5920
- (0, import_child_process.spawnSync)(claudeCmd, ["-p", promptContent], {
6087
+ (0, import_child_process2.spawnSync)(claudeCmd, ["-p", promptContent], {
5921
6088
  cwd: workingDir,
5922
6089
  stdio: "inherit",
5923
6090
  shell: false
5924
6091
  });
5925
- console.log(import_chalk8.default.green("\n \u2714 Claude Code completed."));
6092
+ console.log(import_chalk10.default.green("\n \u2714 Claude Code completed."));
5926
6093
  } catch {
5927
- console.log(import_chalk8.default.yellow("\n Claude Code exited. Check output above."));
6094
+ console.log(import_chalk10.default.yellow("\n Claude Code exited. Check output above."));
5928
6095
  }
5929
6096
  } else {
5930
- console.log(import_chalk8.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
5931
- console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
5932
- if (tasks) console.log(import_chalk8.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
5933
- console.log(import_chalk8.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
6097
+ console.log(import_chalk10.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
6098
+ console.log(import_chalk10.default.gray(` Spec: ${specFilePath}`));
6099
+ if (tasks) console.log(import_chalk10.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
6100
+ console.log(import_chalk10.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
5934
6101
  try {
5935
- (0, import_child_process.execSync)(claudeCmd, { cwd: workingDir, stdio: "inherit" });
5936
- console.log(import_chalk8.default.green("\n \u2714 Claude Code session completed."));
6102
+ (0, import_child_process2.execSync)(claudeCmd, { cwd: workingDir, stdio: "inherit" });
6103
+ console.log(import_chalk10.default.green("\n \u2714 Claude Code session completed."));
5937
6104
  } catch {
5938
- console.log(import_chalk8.default.yellow("\n Claude Code session ended. Continuing workflow."));
6105
+ console.log(import_chalk10.default.yellow("\n Claude Code session ended. Continuing workflow."));
5939
6106
  }
5940
6107
  }
5941
6108
  }
@@ -5948,10 +6115,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5948
6115
  const pending = tasks.filter((t) => t.status !== "done");
5949
6116
  const doneCount = tasks.length - pending.length;
5950
6117
  if (options.resume && doneCount > 0) {
5951
- console.log(import_chalk8.default.cyan(`
6118
+ console.log(import_chalk10.default.cyan(`
5952
6119
  Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
5953
6120
  } else {
5954
- console.log(import_chalk8.default.cyan(`
6121
+ console.log(import_chalk10.default.cyan(`
5955
6122
  Incremental mode: ${tasks.length} tasks`));
5956
6123
  }
5957
6124
  let completed = doneCount;
@@ -5972,7 +6139,7 @@ Full spec is at: ${specFilePath}
5972
6139
  Implement ONLY this task. Do not implement other tasks.`;
5973
6140
  let taskStatus = "done";
5974
6141
  try {
5975
- (0, import_child_process.spawnSync)(claudeCmd, ["-p", taskPrompt], {
6142
+ (0, import_child_process2.spawnSync)(claudeCmd, ["-p", taskPrompt], {
5976
6143
  cwd: workingDir,
5977
6144
  stdio: "inherit",
5978
6145
  shell: false
@@ -5980,33 +6147,33 @@ Implement ONLY this task. Do not implement other tasks.`;
5980
6147
  completed++;
5981
6148
  } catch {
5982
6149
  taskStatus = "failed";
5983
- console.log(import_chalk8.default.yellow(`
6150
+ console.log(import_chalk10.default.yellow(`
5984
6151
  \u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
5985
6152
  }
5986
6153
  await updateTaskStatus(specFilePath, task.id, taskStatus);
5987
6154
  }
5988
6155
  const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
5989
6156
  console.log(
5990
- import_chalk8.default.bold(
6157
+ import_chalk10.default.bold(
5991
6158
  `
5992
- ${successCount === tasks.length ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
6159
+ ${successCount === tasks.length ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
5993
6160
  )
5994
6161
  );
5995
6162
  }
5996
6163
  // ── Mode: api ─────────────────────────────────────────────────────────────
5997
6164
  async runApiMode(specFilePath, workingDir, context, options = {}) {
5998
6165
  console.log(
5999
- import_chalk8.default.blue(
6166
+ import_chalk10.default.blue(
6000
6167
  `
6001
6168
  \u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
6002
6169
  )
6003
6170
  );
6004
6171
  const systemPrompt = getCodeGenSystemPrompt(options.repoType);
6005
6172
  if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
6006
- console.log(import_chalk8.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
6173
+ console.log(import_chalk10.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
6007
6174
  }
6008
6175
  const spec = await fs10.readFile(specFilePath, "utf-8");
6009
- const constitutionSection = context?.constitution ? `
6176
+ let constitutionSection = context?.constitution ? `
6010
6177
  === Project Constitution (MUST follow) ===
6011
6178
  ${context.constitution}
6012
6179
  ` : "";
@@ -6021,7 +6188,7 @@ ${buildDslContextSection(dsl)}
6021
6188
  if (dsl) {
6022
6189
  const cmpCount = dsl.components?.length ?? 0;
6023
6190
  const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
6024
- console.log(import_chalk8.default.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
6191
+ console.log(import_chalk10.default.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
6025
6192
  }
6026
6193
  const isFrontend = isFrontendDeps(context?.dependencies ?? []);
6027
6194
  let frontendSection = "";
@@ -6030,13 +6197,30 @@ ${buildDslContextSection(dsl)}
6030
6197
  frontendSection = `
6031
6198
  ${buildFrontendContextSection(fctx)}
6032
6199
  `;
6033
- console.log(import_chalk8.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6200
+ console.log(import_chalk10.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6201
+ }
6202
+ const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
6203
+ const estimatedTokenCount = estimateTokens(allContextText);
6204
+ const budget = getDefaultBudget(this.provider.providerName);
6205
+ if (estimatedTokenCount > budget * 0.7) {
6206
+ console.log(
6207
+ import_chalk10.default.yellow(
6208
+ ` \u26A0 Context size: ~${Math.round(estimatedTokenCount / 1e3)}K tokens (budget: ${Math.round(budget / 1e3)}K for ${this.provider.providerName})`
6209
+ )
6210
+ );
6211
+ if (constitutionSection.length > 4e3) {
6212
+ const s9Start = constitutionSection.indexOf("## 9.");
6213
+ if (s9Start > 0) {
6214
+ 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";
6215
+ console.log(import_chalk10.default.gray(" \u2192 \xA79 trimmed from constitution to save tokens."));
6216
+ }
6217
+ }
6034
6218
  }
6035
6219
  const tasks = await loadTasksForSpec(specFilePath);
6036
6220
  if (tasks && tasks.length > 0) {
6037
6221
  return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
6038
6222
  }
6039
- console.log(import_chalk8.default.gray(" [1/2] Planning implementation files..."));
6223
+ console.log(import_chalk10.default.gray(" [1/2] Planning implementation files..."));
6040
6224
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
6041
6225
 
6042
6226
  IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
@@ -6059,18 +6243,18 @@ Output ONLY a valid JSON array:
6059
6243
  const planResponse = await this.provider.generate(planPrompt, systemPrompt);
6060
6244
  filePlan = parseJsonArray(planResponse);
6061
6245
  } catch (err) {
6062
- console.error(import_chalk8.default.red(" Failed to generate file plan:"), err);
6246
+ console.error(import_chalk10.default.red(" Failed to generate file plan:"), err);
6063
6247
  }
6064
6248
  if (filePlan.length === 0) {
6065
- console.log(import_chalk8.default.yellow(" Could not determine file plan. Falling back to plan mode."));
6249
+ console.log(import_chalk10.default.yellow(" Could not determine file plan. Falling back to plan mode."));
6066
6250
  await this.runPlanMode(specFilePath);
6067
6251
  return [];
6068
6252
  }
6069
- console.log(import_chalk8.default.cyan(`
6253
+ console.log(import_chalk10.default.cyan(`
6070
6254
  Plan: ${filePlan.length} file(s) to process`));
6071
6255
  filePlan.forEach((item) => {
6072
- const icon = item.action === "create" ? import_chalk8.default.green("+") : import_chalk8.default.yellow("~");
6073
- console.log(` ${icon} ${item.file}: ${import_chalk8.default.gray(item.description)}`);
6256
+ const icon = item.action === "create" ? import_chalk10.default.green("+") : import_chalk10.default.yellow("~");
6257
+ console.log(` ${icon} ${item.file}: ${import_chalk10.default.gray(item.description)}`);
6074
6258
  });
6075
6259
  const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
6076
6260
  return files;
@@ -6079,13 +6263,13 @@ Output ONLY a valid JSON array:
6079
6263
  const pendingTasks = tasks.filter((t) => t.status !== "done");
6080
6264
  const doneCount = tasks.length - pendingTasks.length;
6081
6265
  if (options.resume && doneCount > 0) {
6082
- console.log(import_chalk8.default.cyan(`
6083
- Task-based generation (resume): ${tasks.length} tasks (${import_chalk8.default.green(doneCount + " already done")}, skipping)`));
6266
+ console.log(import_chalk10.default.cyan(`
6267
+ Task-based generation (resume): ${tasks.length} tasks (${import_chalk10.default.green(doneCount + " already done")}, skipping)`));
6084
6268
  } else if (doneCount > 0) {
6085
- console.log(import_chalk8.default.cyan(`
6086
- Task-based generation: ${tasks.length} tasks (${import_chalk8.default.green(doneCount + " already done")}, resuming from checkpoint)`));
6269
+ console.log(import_chalk10.default.cyan(`
6270
+ Task-based generation: ${tasks.length} tasks (${import_chalk10.default.green(doneCount + " already done")}, resuming from checkpoint)`));
6087
6271
  } else {
6088
- console.log(import_chalk8.default.cyan(`
6272
+ console.log(import_chalk10.default.cyan(`
6089
6273
  Task-based generation: ${tasks.length} tasks`));
6090
6274
  }
6091
6275
  const sharedConfigPaths = new Set(
@@ -6117,9 +6301,9 @@ Output ONLY a valid JSON array:
6117
6301
  const pct = Math.round(completedTasks / tasks.length * 100);
6118
6302
  const barWidth = 20;
6119
6303
  const filled = Math.round(pct / 100 * barWidth);
6120
- const bar = import_chalk8.default.green("\u2588".repeat(filled)) + import_chalk8.default.gray("\u2591".repeat(barWidth - filled));
6304
+ const bar = import_chalk10.default.green("\u2588".repeat(filled)) + import_chalk10.default.gray("\u2591".repeat(barWidth - filled));
6121
6305
  console.log(
6122
- import_chalk8.default.bold(`
6306
+ import_chalk10.default.bold(`
6123
6307
  [${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
6124
6308
  );
6125
6309
  } else {
@@ -6127,7 +6311,7 @@ Output ONLY a valid JSON array:
6127
6311
  }
6128
6312
  const executeTask = async (task, batchIsParallel) => {
6129
6313
  if (task.filesToTouch.length === 0) {
6130
- if (!batchIsParallel) console.log(import_chalk8.default.gray(" No files specified, skipping."));
6314
+ if (!batchIsParallel) console.log(import_chalk10.default.gray(" No files specified, skipping."));
6131
6315
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6132
6316
  }
6133
6317
  const filePlan = await Promise.all(
@@ -6184,13 +6368,19 @@ ${taskContext}`,
6184
6368
  const layerResults = [];
6185
6369
  for (const batch of taskBatches) {
6186
6370
  const batchIsParallel = batch.length > 1;
6187
- const batchResultPromises = batch.map(
6188
- (task) => executeTask(task, batchIsParallel).catch((err) => {
6189
- console.log(import_chalk8.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
6190
- return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
6191
- })
6192
- );
6193
- const batchResults = await Promise.all(batchResultPromises);
6371
+ const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
6372
+ const settled = await Promise.allSettled(batchResultPromises);
6373
+ const batchResults = [];
6374
+ for (let i = 0; i < settled.length; i++) {
6375
+ const outcome = settled[i];
6376
+ if (outcome.status === "fulfilled") {
6377
+ batchResults.push(outcome.value);
6378
+ } else {
6379
+ const task = batch[i];
6380
+ console.log(import_chalk10.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
6381
+ batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
6382
+ }
6383
+ }
6194
6384
  layerResults.push(...batchResults);
6195
6385
  await updateCacheFromBatch(batchResults);
6196
6386
  }
@@ -6202,14 +6392,14 @@ ${taskContext}`,
6202
6392
  totalFiles += result.total;
6203
6393
  allGeneratedFiles.push(...result.files);
6204
6394
  if (isParallel) {
6205
- const icon = result.success === result.total ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!");
6395
+ const icon = result.success === result.total ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!");
6206
6396
  const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
6207
6397
  console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
6208
6398
  }
6209
6399
  const taskStatus = result.success === result.total ? "done" : "failed";
6210
6400
  await updateTaskStatus(specFilePath, result.task.id, taskStatus);
6211
6401
  if (taskStatus === "failed") {
6212
- console.log(import_chalk8.default.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
6402
+ console.log(import_chalk10.default.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
6213
6403
  }
6214
6404
  }
6215
6405
  completedTasks += layerTasks.length;
@@ -6224,7 +6414,7 @@ ${taskContext}`,
6224
6414
  if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
6225
6415
  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.`;
6226
6416
  }
6227
- console.log(import_chalk8.default.gray(`
6417
+ console.log(import_chalk10.default.gray(`
6228
6418
  + updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
6229
6419
  const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
6230
6420
  await this.generateFiles(
@@ -6242,17 +6432,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
6242
6432
  }
6243
6433
  }
6244
6434
  console.log(
6245
- import_chalk8.default.bold(
6435
+ import_chalk10.default.bold(
6246
6436
  `
6247
- ${totalSuccess === totalFiles ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
6437
+ ${totalSuccess === totalFiles ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
6248
6438
  )
6249
6439
  );
6250
6440
  return allGeneratedFiles;
6251
6441
  }
6252
6442
  async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
6253
- const prefix = taskLabel ? ` [${import_chalk8.default.cyan(taskLabel)}] ` : " ";
6443
+ const prefix = taskLabel ? ` [${import_chalk10.default.cyan(taskLabel)}] ` : " ";
6254
6444
  if (!taskLabel) {
6255
- console.log(import_chalk8.default.gray(`
6445
+ console.log(import_chalk10.default.gray(`
6256
6446
  Generating ${filePlan.length} file(s)...`));
6257
6447
  }
6258
6448
  let successCount = 0;
@@ -6280,17 +6470,17 @@ ${existingContent || "Output only the complete file content."}`;
6280
6470
  await fs10.ensureDir(path6.dirname(fullPath));
6281
6471
  await fs10.writeFile(fullPath, fileContent, "utf-8");
6282
6472
  getActiveLogger()?.fileWritten(item.file);
6283
- console.log(`${prefix}${existingContent ? import_chalk8.default.yellow("~") : import_chalk8.default.green("+")} ${import_chalk8.default.bold(item.file)} ${import_chalk8.default.green("\u2714")}`);
6473
+ console.log(`${prefix}${existingContent ? import_chalk10.default.yellow("~") : import_chalk10.default.green("+")} ${import_chalk10.default.bold(item.file)} ${import_chalk10.default.green("\u2714")}`);
6284
6474
  successCount++;
6285
6475
  writtenFiles.push(item.file);
6286
6476
  } catch (err) {
6287
- console.log(`${prefix}${import_chalk8.default.red("\u2718")} ${import_chalk8.default.bold(item.file)} \u2014 ${import_chalk8.default.red(err.message)}`);
6477
+ console.log(`${prefix}${import_chalk10.default.red("\u2718")} ${import_chalk10.default.bold(item.file)} \u2014 ${import_chalk10.default.red(err.message)}`);
6288
6478
  }
6289
6479
  }
6290
6480
  if (!taskLabel) {
6291
6481
  console.log(
6292
- import_chalk8.default.bold(
6293
- ` ${successCount === filePlan.length ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
6482
+ import_chalk10.default.bold(
6483
+ ` ${successCount === filePlan.length ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
6294
6484
  )
6295
6485
  );
6296
6486
  }
@@ -6298,7 +6488,7 @@ ${existingContent || "Output only the complete file content."}`;
6298
6488
  }
6299
6489
  // ── Mode: plan ─────────────────────────────────────────────────────────────
6300
6490
  async runPlanMode(specFilePath) {
6301
- console.log(import_chalk8.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6491
+ console.log(import_chalk10.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6302
6492
  const spec = await fs10.readFile(specFilePath, "utf-8");
6303
6493
  const plan = await this.provider.generate(
6304
6494
  `Create a detailed, step-by-step implementation plan for the following feature spec.
@@ -6311,80 +6501,18 @@ Be specific about:
6311
6501
  ${spec}`,
6312
6502
  "You are a senior developer creating an actionable implementation guide."
6313
6503
  );
6314
- console.log(import_chalk8.default.cyan("\n") + plan);
6504
+ console.log(import_chalk10.default.cyan("\n") + plan);
6315
6505
  }
6316
6506
  };
6317
- function topoSortLayerTasks(tasks) {
6318
- if (tasks.length <= 1) return [tasks];
6319
- const idSet = new Set(tasks.map((t) => t.id));
6320
- const taskById = new Map(tasks.map((t) => [t.id, t]));
6321
- const inDegree = /* @__PURE__ */ new Map();
6322
- const dependents = /* @__PURE__ */ new Map();
6323
- for (const task of tasks) {
6324
- inDegree.set(task.id, 0);
6325
- dependents.set(task.id, []);
6326
- }
6327
- for (const task of tasks) {
6328
- const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
6329
- inDegree.set(task.id, intraDeps.length);
6330
- for (const dep of intraDeps) {
6331
- dependents.get(dep).push(task.id);
6332
- }
6333
- }
6334
- const batches = [];
6335
- const remaining = new Set(tasks.map((t) => t.id));
6336
- while (remaining.size > 0) {
6337
- const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
6338
- if (batch.length === 0) {
6339
- batches.push([...remaining].map((id) => taskById.get(id)));
6340
- break;
6341
- }
6342
- batches.push(batch);
6343
- for (const task of batch) {
6344
- remaining.delete(task.id);
6345
- for (const dependent of dependents.get(task.id)) {
6346
- inDegree.set(dependent, inDegree.get(dependent) - 1);
6347
- }
6348
- }
6349
- }
6350
- return batches;
6351
- }
6352
- var LAYER_ICONS = {
6353
- data: "\u{1F4BE}",
6354
- infra: "\u2699\uFE0F ",
6355
- service: "\u{1F527}",
6356
- api: "\u{1F310}",
6357
- view: "\u{1F5A5}\uFE0F ",
6358
- route: "\u{1F5FA}\uFE0F ",
6359
- test: "\u{1F9EA}"
6360
- };
6361
- function printTaskProgress(completed, total, task, mode) {
6362
- const pct = total > 0 ? Math.round(completed / total * 100) : 0;
6363
- const barWidth = 20;
6364
- const filled = Math.round(pct / 100 * barWidth);
6365
- const bar = import_chalk8.default.green("\u2588".repeat(filled)) + import_chalk8.default.gray("\u2591".repeat(barWidth - filled));
6366
- const icon = LAYER_ICONS[task.layer] ?? " ";
6367
- if (mode === "skip") {
6368
- console.log(
6369
- import_chalk8.default.gray(`
6370
- [${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
6371
- );
6372
- } else {
6373
- console.log(
6374
- import_chalk8.default.bold(`
6375
- [${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
6376
- );
6377
- }
6378
- }
6379
6507
 
6380
6508
  // core/reviewer.ts
6381
- var import_chalk10 = __toESM(require("chalk"));
6382
- var import_child_process2 = require("child_process");
6509
+ var import_chalk12 = __toESM(require("chalk"));
6510
+ var import_child_process3 = require("child_process");
6383
6511
  var path8 = __toESM(require("path"));
6384
6512
  var fs12 = __toESM(require("fs-extra"));
6385
6513
 
6386
6514
  // core/constitution-generator.ts
6387
- var import_chalk9 = __toESM(require("chalk"));
6515
+ var import_chalk11 = __toESM(require("chalk"));
6388
6516
  var fs11 = __toESM(require("fs-extra"));
6389
6517
  var path7 = __toESM(require("path"));
6390
6518
 
@@ -6534,7 +6662,7 @@ async function loadConstitution(projectRoot) {
6534
6662
  function printConstitutionHint(exists) {
6535
6663
  if (!exists) {
6536
6664
  console.log(
6537
- import_chalk9.default.yellow(
6665
+ import_chalk11.default.yellow(
6538
6666
  " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6539
6667
  )
6540
6668
  );
@@ -6618,16 +6746,16 @@ var CodeReviewer = class {
6618
6746
  this.projectRoot = projectRoot;
6619
6747
  }
6620
6748
  getGitDiff() {
6621
- const silent = { encoding: "utf-8", stdio: "pipe" };
6749
+ const silent = { encoding: "utf-8", stdio: "pipe", cwd: this.projectRoot, timeout: 3e4 };
6622
6750
  try {
6623
- (0, import_child_process2.execSync)("git rev-parse --is-inside-work-tree", silent);
6751
+ (0, import_child_process3.execSync)("git rev-parse --is-inside-work-tree", silent);
6624
6752
  } catch {
6625
6753
  return "";
6626
6754
  }
6627
6755
  try {
6628
- let diff = (0, import_child_process2.execSync)("git diff --cached", silent);
6629
- if (!diff.trim()) diff = (0, import_child_process2.execSync)("git diff HEAD", silent);
6630
- if (!diff.trim()) diff = (0, import_child_process2.execSync)("git diff", silent);
6756
+ let diff = (0, import_child_process3.execSync)("git diff --cached", silent);
6757
+ if (!diff.trim()) diff = (0, import_child_process3.execSync)("git diff HEAD", silent);
6758
+ if (!diff.trim()) diff = (0, import_child_process3.execSync)("git diff", silent);
6631
6759
  return diff;
6632
6760
  } catch {
6633
6761
  return "";
@@ -6652,7 +6780,7 @@ var CodeReviewer = class {
6652
6780
  async runThreePassReview(specContent, codeContext, specFile) {
6653
6781
  let complianceReview = "";
6654
6782
  if (specContent && specContent.trim() && specContent !== "(No spec \u2014 review for general code quality)") {
6655
- console.log(import_chalk10.default.gray(" Pass 0/3: Spec compliance check..."));
6783
+ console.log(import_chalk12.default.gray(" Pass 0/3: Spec compliance check..."));
6656
6784
  const compliancePrompt = `Check whether the implementation covers every requirement in the spec.
6657
6785
 
6658
6786
  === Feature Spec ===
@@ -6664,13 +6792,13 @@ ${codeContext}`;
6664
6792
  const complianceScore2 = extractComplianceScore(complianceReview);
6665
6793
  const missingCount = extractMissingCount(complianceReview);
6666
6794
  if (complianceScore2 > 0) {
6667
- const scoreColor = complianceScore2 >= 8 ? import_chalk10.default.green : complianceScore2 >= 6 ? import_chalk10.default.yellow : import_chalk10.default.red;
6795
+ const scoreColor = complianceScore2 >= 8 ? import_chalk12.default.green : complianceScore2 >= 6 ? import_chalk12.default.yellow : import_chalk12.default.red;
6668
6796
  console.log(
6669
- import_chalk10.default.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? import_chalk10.default.red(` \xB7 ${missingCount} missing requirement(s)`) : import_chalk10.default.green(" \xB7 all requirements covered"))
6797
+ import_chalk12.default.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? import_chalk12.default.red(` \xB7 ${missingCount} missing requirement(s)`) : import_chalk12.default.green(" \xB7 all requirements covered"))
6670
6798
  );
6671
6799
  }
6672
6800
  }
6673
- console.log(import_chalk10.default.gray(` Pass 1/3: Architecture review...`));
6801
+ console.log(import_chalk12.default.gray(` Pass 1/3: Architecture review...`));
6674
6802
  const accumulatedLessons = await loadAccumulatedLessons(this.projectRoot);
6675
6803
  const archPrompt = `Review the architecture of this change.
6676
6804
  ${complianceReview ? `
@@ -6687,7 +6815,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6687
6815
  === Code ===
6688
6816
  ${codeContext}`;
6689
6817
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6690
- console.log(import_chalk10.default.gray(" Pass 2/3: Implementation review..."));
6818
+ console.log(import_chalk12.default.gray(" Pass 2/3: Implementation review..."));
6691
6819
  const history = await loadReviewHistory(this.projectRoot);
6692
6820
  const historyContext = buildHistoryContext(history);
6693
6821
  const implPrompt = `Review the implementation details of this change.
@@ -6702,7 +6830,7 @@ ${codeContext}
6702
6830
  ${archReview}
6703
6831
  ${historyContext}`;
6704
6832
  const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
6705
- console.log(import_chalk10.default.gray(" Pass 3/3: Impact & complexity assessment..."));
6833
+ console.log(import_chalk12.default.gray(" Pass 3/3: Impact & complexity assessment..."));
6706
6834
  const impactPrompt = `Assess the impact and complexity of this change.
6707
6835
 
6708
6836
  === Feature Spec ===
@@ -6743,37 +6871,37 @@ ${sep}
6743
6871
  return combined;
6744
6872
  }
6745
6873
  async reviewCode(specContent, specFile) {
6746
- console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6874
+ console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6747
6875
  const diff = this.getGitDiff();
6748
6876
  if (!diff.trim()) {
6749
6877
  console.log(
6750
- import_chalk10.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
6878
+ import_chalk12.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
6751
6879
  );
6752
- console.log(import_chalk10.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6880
+ console.log(import_chalk12.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6753
6881
  return "No changes";
6754
6882
  }
6755
6883
  const { files, added, removed } = this.getDiffStats(diff);
6756
6884
  console.log(
6757
- import_chalk10.default.gray(` Diff: ${files} file(s), ${import_chalk10.default.green("+" + added)} ${import_chalk10.default.red("-" + removed)}`)
6885
+ import_chalk12.default.gray(` Diff: ${files} file(s), ${import_chalk12.default.green("+" + added)} ${import_chalk12.default.red("-" + removed)}`)
6758
6886
  );
6759
6887
  console.log(
6760
- import_chalk10.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6888
+ import_chalk12.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6761
6889
  );
6762
6890
  const codeContext = diff.slice(0, 1e4);
6763
6891
  const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
6764
- console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6892
+ console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6765
6893
  console.log(reviewResult);
6766
- console.log(import_chalk10.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6894
+ console.log(import_chalk12.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6767
6895
  return reviewResult;
6768
6896
  }
6769
6897
  /**
6770
6898
  * Review directly from generated file contents (for api mode where git diff is empty).
6771
6899
  */
6772
6900
  async reviewFiles(specContent, filePaths, workingDir, specFile) {
6773
- console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6774
- console.log(import_chalk10.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6901
+ console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6902
+ console.log(import_chalk12.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6775
6903
  console.log(
6776
- import_chalk10.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6904
+ import_chalk12.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6777
6905
  );
6778
6906
  let filesSection = "";
6779
6907
  for (const filePath of filePaths) {
@@ -6794,28 +6922,28 @@ ${content.slice(0, 3e3)}`;
6794
6922
  }
6795
6923
  }
6796
6924
  const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
6797
- console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6925
+ console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6798
6926
  console.log(reviewResult);
6799
- console.log(import_chalk10.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6927
+ console.log(import_chalk12.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6800
6928
  return reviewResult;
6801
6929
  }
6802
6930
  /** Print score trend from history (last N reviews) */
6803
6931
  async printScoreTrend(limit = 5) {
6804
6932
  const history = await loadReviewHistory(this.projectRoot);
6805
6933
  if (history.length === 0) {
6806
- console.log(import_chalk10.default.gray(" No review history yet."));
6934
+ console.log(import_chalk12.default.gray(" No review history yet."));
6807
6935
  return;
6808
6936
  }
6809
6937
  const recent = history.slice(-limit);
6810
- console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6938
+ console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6811
6939
  for (const entry of recent) {
6812
6940
  const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
6813
- const color = entry.score >= 8 ? import_chalk10.default.green : entry.score >= 6 ? import_chalk10.default.yellow : import_chalk10.default.red;
6814
- const impactTag = entry.impactLevel ? import_chalk10.default.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? import_chalk10.default.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? import_chalk10.default.yellow(entry.impactLevel) : import_chalk10.default.green(entry.impactLevel)}`) : "";
6815
- const complexityTag = entry.complexityLevel ? import_chalk10.default.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? import_chalk10.default.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? import_chalk10.default.yellow(entry.complexityLevel) : import_chalk10.default.green(entry.complexityLevel)}`) : "";
6941
+ const color = entry.score >= 8 ? import_chalk12.default.green : entry.score >= 6 ? import_chalk12.default.yellow : import_chalk12.default.red;
6942
+ const impactTag = entry.impactLevel ? import_chalk12.default.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? import_chalk12.default.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? import_chalk12.default.yellow(entry.impactLevel) : import_chalk12.default.green(entry.impactLevel)}`) : "";
6943
+ const complexityTag = entry.complexityLevel ? import_chalk12.default.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? import_chalk12.default.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? import_chalk12.default.yellow(entry.complexityLevel) : import_chalk12.default.green(entry.complexityLevel)}`) : "";
6816
6944
  console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
6817
6945
  }
6818
- console.log(import_chalk10.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6946
+ console.log(import_chalk12.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6819
6947
  }
6820
6948
  };
6821
6949
 
@@ -6865,17 +6993,17 @@ function parseSpecAndTasks(raw) {
6865
6993
  }
6866
6994
 
6867
6995
  // git/worktree.ts
6868
- var import_child_process3 = require("child_process");
6996
+ var import_child_process4 = require("child_process");
6869
6997
  var path9 = __toESM(require("path"));
6870
6998
  var fs13 = __toESM(require("fs-extra"));
6871
- var import_chalk11 = __toESM(require("chalk"));
6999
+ var import_chalk13 = __toESM(require("chalk"));
6872
7000
  var GitWorktreeManager = class {
6873
7001
  constructor(baseDir) {
6874
7002
  this.baseDir = baseDir;
6875
7003
  }
6876
7004
  isGitRepo() {
6877
7005
  try {
6878
- (0, import_child_process3.execSync)("git rev-parse --is-inside-work-tree", { cwd: this.baseDir, stdio: "ignore" });
7006
+ (0, import_child_process4.execSync)("git rev-parse --is-inside-work-tree", { cwd: this.baseDir, stdio: "ignore" });
6879
7007
  return true;
6880
7008
  } catch {
6881
7009
  return false;
@@ -6899,58 +7027,58 @@ var GitWorktreeManager = class {
6899
7027
  if (await fs13.pathExists(dest)) continue;
6900
7028
  try {
6901
7029
  await fs13.ensureSymlink(src, dest, "dir");
6902
- console.log(import_chalk11.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
7030
+ console.log(import_chalk13.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
6903
7031
  } catch (err) {
6904
- console.log(import_chalk11.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
6905
- console.log(import_chalk11.default.yellow(` Run \`npm install\` inside the worktree manually.`));
7032
+ console.log(import_chalk13.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
7033
+ console.log(import_chalk13.default.yellow(` Run \`npm install\` inside the worktree manually.`));
6906
7034
  }
6907
7035
  }
6908
7036
  }
6909
7037
  async createWorktree(idea) {
6910
7038
  if (!this.isGitRepo()) {
6911
- console.log(import_chalk11.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
7039
+ console.log(import_chalk13.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
6912
7040
  return null;
6913
7041
  }
6914
7042
  const featureName = this.sanitizeFeatureName(idea);
6915
7043
  const branchName = `feature/${featureName}`;
6916
7044
  const repoName = path9.basename(this.baseDir);
6917
7045
  const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
6918
- console.log(import_chalk11.default.cyan(`
7046
+ console.log(import_chalk13.default.cyan(`
6919
7047
  --- Setting up Git Worktree ---`));
6920
7048
  if (await fs13.pathExists(worktreePath)) {
6921
- console.log(import_chalk11.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
7049
+ console.log(import_chalk13.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
6922
7050
  await this.linkDependencies(worktreePath);
6923
7051
  return worktreePath;
6924
7052
  }
6925
7053
  try {
6926
7054
  let branchExists = false;
6927
7055
  try {
6928
- (0, import_child_process3.execSync)(`git show-ref --verify refs/heads/${branchName}`, {
7056
+ (0, import_child_process4.execSync)(`git show-ref --verify refs/heads/${branchName}`, {
6929
7057
  cwd: this.baseDir,
6930
7058
  stdio: "ignore"
6931
7059
  });
6932
7060
  branchExists = true;
6933
7061
  } catch {
6934
7062
  }
6935
- console.log(import_chalk11.default.gray(`Creating worktree at: ${worktreePath}`));
7063
+ console.log(import_chalk13.default.gray(`Creating worktree at: ${worktreePath}`));
6936
7064
  if (branchExists) {
6937
- (0, import_child_process3.execSync)(`git worktree add "${worktreePath}" ${branchName}`, {
7065
+ (0, import_child_process4.execSync)(`git worktree add "${worktreePath}" ${branchName}`, {
6938
7066
  cwd: this.baseDir,
6939
7067
  stdio: "inherit"
6940
7068
  });
6941
7069
  } else {
6942
- (0, import_child_process3.execSync)(`git worktree add -b ${branchName} "${worktreePath}"`, {
7070
+ (0, import_child_process4.execSync)(`git worktree add -b ${branchName} "${worktreePath}"`, {
6943
7071
  cwd: this.baseDir,
6944
7072
  stdio: "inherit"
6945
7073
  });
6946
7074
  }
6947
7075
  console.log(
6948
- import_chalk11.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
7076
+ import_chalk13.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
6949
7077
  );
6950
7078
  await this.linkDependencies(worktreePath);
6951
7079
  return worktreePath;
6952
7080
  } catch (error) {
6953
- console.error(import_chalk11.default.red("Failed to create git worktree:"), error);
7081
+ console.error(import_chalk13.default.red("Failed to create git worktree:"), error);
6954
7082
  return null;
6955
7083
  }
6956
7084
  }