ai-spec-dev 0.42.0 → 0.55.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 (70) hide show
  1. package/README.md +86 -40
  2. package/cli/commands/config.ts +129 -1
  3. package/cli/commands/create.ts +246 -11
  4. package/cli/commands/fix-history.ts +176 -0
  5. package/cli/commands/init.ts +344 -106
  6. package/cli/index.ts +3 -7
  7. package/cli/pipeline/helpers.ts +6 -0
  8. package/cli/pipeline/multi-repo.ts +291 -26
  9. package/cli/pipeline/single-repo.ts +103 -2
  10. package/cli/utils.ts +95 -4
  11. package/core/code-generator.ts +63 -14
  12. package/core/config-defaults.ts +44 -0
  13. package/core/constitution-generator.ts +2 -1
  14. package/core/cross-stack-verifier.ts +395 -0
  15. package/core/dsl-extractor.ts +2 -1
  16. package/core/error-feedback.ts +3 -2
  17. package/core/fix-history.ts +333 -0
  18. package/core/import-fixer.ts +827 -0
  19. package/core/import-verifier.ts +569 -0
  20. package/core/knowledge-memory.ts +55 -6
  21. package/core/openapi-exporter.ts +3 -2
  22. package/core/repo-store.ts +95 -0
  23. package/core/reviewer.ts +14 -13
  24. package/core/run-logger.ts +3 -4
  25. package/core/run-snapshot.ts +2 -3
  26. package/core/run-trend.ts +3 -4
  27. package/core/self-evaluator.ts +44 -7
  28. package/core/spec-generator.ts +30 -45
  29. package/core/token-budget.ts +3 -8
  30. package/core/types-generator.ts +2 -2
  31. package/core/vcr.ts +3 -1
  32. package/dist/cli/index.js +3889 -1937
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/index.mjs +3888 -1936
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/index.d.mts +17 -2
  37. package/dist/index.d.ts +17 -2
  38. package/dist/index.js +292 -181
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +292 -181
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +2 -2
  43. package/tests/cross-stack-verifier.test.ts +301 -0
  44. package/tests/fix-history.test.ts +335 -0
  45. package/tests/import-fixer.test.ts +944 -0
  46. package/tests/import-verifier.test.ts +420 -0
  47. package/tests/knowledge-memory.test.ts +40 -0
  48. package/tests/self-evaluator.test.ts +97 -0
  49. package/cli/commands/model.ts +0 -156
  50. package/cli/commands/scan.ts +0 -99
  51. package/cli/commands/workspace.ts +0 -219
  52. package/demo-backend/.ai-spec-constitution.md +0 -65
  53. package/demo-backend/package.json +0 -21
  54. package/demo-backend/prisma/schema.prisma +0 -22
  55. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
  56. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
  57. package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
  58. package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
  59. package/demo-backend/src/index.ts +0 -17
  60. package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
  61. package/demo-backend/src/routes/bookmark.routes.ts +0 -11
  62. package/demo-backend/src/routes/index.ts +0 -8
  63. package/demo-backend/src/services/bookmark.service.test.ts +0 -433
  64. package/demo-backend/src/services/bookmark.service.ts +0 -261
  65. package/demo-backend/tsconfig.json +0 -12
  66. package/demo-frontend/.ai-spec-constitution.md +0 -95
  67. package/demo-frontend/package.json +0 -23
  68. package/demo-frontend/src/App.tsx +0 -12
  69. package/demo-frontend/src/main.tsx +0 -9
  70. package/demo-frontend/tsconfig.json +0 -13
package/dist/index.js CHANGED
@@ -68,7 +68,6 @@ module.exports = __toCommonJS(index_exports);
68
68
  var import_generative_ai = require("@google/generative-ai");
69
69
  var import_sdk = __toESM(require("@anthropic-ai/sdk"));
70
70
  var import_openai = __toESM(require("openai"));
71
- var import_axios = __toESM(require("axios"));
72
71
  var import_undici = require("undici");
73
72
 
74
73
  // prompts/spec.prompt.ts
@@ -361,7 +360,9 @@ var PROVIDER_CATALOG = {
361
360
  displayName: "MiMo (Xiaomi)",
362
361
  description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
363
362
  models: ["mimo-v2-pro"],
364
- envKey: "MIMO_API_KEY"
363
+ envKey: "MIMO_API_KEY",
364
+ // Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
365
+ fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
365
366
  // baseURL not used — MiMo has a dedicated provider class
366
367
  },
367
368
  gemini: {
@@ -404,12 +405,12 @@ var PROVIDER_CATALOG = {
404
405
  },
405
406
  deepseek: {
406
407
  displayName: "DeepSeek",
407
- description: "DeepSeek \u2014 V3 (chat) / R1 (reasoning)",
408
+ description: "DeepSeek V3.2 (chat) / R1 (reasoner) \u2014 alias auto-tracks latest stable",
408
409
  models: [
409
410
  "deepseek-chat",
410
- // DeepSeek-V3
411
+ // V3.2 (alias auto-updates as DeepSeek releases new versions)
411
412
  "deepseek-reasoner"
412
- // DeepSeek-R1
413
+ // R1 (reasoning model)
413
414
  ],
414
415
  envKey: "DEEPSEEK_API_KEY",
415
416
  baseURL: "https://api.deepseek.com/v1"
@@ -530,8 +531,8 @@ var ClaudeProvider = class {
530
531
  ...systemInstruction ? { system: systemInstruction } : {},
531
532
  messages: [{ role: "user", content: prompt }]
532
533
  });
533
- const block = message.content[0];
534
- if (block.type === "text") return block.text;
534
+ const textBlock = message.content.find((b) => b.type === "text");
535
+ if (textBlock) return textBlock.text;
535
536
  throw new Error("Unexpected response type from Claude API");
536
537
  },
537
538
  { label: `${this.providerName}/${this.modelName}` }
@@ -576,43 +577,29 @@ var OpenAICompatibleProvider = class {
576
577
  }
577
578
  };
578
579
  var MiMoProvider = class {
580
+ client;
579
581
  providerName = "mimo";
580
582
  modelName;
581
- apiKey;
582
- baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
583
583
  constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
584
- this.apiKey = apiKey;
584
+ const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
585
+ this.client = new import_sdk.default({ apiKey, baseURL });
585
586
  this.modelName = modelName;
586
587
  }
587
588
  async generate(prompt, systemInstruction) {
588
589
  return withReliability(
589
590
  async () => {
590
- const body = {
591
+ const stream = this.client.messages.stream({
591
592
  model: this.modelName,
592
- max_tokens: 16384,
593
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
594
- top_p: 0.95,
595
- stream: false,
596
- temperature: 1,
597
- stop_sequences: null
598
- };
599
- if (systemInstruction) {
600
- body.system = systemInstruction;
601
- }
602
- const response = await import_axios.default.post(this.baseUrl, body, {
603
- headers: {
604
- "api-key": this.apiKey,
605
- "Content-Type": "application/json"
606
- }
593
+ max_tokens: 65536,
594
+ ...systemInstruction ? { system: systemInstruction } : {},
595
+ messages: [{ role: "user", content: prompt }]
607
596
  });
608
- const data = response.data;
609
- const blocks = data?.content ?? [];
610
- const textBlock = blocks.find((b) => b.type === "text");
611
- if (textBlock?.text) return textBlock.text;
612
- if (data?.stop_reason === "max_tokens") {
613
- throw new Error(`MiMo response truncated (max_tokens reached). The prompt may be too long. Try a shorter spec or switch to a model with larger context.`);
614
- }
615
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
597
+ const message = await stream.finalMessage();
598
+ const textBlock = message.content.find((b) => b.type === "text");
599
+ if (textBlock) return textBlock.text;
600
+ const thinkBlock = message.content.find((b) => b.type === "thinking");
601
+ if (thinkBlock) return thinkBlock.thinking;
602
+ return message.content.map((b) => b.text ?? "").join("");
616
603
  },
617
604
  { label: `${this.providerName}/${this.modelName}` }
618
605
  );
@@ -4246,8 +4233,8 @@ ${currentSpec}`,
4246
4233
  // core/code-generator.ts
4247
4234
  var import_chalk10 = __toESM(require("chalk"));
4248
4235
  var import_child_process2 = require("child_process");
4249
- var path6 = __toESM(require("path"));
4250
- var fs10 = __toESM(require("fs-extra"));
4236
+ var path7 = __toESM(require("path"));
4237
+ var fs11 = __toESM(require("fs-extra"));
4251
4238
 
4252
4239
  // prompts/codegen.prompt.ts
4253
4240
  var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
@@ -4917,32 +4904,32 @@ function validateDsl(raw) {
4917
4904
  }
4918
4905
  return { valid: true, dsl: raw };
4919
4906
  }
4920
- function validateFeature(raw, path10, errors) {
4907
+ function validateFeature(raw, path11, errors) {
4921
4908
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4922
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4909
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4923
4910
  return;
4924
4911
  }
4925
4912
  const f = raw;
4926
- requireNonEmptyString(f["id"], `${path10}.id`, errors);
4927
- requireNonEmptyString(f["title"], `${path10}.title`, errors);
4928
- requireNonEmptyString(f["description"], `${path10}.description`, errors);
4913
+ requireNonEmptyString(f["id"], `${path11}.id`, errors);
4914
+ requireNonEmptyString(f["title"], `${path11}.title`, errors);
4915
+ requireNonEmptyString(f["description"], `${path11}.description`, errors);
4929
4916
  }
4930
- function validateModel(raw, path10, errors) {
4917
+ function validateModel(raw, path11, errors) {
4931
4918
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4932
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4919
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4933
4920
  return;
4934
4921
  }
4935
4922
  const m = raw;
4936
- requireNonEmptyString(m["name"], `${path10}.name`, errors);
4923
+ requireNonEmptyString(m["name"], `${path11}.name`, errors);
4937
4924
  if (!Array.isArray(m["fields"])) {
4938
- errors.push({ path: `${path10}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4925
+ errors.push({ path: `${path11}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4939
4926
  } else {
4940
4927
  const fields = m["fields"];
4941
4928
  if (fields.length > MAX_FIELDS_PER_MODEL) {
4942
- errors.push({ path: `${path10}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4929
+ errors.push({ path: `${path11}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4943
4930
  }
4944
4931
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4945
- validateModelField(fields[j2], `${path10}.fields[${j2}]`, errors);
4932
+ validateModelField(fields[j2], `${path11}.fields[${j2}]`, errors);
4946
4933
  }
4947
4934
  const seenFieldNames = /* @__PURE__ */ new Set();
4948
4935
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
@@ -4951,7 +4938,7 @@ function validateModel(raw, path10, errors) {
4951
4938
  const name = f["name"];
4952
4939
  if (seenFieldNames.has(name)) {
4953
4940
  errors.push({
4954
- path: `${path10}.fields[${j2}].name`,
4941
+ path: `${path11}.fields[${j2}].name`,
4955
4942
  message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
4956
4943
  });
4957
4944
  } else {
@@ -4962,184 +4949,184 @@ function validateModel(raw, path10, errors) {
4962
4949
  }
4963
4950
  if (m["relations"] !== void 0) {
4964
4951
  if (!Array.isArray(m["relations"])) {
4965
- errors.push({ path: `${path10}.relations`, message: "Must be an array of strings if present" });
4952
+ errors.push({ path: `${path11}.relations`, message: "Must be an array of strings if present" });
4966
4953
  } else {
4967
4954
  const rels = m["relations"];
4968
4955
  for (let j2 = 0; j2 < rels.length; j2++) {
4969
4956
  if (typeof rels[j2] !== "string") {
4970
- errors.push({ path: `${path10}.relations[${j2}]`, message: "Must be a string" });
4957
+ errors.push({ path: `${path11}.relations[${j2}]`, message: "Must be a string" });
4971
4958
  }
4972
4959
  }
4973
4960
  }
4974
4961
  }
4975
4962
  }
4976
- function validateModelField(raw, path10, errors) {
4963
+ function validateModelField(raw, path11, errors) {
4977
4964
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4978
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4965
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4979
4966
  return;
4980
4967
  }
4981
4968
  const f = raw;
4982
- requireNonEmptyString(f["name"], `${path10}.name`, errors);
4983
- requireNonEmptyString(f["type"], `${path10}.type`, errors);
4969
+ requireNonEmptyString(f["name"], `${path11}.name`, errors);
4970
+ requireNonEmptyString(f["type"], `${path11}.type`, errors);
4984
4971
  if (typeof f["required"] !== "boolean") {
4985
- errors.push({ path: `${path10}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4972
+ errors.push({ path: `${path11}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4986
4973
  }
4987
4974
  }
4988
- function validateEndpoint(raw, path10, errors) {
4975
+ function validateEndpoint(raw, path11, errors) {
4989
4976
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4990
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4977
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4991
4978
  return;
4992
4979
  }
4993
4980
  const e = raw;
4994
- requireNonEmptyString(e["id"], `${path10}.id`, errors);
4995
- requireNonEmptyString(e["description"], `${path10}.description`, errors);
4981
+ requireNonEmptyString(e["id"], `${path11}.id`, errors);
4982
+ requireNonEmptyString(e["description"], `${path11}.description`, errors);
4996
4983
  if (!VALID_METHODS.includes(e["method"])) {
4997
4984
  errors.push({
4998
- path: `${path10}.method`,
4985
+ path: `${path11}.method`,
4999
4986
  message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
5000
4987
  });
5001
4988
  }
5002
4989
  if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
5003
4990
  errors.push({
5004
- path: `${path10}.path`,
4991
+ path: `${path11}.path`,
5005
4992
  message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
5006
4993
  });
5007
4994
  }
5008
4995
  if (typeof e["auth"] !== "boolean") {
5009
- errors.push({ path: `${path10}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4996
+ errors.push({ path: `${path11}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
5010
4997
  }
5011
4998
  if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
5012
4999
  errors.push({
5013
- path: `${path10}.successStatus`,
5000
+ path: `${path11}.successStatus`,
5014
5001
  message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
5015
5002
  });
5016
5003
  }
5017
- requireNonEmptyString(e["successDescription"], `${path10}.successDescription`, errors);
5004
+ requireNonEmptyString(e["successDescription"], `${path11}.successDescription`, errors);
5018
5005
  if (e["request"] !== void 0) {
5019
- validateRequestSchema(e["request"], `${path10}.request`, errors);
5006
+ validateRequestSchema(e["request"], `${path11}.request`, errors);
5020
5007
  }
5021
5008
  if (e["errors"] !== void 0) {
5022
5009
  if (!Array.isArray(e["errors"])) {
5023
- errors.push({ path: `${path10}.errors`, message: "Must be an array if present" });
5010
+ errors.push({ path: `${path11}.errors`, message: "Must be an array if present" });
5024
5011
  } else {
5025
5012
  const errs = e["errors"];
5026
5013
  if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
5027
- errors.push({ path: `${path10}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
5014
+ errors.push({ path: `${path11}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
5028
5015
  }
5029
5016
  for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
5030
- validateResponseError(errs[j2], `${path10}.errors[${j2}]`, errors);
5017
+ validateResponseError(errs[j2], `${path11}.errors[${j2}]`, errors);
5031
5018
  }
5032
5019
  }
5033
5020
  }
5034
5021
  }
5035
- function validateRequestSchema(raw, path10, errors) {
5022
+ function validateRequestSchema(raw, path11, errors) {
5036
5023
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5037
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
5024
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
5038
5025
  return;
5039
5026
  }
5040
5027
  const r = raw;
5041
5028
  for (const key of ["body", "query", "params"]) {
5042
5029
  if (r[key] !== void 0) {
5043
- validateFieldMap(r[key], `${path10}.${key}`, errors);
5030
+ validateFieldMap(r[key], `${path11}.${key}`, errors);
5044
5031
  }
5045
5032
  }
5046
5033
  }
5047
- function validateFieldMap(raw, path10, errors) {
5034
+ function validateFieldMap(raw, path11, errors) {
5048
5035
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5049
- errors.push({ path: path10, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
5036
+ errors.push({ path: path11, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
5050
5037
  return;
5051
5038
  }
5052
5039
  const map = raw;
5053
5040
  for (const [k2, v2] of Object.entries(map)) {
5054
5041
  if (typeof v2 !== "string") {
5055
- errors.push({ path: `${path10}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
5042
+ errors.push({ path: `${path11}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
5056
5043
  }
5057
5044
  }
5058
5045
  }
5059
- function validateResponseError(raw, path10, errors) {
5046
+ function validateResponseError(raw, path11, errors) {
5060
5047
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5061
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
5048
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
5062
5049
  return;
5063
5050
  }
5064
5051
  const e = raw;
5065
5052
  if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
5066
- errors.push({ path: `${path10}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5053
+ errors.push({ path: `${path11}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5067
5054
  }
5068
- requireNonEmptyString(e["code"], `${path10}.code`, errors);
5069
- requireNonEmptyString(e["description"], `${path10}.description`, errors);
5055
+ requireNonEmptyString(e["code"], `${path11}.code`, errors);
5056
+ requireNonEmptyString(e["description"], `${path11}.description`, errors);
5070
5057
  }
5071
- function validateBehavior(raw, path10, errors) {
5058
+ function validateBehavior(raw, path11, errors) {
5072
5059
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5073
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
5060
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
5074
5061
  return;
5075
5062
  }
5076
5063
  const b = raw;
5077
- requireNonEmptyString(b["id"], `${path10}.id`, errors);
5078
- requireNonEmptyString(b["description"], `${path10}.description`, errors);
5064
+ requireNonEmptyString(b["id"], `${path11}.id`, errors);
5065
+ requireNonEmptyString(b["description"], `${path11}.description`, errors);
5079
5066
  if (b["constraints"] !== void 0) {
5080
5067
  if (!Array.isArray(b["constraints"])) {
5081
- errors.push({ path: `${path10}.constraints`, message: "Must be an array of strings if present" });
5068
+ errors.push({ path: `${path11}.constraints`, message: "Must be an array of strings if present" });
5082
5069
  } else {
5083
5070
  const cs2 = b["constraints"];
5084
5071
  for (let j2 = 0; j2 < cs2.length; j2++) {
5085
5072
  if (typeof cs2[j2] !== "string") {
5086
- errors.push({ path: `${path10}.constraints[${j2}]`, message: "Must be a string" });
5073
+ errors.push({ path: `${path11}.constraints[${j2}]`, message: "Must be a string" });
5087
5074
  }
5088
5075
  }
5089
5076
  }
5090
5077
  }
5091
5078
  }
5092
- function validateComponent(raw, path10, errors) {
5079
+ function validateComponent(raw, path11, errors) {
5093
5080
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5094
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
5081
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
5095
5082
  return;
5096
5083
  }
5097
5084
  const c = raw;
5098
- requireNonEmptyString(c["id"], `${path10}.id`, errors);
5099
- requireNonEmptyString(c["name"], `${path10}.name`, errors);
5100
- requireNonEmptyString(c["description"], `${path10}.description`, errors);
5085
+ requireNonEmptyString(c["id"], `${path11}.id`, errors);
5086
+ requireNonEmptyString(c["name"], `${path11}.name`, errors);
5087
+ requireNonEmptyString(c["description"], `${path11}.description`, errors);
5101
5088
  if (c["props"] !== void 0) {
5102
5089
  if (!Array.isArray(c["props"])) {
5103
- errors.push({ path: `${path10}.props`, message: "Must be an array if present" });
5090
+ errors.push({ path: `${path11}.props`, message: "Must be an array if present" });
5104
5091
  } else {
5105
5092
  const props = c["props"];
5106
5093
  for (let j2 = 0; j2 < props.length; j2++) {
5107
5094
  const p = props[j2];
5108
5095
  if (typeof p !== "object" || p === null) {
5109
- errors.push({ path: `${path10}.props[${j2}]`, message: "Must be an object" });
5096
+ errors.push({ path: `${path11}.props[${j2}]`, message: "Must be an object" });
5110
5097
  continue;
5111
5098
  }
5112
- requireNonEmptyString(p["name"], `${path10}.props[${j2}].name`, errors);
5113
- requireNonEmptyString(p["type"], `${path10}.props[${j2}].type`, errors);
5099
+ requireNonEmptyString(p["name"], `${path11}.props[${j2}].name`, errors);
5100
+ requireNonEmptyString(p["type"], `${path11}.props[${j2}].type`, errors);
5114
5101
  if (typeof p["required"] !== "boolean") {
5115
- errors.push({ path: `${path10}.props[${j2}].required`, message: "Must be boolean" });
5102
+ errors.push({ path: `${path11}.props[${j2}].required`, message: "Must be boolean" });
5116
5103
  }
5117
5104
  }
5118
5105
  }
5119
5106
  }
5120
5107
  if (c["events"] !== void 0) {
5121
5108
  if (!Array.isArray(c["events"])) {
5122
- errors.push({ path: `${path10}.events`, message: "Must be an array if present" });
5109
+ errors.push({ path: `${path11}.events`, message: "Must be an array if present" });
5123
5110
  } else {
5124
5111
  const events = c["events"];
5125
5112
  for (let j2 = 0; j2 < events.length; j2++) {
5126
5113
  const e = events[j2];
5127
5114
  if (typeof e !== "object" || e === null) {
5128
- errors.push({ path: `${path10}.events[${j2}]`, message: "Must be an object" });
5115
+ errors.push({ path: `${path11}.events[${j2}]`, message: "Must be an object" });
5129
5116
  continue;
5130
5117
  }
5131
- requireNonEmptyString(e["name"], `${path10}.events[${j2}].name`, errors);
5118
+ requireNonEmptyString(e["name"], `${path11}.events[${j2}].name`, errors);
5132
5119
  }
5133
5120
  }
5134
5121
  }
5135
5122
  if (c["state"] !== void 0) {
5136
5123
  if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
5137
- errors.push({ path: `${path10}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5124
+ errors.push({ path: `${path11}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5138
5125
  }
5139
5126
  }
5140
5127
  if (c["apiCalls"] !== void 0) {
5141
5128
  if (!Array.isArray(c["apiCalls"])) {
5142
- errors.push({ path: `${path10}.apiCalls`, message: "Must be an array of strings if present" });
5129
+ errors.push({ path: `${path11}.apiCalls`, message: "Must be an array of strings if present" });
5143
5130
  }
5144
5131
  }
5145
5132
  }
@@ -5201,10 +5188,10 @@ function crossReferenceChecks(obj, errors) {
5201
5188
  }
5202
5189
  }
5203
5190
  }
5204
- function requireNonEmptyString(v2, path10, errors) {
5191
+ function requireNonEmptyString(v2, path11, errors) {
5205
5192
  if (typeof v2 !== "string" || v2.trim().length === 0) {
5206
5193
  errors.push({
5207
- path: path10,
5194
+ path: path11,
5208
5195
  message: `Must be a non-empty string, got: ${typeLabel(v2)}`
5209
5196
  });
5210
5197
  }
@@ -5217,13 +5204,11 @@ function typeLabel(v2) {
5217
5204
 
5218
5205
  // core/token-budget.ts
5219
5206
  var import_chalk6 = __toESM(require("chalk"));
5220
- var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5221
- function estimateTokens(text) {
5222
- if (!text) return 0;
5223
- const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5224
- const nonCjkLength = text.length - cjkCount;
5225
- return Math.ceil(cjkCount + nonCjkLength / 4);
5226
- }
5207
+
5208
+ // core/config-defaults.ts
5209
+ var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5210
+ var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
5211
+ var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
5227
5212
  var DEFAULT_TOKEN_BUDGETS = {
5228
5213
  gemini: 9e5,
5229
5214
  claude: 18e4,
@@ -5231,8 +5216,18 @@ var DEFAULT_TOKEN_BUDGETS = {
5231
5216
  deepseek: 6e4,
5232
5217
  default: 1e5
5233
5218
  };
5219
+
5220
+ // core/token-budget.ts
5221
+ var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5222
+ function estimateTokens(text) {
5223
+ if (!text) return 0;
5224
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5225
+ const nonCjkLength = text.length - cjkCount;
5226
+ return Math.ceil(cjkCount + nonCjkLength / 4);
5227
+ }
5228
+ var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
5234
5229
  function getDefaultBudget(providerName) {
5235
- return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
5230
+ return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
5236
5231
  }
5237
5232
 
5238
5233
  // core/dsl-extractor.ts
@@ -6085,6 +6080,101 @@ function printTaskProgress(completed, total, task, mode) {
6085
6080
  }
6086
6081
  }
6087
6082
 
6083
+ // core/fix-history.ts
6084
+ var fs10 = __toESM(require("fs-extra"));
6085
+ var path6 = __toESM(require("path"));
6086
+ var FIX_HISTORY_FILE = ".ai-spec-fix-history.json";
6087
+ var FIX_HISTORY_VERSION = "1.0";
6088
+ async function loadFixHistory(repoRoot) {
6089
+ const filePath = path6.join(repoRoot, FIX_HISTORY_FILE);
6090
+ if (!await fs10.pathExists(filePath)) {
6091
+ return { version: FIX_HISTORY_VERSION, entries: [] };
6092
+ }
6093
+ try {
6094
+ const data = await fs10.readJson(filePath);
6095
+ if (!data || typeof data !== "object" || !Array.isArray(data.entries)) {
6096
+ return { version: FIX_HISTORY_VERSION, entries: [] };
6097
+ }
6098
+ return {
6099
+ version: typeof data.version === "string" ? data.version : FIX_HISTORY_VERSION,
6100
+ entries: data.entries
6101
+ };
6102
+ } catch {
6103
+ return { version: FIX_HISTORY_VERSION, entries: [] };
6104
+ }
6105
+ }
6106
+ function aggregateFixPatterns(history) {
6107
+ const byKey = /* @__PURE__ */ new Map();
6108
+ for (const entry of history.entries) {
6109
+ if (!byKey.has(entry.patternKey)) byKey.set(entry.patternKey, []);
6110
+ byKey.get(entry.patternKey).push(entry);
6111
+ }
6112
+ const aggregates = [];
6113
+ for (const [patternKey, entries] of byKey) {
6114
+ entries.sort((a, b) => a.ts.localeCompare(b.ts));
6115
+ const first = entries[0];
6116
+ const last = entries[entries.length - 1];
6117
+ const uniqueRunIds = new Set(entries.map((e) => e.runId)).size;
6118
+ aggregates.push({
6119
+ patternKey,
6120
+ count: entries.length,
6121
+ firstSeen: first.ts,
6122
+ lastSeen: last.ts,
6123
+ uniqueRunIds,
6124
+ source: last.brokenImport.source,
6125
+ names: last.brokenImport.names,
6126
+ reason: last.brokenImport.reason,
6127
+ fix: {
6128
+ kind: last.fix.kind,
6129
+ target: last.fix.target,
6130
+ stage: last.fix.stage
6131
+ }
6132
+ });
6133
+ }
6134
+ aggregates.sort((a, b) => {
6135
+ if (b.count !== a.count) return b.count - a.count;
6136
+ return b.lastSeen.localeCompare(a.lastSeen);
6137
+ });
6138
+ return aggregates;
6139
+ }
6140
+ function buildHallucinationAvoidanceSection(history, opts = {}) {
6141
+ const minCount = opts.minCount ?? 1;
6142
+ const maxItems = opts.maxItems ?? 10;
6143
+ const patterns = aggregateFixPatterns(history).filter((p) => p.count >= minCount);
6144
+ if (patterns.length === 0) return null;
6145
+ const top = patterns.slice(0, maxItems);
6146
+ const lines = [
6147
+ "=== Prior Hallucinations in This Project (DO NOT REPEAT) ===",
6148
+ "",
6149
+ "The following imports were previously hallucinated by AI codegen in this",
6150
+ "project and had to be auto-fixed. When generating new files, actively avoid",
6151
+ "these exact imports \u2014 they were wrong in the past and will be wrong again.",
6152
+ ""
6153
+ ];
6154
+ for (const p of top) {
6155
+ const namesLabel = p.names.length > 0 ? `{ ${p.names.join(", ")} }` : "(no names)";
6156
+ const reasonLabel = p.reason === "file_not_found" ? "file did not exist" : "named export did not exist";
6157
+ const countLabel = p.count === 1 ? "1x" : `${p.count}x`;
6158
+ const dateLabel = p.lastSeen.slice(0, 10);
6159
+ lines.push(`\u274C Do NOT: import ${namesLabel} from '${p.source}'`);
6160
+ lines.push(` Reason: ${reasonLabel} (seen ${countLabel}, last ${dateLabel})`);
6161
+ if (p.fix.kind === "create_file") {
6162
+ lines.push(` Previously fixed by creating: ${p.fix.target}`);
6163
+ } else if (p.fix.kind === "rewrite_import") {
6164
+ lines.push(` Previously fixed by rewriting the import path`);
6165
+ } else {
6166
+ lines.push(` Previously fixed by appending to: ${p.fix.target}`);
6167
+ }
6168
+ lines.push("");
6169
+ }
6170
+ if (patterns.length > maxItems) {
6171
+ lines.push(`(${patterns.length - maxItems} more pattern(s) hidden \u2014 run \`ai-spec fix-history\` to see all)`);
6172
+ lines.push("");
6173
+ }
6174
+ lines.push("=== End of Prior Hallucinations ===");
6175
+ return lines.join("\n");
6176
+ }
6177
+
6088
6178
  // core/code-generator.ts
6089
6179
  var CodeGenerator = class {
6090
6180
  constructor(provider, mode = "claude-code") {
@@ -6149,8 +6239,8 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
6149
6239
  Files: ${t.filesToTouch.join(", ")}
6150
6240
  Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
6151
6241
  const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
6152
- const promptFile = path6.join(workingDir, ".claude-prompt.txt");
6153
- await fs10.writeFile(promptFile, promptContent, "utf-8");
6242
+ const promptFile = path7.join(workingDir, ".claude-prompt.txt");
6243
+ await fs11.writeFile(promptFile, promptContent, "utf-8");
6154
6244
  if (options.auto) {
6155
6245
  console.log(import_chalk10.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
6156
6246
  console.log(import_chalk10.default.gray(` Spec: ${specFilePath}`));
@@ -6243,7 +6333,7 @@ Implement ONLY this task. Do not implement other tasks.`;
6243
6333
  if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
6244
6334
  console.log(import_chalk10.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
6245
6335
  }
6246
- const spec = await fs10.readFile(specFilePath, "utf-8");
6336
+ const spec = await fs11.readFile(specFilePath, "utf-8");
6247
6337
  let constitutionSection = context?.constitution ? `
6248
6338
  === Project Constitution (MUST follow) ===
6249
6339
  ${context.constitution}
@@ -6270,7 +6360,24 @@ ${buildFrontendContextSection(fctx)}
6270
6360
  `;
6271
6361
  console.log(import_chalk10.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6272
6362
  }
6273
- const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
6363
+ let fixHistorySection = "";
6364
+ if (options.injectFixHistory !== false) {
6365
+ try {
6366
+ const history = await loadFixHistory(workingDir);
6367
+ const section = buildHallucinationAvoidanceSection(history, {
6368
+ maxItems: options.fixHistoryInjectMax ?? 10
6369
+ });
6370
+ if (section) {
6371
+ fixHistorySection = `
6372
+ ${section}
6373
+ `;
6374
+ const patternCount = (section.match(/❌ Do NOT/g) ?? []).length;
6375
+ console.log(import_chalk10.default.cyan(` \u2713 Injected ${patternCount} prior hallucination pattern(s) from fix-history`));
6376
+ }
6377
+ } catch {
6378
+ }
6379
+ }
6380
+ const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection + fixHistorySection;
6274
6381
  const estimatedTokenCount = estimateTokens(allContextText);
6275
6382
  const budget = getDefaultBudget(this.provider.providerName);
6276
6383
  if (estimatedTokenCount > budget * 0.7) {
@@ -6289,7 +6396,7 @@ ${buildFrontendContextSection(fctx)}
6289
6396
  }
6290
6397
  const tasks = await loadTasksForSpec(specFilePath);
6291
6398
  if (tasks && tasks.length > 0) {
6292
- return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
6399
+ return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection + fixHistorySection, frontendSection, sharedConfigSection, options, systemPrompt, context);
6293
6400
  }
6294
6401
  console.log(import_chalk10.default.gray(" [1/2] Planning implementation files..."));
6295
6402
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
@@ -6300,7 +6407,7 @@ IMPORTANT: Check the "Frontend Project Context" section below. Extend existing h
6300
6407
 
6301
6408
  === Feature Spec ===
6302
6409
  ${spec}
6303
- ${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
6410
+ ${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}${fixHistorySection}
6304
6411
  === Project Context ===
6305
6412
  ${contextSummary}
6306
6413
 
@@ -6327,7 +6434,7 @@ Output ONLY a valid JSON array:
6327
6434
  const icon = item.action === "create" ? import_chalk10.default.green("+") : import_chalk10.default.yellow("~");
6328
6435
  console.log(` ${icon} ${item.file}: ${import_chalk10.default.gray(item.description)}`);
6329
6436
  });
6330
- const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
6437
+ const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection + fixHistorySection, systemPrompt);
6331
6438
  return files;
6332
6439
  }
6333
6440
  async runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection, frontendSection = "", sharedConfigSection = "", options = {}, systemPrompt = getCodeGenSystemPrompt(), context) {
@@ -6387,7 +6494,7 @@ Output ONLY a valid JSON array:
6387
6494
  }
6388
6495
  const filePlan = await Promise.all(
6389
6496
  task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
6390
- const exists = await fs10.pathExists(path6.join(workingDir, f));
6497
+ const exists = await fs11.pathExists(path7.join(workingDir, f));
6391
6498
  return {
6392
6499
  file: f,
6393
6500
  action: exists ? "modify" : "create",
@@ -6427,7 +6534,7 @@ ${taskContext}`,
6427
6534
  const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
6428
6535
  if (isCodeFile || isViewFile) {
6429
6536
  try {
6430
- const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path6.join(workingDir, writtenFile), "utf-8");
6537
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs11.readFile(path7.join(workingDir, writtenFile), "utf-8");
6431
6538
  generatedFileCache.set(writtenFile, content);
6432
6539
  } catch {
6433
6540
  }
@@ -6437,19 +6544,28 @@ ${taskContext}`,
6437
6544
  };
6438
6545
  const taskBatches = topoSortLayerTasks(layerTasks);
6439
6546
  const layerResults = [];
6547
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? 3);
6440
6548
  for (const batch of taskBatches) {
6441
6549
  const batchIsParallel = batch.length > 1;
6442
- const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
6443
- const settled = await Promise.allSettled(batchResultPromises);
6444
6550
  const batchResults = [];
6445
- for (let i = 0; i < settled.length; i++) {
6446
- const outcome = settled[i];
6447
- if (outcome.status === "fulfilled") {
6448
- batchResults.push(outcome.value);
6449
- } else {
6450
- const task = batch[i];
6451
- console.log(import_chalk10.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
6452
- batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
6551
+ for (let chunkStart = 0; chunkStart < batch.length; chunkStart += maxConcurrency) {
6552
+ const chunk = batch.slice(chunkStart, chunkStart + maxConcurrency);
6553
+ if (batchIsParallel && batch.length > maxConcurrency) {
6554
+ const chunkIdx = Math.floor(chunkStart / maxConcurrency) + 1;
6555
+ const totalChunks = Math.ceil(batch.length / maxConcurrency);
6556
+ console.log(import_chalk10.default.gray(` \u21B3 chunk ${chunkIdx}/${totalChunks} (${chunk.length} tasks, concurrency cap: ${maxConcurrency})`));
6557
+ }
6558
+ const chunkResultPromises = chunk.map((task) => executeTask(task, batchIsParallel));
6559
+ const settled = await Promise.allSettled(chunkResultPromises);
6560
+ for (let i = 0; i < settled.length; i++) {
6561
+ const outcome = settled[i];
6562
+ if (outcome.status === "fulfilled") {
6563
+ batchResults.push(outcome.value);
6564
+ } else {
6565
+ const task = chunk[i];
6566
+ console.log(import_chalk10.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
6567
+ batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
6568
+ }
6453
6569
  }
6454
6570
  }
6455
6571
  layerResults.push(...batchResults);
@@ -6479,7 +6595,7 @@ ${taskContext}`,
6479
6595
  const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
6480
6596
  for (const sharedFile of context.sharedConfigFiles) {
6481
6597
  if (processedSharedConfigs.has(sharedFile.path)) continue;
6482
- const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path6.basename(f).replace(/\.[jt]sx?$/, ""));
6598
+ const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path7.basename(f).replace(/\.[jt]sx?$/, ""));
6483
6599
  if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
6484
6600
  let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
6485
6601
  if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
@@ -6519,10 +6635,10 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
6519
6635
  let successCount = 0;
6520
6636
  const writtenFiles = [];
6521
6637
  for (const item of filePlan) {
6522
- const fullPath = path6.join(workingDir, item.file);
6638
+ const fullPath = path7.join(workingDir, item.file);
6523
6639
  let existingContent = "";
6524
- if (await fs10.pathExists(fullPath)) {
6525
- existingContent = await fs10.readFile(fullPath, "utf-8");
6640
+ if (await fs11.pathExists(fullPath)) {
6641
+ existingContent = await fs11.readFile(fullPath, "utf-8");
6526
6642
  }
6527
6643
  const codePrompt = `Implement this file.
6528
6644
 
@@ -6539,8 +6655,8 @@ ${existingContent || "Output only the complete file content."}`;
6539
6655
  const raw = await this.provider.generate(codePrompt, systemPrompt);
6540
6656
  const fileContent = stripCodeFences(raw);
6541
6657
  await getActiveSnapshot()?.snapshotFile(fullPath);
6542
- await fs10.ensureDir(path6.dirname(fullPath));
6543
- await fs10.writeFile(fullPath, fileContent, "utf-8");
6658
+ await fs11.ensureDir(path7.dirname(fullPath));
6659
+ await fs11.writeFile(fullPath, fileContent, "utf-8");
6544
6660
  getActiveLogger()?.fileWritten(item.file);
6545
6661
  fileSpinner.succeed(`${existingContent ? import_chalk10.default.yellow("~") : import_chalk10.default.green("+")} ${import_chalk10.default.bold(item.file)}`);
6546
6662
  successCount++;
@@ -6561,7 +6677,7 @@ ${existingContent || "Output only the complete file content."}`;
6561
6677
  // ── Mode: plan ─────────────────────────────────────────────────────────────
6562
6678
  async runPlanMode(specFilePath) {
6563
6679
  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"));
6564
- const spec = await fs10.readFile(specFilePath, "utf-8");
6680
+ const spec = await fs11.readFile(specFilePath, "utf-8");
6565
6681
  const plan = await this.provider.generate(
6566
6682
  `Create a detailed, step-by-step implementation plan for the following feature spec.
6567
6683
  Be specific about:
@@ -6580,13 +6696,13 @@ ${spec}`,
6580
6696
  // core/reviewer.ts
6581
6697
  var import_chalk12 = __toESM(require("chalk"));
6582
6698
  var import_child_process3 = require("child_process");
6583
- var path8 = __toESM(require("path"));
6584
- var fs12 = __toESM(require("fs-extra"));
6699
+ var path9 = __toESM(require("path"));
6700
+ var fs13 = __toESM(require("fs-extra"));
6585
6701
 
6586
6702
  // core/constitution-generator.ts
6587
6703
  var import_chalk11 = __toESM(require("chalk"));
6588
- var fs11 = __toESM(require("fs-extra"));
6589
- var path7 = __toESM(require("path"));
6704
+ var fs12 = __toESM(require("fs-extra"));
6705
+ var path8 = __toESM(require("path"));
6590
6706
 
6591
6707
  // prompts/constitution.prompt.ts
6592
6708
  var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
@@ -6666,8 +6782,8 @@ var ConstitutionGenerator = class {
6666
6782
  return this.provider.generate(prompt, constitutionSystemPrompt);
6667
6783
  }
6668
6784
  async saveConstitution(projectRoot, content) {
6669
- const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6670
- await fs11.writeFile(filePath, content, "utf-8");
6785
+ const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6786
+ await fs12.writeFile(filePath, content, "utf-8");
6671
6787
  return filePath;
6672
6788
  }
6673
6789
  };
@@ -6693,7 +6809,7 @@ ${context.routeSummary}
6693
6809
  }
6694
6810
  if (context.schema) {
6695
6811
  parts.push(`=== Prisma Schema ===
6696
- ${context.schema.slice(0, 4e3)}
6812
+ ${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
6697
6813
  `);
6698
6814
  }
6699
6815
  if (context.errorPatterns) {
@@ -6725,9 +6841,9 @@ ${sections.join("\n")}
6725
6841
  return parts.join("\n");
6726
6842
  }
6727
6843
  async function loadConstitution(projectRoot) {
6728
- const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6729
- if (await fs11.pathExists(filePath)) {
6730
- return fs11.readFile(filePath, "utf-8");
6844
+ const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6845
+ if (await fs12.pathExists(filePath)) {
6846
+ return fs12.readFile(filePath, "utf-8");
6731
6847
  }
6732
6848
  return void 0;
6733
6849
  }
@@ -6743,10 +6859,10 @@ function printConstitutionHint(exists) {
6743
6859
 
6744
6860
  // core/reviewer.ts
6745
6861
  async function loadAccumulatedLessons(projectRoot) {
6746
- const constitutionPath = path8.join(projectRoot, CONSTITUTION_FILE);
6862
+ const constitutionPath = path9.join(projectRoot, CONSTITUTION_FILE);
6747
6863
  let content;
6748
6864
  try {
6749
- content = await fs12.readFile(constitutionPath, "utf-8");
6865
+ content = await fs13.readFile(constitutionPath, "utf-8");
6750
6866
  } catch {
6751
6867
  return null;
6752
6868
  }
@@ -6757,23 +6873,23 @@ async function loadAccumulatedLessons(projectRoot) {
6757
6873
  const nextSection = section.slice(marker.length).match(/\n## \d/);
6758
6874
  return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
6759
6875
  }
6760
- var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
6876
+ var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
6761
6877
  async function loadReviewHistory(projectRoot) {
6762
- const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6878
+ const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
6763
6879
  try {
6764
- if (await fs12.pathExists(historyPath)) {
6765
- return await fs12.readJson(historyPath);
6880
+ if (await fs13.pathExists(historyPath)) {
6881
+ return await fs13.readJson(historyPath);
6766
6882
  }
6767
6883
  } catch {
6768
6884
  }
6769
6885
  return [];
6770
6886
  }
6771
6887
  async function appendReviewHistory(projectRoot, entry) {
6772
- const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6888
+ const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
6773
6889
  const existing = await loadReviewHistory(projectRoot);
6774
6890
  const updated = [...existing, entry].slice(-20);
6775
6891
  try {
6776
- await fs12.writeJson(historyPath, updated, { spaces: 2 });
6892
+ await fs13.writeJson(historyPath, updated, { spaces: 2 });
6777
6893
  } catch {
6778
6894
  }
6779
6895
  }
@@ -6807,7 +6923,7 @@ function buildHistoryContext(history) {
6807
6923
  const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
6808
6924
  for (const entry of recent) {
6809
6925
  lines.push(`
6810
- [${entry.date}] ${path8.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6926
+ [${entry.date}] ${path9.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6811
6927
  entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
6812
6928
  }
6813
6929
  return lines.join("\n") + "\n";
@@ -6888,15 +7004,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6888
7004
  ${codeContext}`;
6889
7005
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6890
7006
  console.log(import_chalk12.default.gray(" Pass 2/3: Implementation review..."));
7007
+ const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
6891
7008
  const history = await loadReviewHistory(this.projectRoot);
6892
7009
  const historyContext = buildHistoryContext(history);
6893
7010
  const implPrompt = `Review the implementation details of this change.
6894
7011
 
6895
- === Feature Spec ===
6896
- ${specContent || "(No spec \u2014 review for general code quality)"}
6897
-
6898
- === Code ===
6899
- ${codeContext}
7012
+ === Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
7013
+ ${specDigest}
6900
7014
 
6901
7015
  === Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
6902
7016
  ${archReview}
@@ -6905,11 +7019,8 @@ ${historyContext}`;
6905
7019
  console.log(import_chalk12.default.gray(" Pass 3/3: Impact & complexity assessment..."));
6906
7020
  const impactPrompt = `Assess the impact and complexity of this change.
6907
7021
 
6908
- === Feature Spec ===
6909
- ${specContent || "(No spec \u2014 review for general code quality)"}
6910
-
6911
- === Code ===
6912
- ${codeContext}
7022
+ === Feature Spec (digest) ===
7023
+ ${specDigest}
6913
7024
 
6914
7025
  === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
6915
7026
  ${archReview}
@@ -6932,7 +7043,7 @@ ${sep}
6932
7043
  if (score > 0 && specFile) {
6933
7044
  await appendReviewHistory(this.projectRoot, {
6934
7045
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
6935
- specFile: path8.relative(this.projectRoot, specFile),
7046
+ specFile: path9.relative(this.projectRoot, specFile),
6936
7047
  score,
6937
7048
  ...complianceScore > 0 ? { complianceScore } : {},
6938
7049
  topIssues,
@@ -6977,14 +7088,14 @@ ${sep}
6977
7088
  );
6978
7089
  let filesSection = "";
6979
7090
  for (const filePath of filePaths) {
6980
- const fullPath = path8.join(workingDir, filePath);
7091
+ const fullPath = path9.join(workingDir, filePath);
6981
7092
  try {
6982
- const content = await fs12.readFile(fullPath, "utf-8");
7093
+ const content = await fs13.readFile(fullPath, "utf-8");
6983
7094
  filesSection += `
6984
7095
 
6985
7096
  === ${filePath} ===
6986
- ${content.slice(0, 3e3)}`;
6987
- if (content.length > 3e3) filesSection += `
7097
+ ${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
7098
+ if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
6988
7099
  ... (truncated, ${content.length} chars total)`;
6989
7100
  } catch {
6990
7101
  filesSection += `
@@ -7013,7 +7124,7 @@ ${content.slice(0, 3e3)}`;
7013
7124
  const color = entry.score >= 8 ? import_chalk12.default.green : entry.score >= 6 ? import_chalk12.default.yellow : import_chalk12.default.red;
7014
7125
  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)}`) : "";
7015
7126
  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)}`) : "";
7016
- console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
7127
+ console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path9.basename(entry.specFile)}`);
7017
7128
  }
7018
7129
  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"));
7019
7130
  }
@@ -7066,8 +7177,8 @@ function parseSpecAndTasks(raw) {
7066
7177
 
7067
7178
  // git/worktree.ts
7068
7179
  var import_child_process4 = require("child_process");
7069
- var path9 = __toESM(require("path"));
7070
- var fs13 = __toESM(require("fs-extra"));
7180
+ var path10 = __toESM(require("path"));
7181
+ var fs14 = __toESM(require("fs-extra"));
7071
7182
  var import_chalk13 = __toESM(require("chalk"));
7072
7183
  var GitWorktreeManager = class {
7073
7184
  constructor(baseDir) {
@@ -7093,12 +7204,12 @@ var GitWorktreeManager = class {
7093
7204
  async linkDependencies(worktreePath) {
7094
7205
  const candidates = ["node_modules", "vendor"];
7095
7206
  for (const dir of candidates) {
7096
- const src = path9.join(this.baseDir, dir);
7097
- const dest = path9.join(worktreePath, dir);
7098
- if (!await fs13.pathExists(src)) continue;
7099
- if (await fs13.pathExists(dest)) continue;
7207
+ const src = path10.join(this.baseDir, dir);
7208
+ const dest = path10.join(worktreePath, dir);
7209
+ if (!await fs14.pathExists(src)) continue;
7210
+ if (await fs14.pathExists(dest)) continue;
7100
7211
  try {
7101
- await fs13.ensureSymlink(src, dest, "dir");
7212
+ await fs14.ensureSymlink(src, dest, "dir");
7102
7213
  console.log(import_chalk13.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
7103
7214
  } catch (err) {
7104
7215
  console.log(import_chalk13.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
@@ -7113,11 +7224,11 @@ var GitWorktreeManager = class {
7113
7224
  }
7114
7225
  const featureName = this.sanitizeFeatureName(idea);
7115
7226
  const branchName = `feature/${featureName}`;
7116
- const repoName = path9.basename(this.baseDir);
7117
- const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
7227
+ const repoName = path10.basename(this.baseDir);
7228
+ const worktreePath = path10.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
7118
7229
  console.log(import_chalk13.default.cyan(`
7119
7230
  --- Setting up Git Worktree ---`));
7120
- if (await fs13.pathExists(worktreePath)) {
7231
+ if (await fs14.pathExists(worktreePath)) {
7121
7232
  console.log(import_chalk13.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
7122
7233
  await this.linkDependencies(worktreePath);
7123
7234
  return worktreePath;