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.mjs CHANGED
@@ -2,7 +2,6 @@
2
2
  import { GoogleGenerativeAI } from "@google/generative-ai";
3
3
  import Anthropic from "@anthropic-ai/sdk";
4
4
  import OpenAI from "openai";
5
- import axios from "axios";
6
5
  import { ProxyAgent } from "undici";
7
6
 
8
7
  // prompts/spec.prompt.ts
@@ -295,7 +294,9 @@ var PROVIDER_CATALOG = {
295
294
  displayName: "MiMo (Xiaomi)",
296
295
  description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
297
296
  models: ["mimo-v2-pro"],
298
- envKey: "MIMO_API_KEY"
297
+ envKey: "MIMO_API_KEY",
298
+ // Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
299
+ fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
299
300
  // baseURL not used — MiMo has a dedicated provider class
300
301
  },
301
302
  gemini: {
@@ -338,12 +339,12 @@ var PROVIDER_CATALOG = {
338
339
  },
339
340
  deepseek: {
340
341
  displayName: "DeepSeek",
341
- description: "DeepSeek \u2014 V3 (chat) / R1 (reasoning)",
342
+ description: "DeepSeek V3.2 (chat) / R1 (reasoner) \u2014 alias auto-tracks latest stable",
342
343
  models: [
343
344
  "deepseek-chat",
344
- // DeepSeek-V3
345
+ // V3.2 (alias auto-updates as DeepSeek releases new versions)
345
346
  "deepseek-reasoner"
346
- // DeepSeek-R1
347
+ // R1 (reasoning model)
347
348
  ],
348
349
  envKey: "DEEPSEEK_API_KEY",
349
350
  baseURL: "https://api.deepseek.com/v1"
@@ -464,8 +465,8 @@ var ClaudeProvider = class {
464
465
  ...systemInstruction ? { system: systemInstruction } : {},
465
466
  messages: [{ role: "user", content: prompt }]
466
467
  });
467
- const block = message.content[0];
468
- if (block.type === "text") return block.text;
468
+ const textBlock = message.content.find((b) => b.type === "text");
469
+ if (textBlock) return textBlock.text;
469
470
  throw new Error("Unexpected response type from Claude API");
470
471
  },
471
472
  { label: `${this.providerName}/${this.modelName}` }
@@ -510,43 +511,29 @@ var OpenAICompatibleProvider = class {
510
511
  }
511
512
  };
512
513
  var MiMoProvider = class {
514
+ client;
513
515
  providerName = "mimo";
514
516
  modelName;
515
- apiKey;
516
- baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
517
517
  constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
518
- this.apiKey = apiKey;
518
+ const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
519
+ this.client = new Anthropic({ apiKey, baseURL });
519
520
  this.modelName = modelName;
520
521
  }
521
522
  async generate(prompt, systemInstruction) {
522
523
  return withReliability(
523
524
  async () => {
524
- const body = {
525
+ const stream = this.client.messages.stream({
525
526
  model: this.modelName,
526
- max_tokens: 16384,
527
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
528
- top_p: 0.95,
529
- stream: false,
530
- temperature: 1,
531
- stop_sequences: null
532
- };
533
- if (systemInstruction) {
534
- body.system = systemInstruction;
535
- }
536
- const response = await axios.post(this.baseUrl, body, {
537
- headers: {
538
- "api-key": this.apiKey,
539
- "Content-Type": "application/json"
540
- }
527
+ max_tokens: 65536,
528
+ ...systemInstruction ? { system: systemInstruction } : {},
529
+ messages: [{ role: "user", content: prompt }]
541
530
  });
542
- const data = response.data;
543
- const blocks = data?.content ?? [];
544
- const textBlock = blocks.find((b) => b.type === "text");
545
- if (textBlock?.text) return textBlock.text;
546
- if (data?.stop_reason === "max_tokens") {
547
- 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.`);
548
- }
549
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
531
+ const message = await stream.finalMessage();
532
+ const textBlock = message.content.find((b) => b.type === "text");
533
+ if (textBlock) return textBlock.text;
534
+ const thinkBlock = message.content.find((b) => b.type === "thinking");
535
+ if (thinkBlock) return thinkBlock.thinking;
536
+ return message.content.map((b) => b.text ?? "").join("");
550
537
  },
551
538
  { label: `${this.providerName}/${this.modelName}` }
552
539
  );
@@ -4180,8 +4167,8 @@ ${currentSpec}`,
4180
4167
  // core/code-generator.ts
4181
4168
  import chalk10 from "chalk";
4182
4169
  import { execSync as execSync2, spawnSync } from "child_process";
4183
- import * as path6 from "path";
4184
- import * as fs10 from "fs-extra";
4170
+ import * as path7 from "path";
4171
+ import * as fs11 from "fs-extra";
4185
4172
 
4186
4173
  // prompts/codegen.prompt.ts
4187
4174
  var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
@@ -4851,32 +4838,32 @@ function validateDsl(raw) {
4851
4838
  }
4852
4839
  return { valid: true, dsl: raw };
4853
4840
  }
4854
- function validateFeature(raw, path10, errors) {
4841
+ function validateFeature(raw, path11, errors) {
4855
4842
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4856
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4843
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4857
4844
  return;
4858
4845
  }
4859
4846
  const f = raw;
4860
- requireNonEmptyString(f["id"], `${path10}.id`, errors);
4861
- requireNonEmptyString(f["title"], `${path10}.title`, errors);
4862
- requireNonEmptyString(f["description"], `${path10}.description`, errors);
4847
+ requireNonEmptyString(f["id"], `${path11}.id`, errors);
4848
+ requireNonEmptyString(f["title"], `${path11}.title`, errors);
4849
+ requireNonEmptyString(f["description"], `${path11}.description`, errors);
4863
4850
  }
4864
- function validateModel(raw, path10, errors) {
4851
+ function validateModel(raw, path11, errors) {
4865
4852
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4866
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4853
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4867
4854
  return;
4868
4855
  }
4869
4856
  const m = raw;
4870
- requireNonEmptyString(m["name"], `${path10}.name`, errors);
4857
+ requireNonEmptyString(m["name"], `${path11}.name`, errors);
4871
4858
  if (!Array.isArray(m["fields"])) {
4872
- errors.push({ path: `${path10}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4859
+ errors.push({ path: `${path11}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4873
4860
  } else {
4874
4861
  const fields = m["fields"];
4875
4862
  if (fields.length > MAX_FIELDS_PER_MODEL) {
4876
- errors.push({ path: `${path10}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4863
+ errors.push({ path: `${path11}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4877
4864
  }
4878
4865
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4879
- validateModelField(fields[j2], `${path10}.fields[${j2}]`, errors);
4866
+ validateModelField(fields[j2], `${path11}.fields[${j2}]`, errors);
4880
4867
  }
4881
4868
  const seenFieldNames = /* @__PURE__ */ new Set();
4882
4869
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
@@ -4885,7 +4872,7 @@ function validateModel(raw, path10, errors) {
4885
4872
  const name = f["name"];
4886
4873
  if (seenFieldNames.has(name)) {
4887
4874
  errors.push({
4888
- path: `${path10}.fields[${j2}].name`,
4875
+ path: `${path11}.fields[${j2}].name`,
4889
4876
  message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
4890
4877
  });
4891
4878
  } else {
@@ -4896,184 +4883,184 @@ function validateModel(raw, path10, errors) {
4896
4883
  }
4897
4884
  if (m["relations"] !== void 0) {
4898
4885
  if (!Array.isArray(m["relations"])) {
4899
- errors.push({ path: `${path10}.relations`, message: "Must be an array of strings if present" });
4886
+ errors.push({ path: `${path11}.relations`, message: "Must be an array of strings if present" });
4900
4887
  } else {
4901
4888
  const rels = m["relations"];
4902
4889
  for (let j2 = 0; j2 < rels.length; j2++) {
4903
4890
  if (typeof rels[j2] !== "string") {
4904
- errors.push({ path: `${path10}.relations[${j2}]`, message: "Must be a string" });
4891
+ errors.push({ path: `${path11}.relations[${j2}]`, message: "Must be a string" });
4905
4892
  }
4906
4893
  }
4907
4894
  }
4908
4895
  }
4909
4896
  }
4910
- function validateModelField(raw, path10, errors) {
4897
+ function validateModelField(raw, path11, errors) {
4911
4898
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4912
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4899
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4913
4900
  return;
4914
4901
  }
4915
4902
  const f = raw;
4916
- requireNonEmptyString(f["name"], `${path10}.name`, errors);
4917
- requireNonEmptyString(f["type"], `${path10}.type`, errors);
4903
+ requireNonEmptyString(f["name"], `${path11}.name`, errors);
4904
+ requireNonEmptyString(f["type"], `${path11}.type`, errors);
4918
4905
  if (typeof f["required"] !== "boolean") {
4919
- errors.push({ path: `${path10}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4906
+ errors.push({ path: `${path11}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4920
4907
  }
4921
4908
  }
4922
- function validateEndpoint(raw, path10, errors) {
4909
+ function validateEndpoint(raw, path11, errors) {
4923
4910
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4924
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4911
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4925
4912
  return;
4926
4913
  }
4927
4914
  const e = raw;
4928
- requireNonEmptyString(e["id"], `${path10}.id`, errors);
4929
- requireNonEmptyString(e["description"], `${path10}.description`, errors);
4915
+ requireNonEmptyString(e["id"], `${path11}.id`, errors);
4916
+ requireNonEmptyString(e["description"], `${path11}.description`, errors);
4930
4917
  if (!VALID_METHODS.includes(e["method"])) {
4931
4918
  errors.push({
4932
- path: `${path10}.method`,
4919
+ path: `${path11}.method`,
4933
4920
  message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
4934
4921
  });
4935
4922
  }
4936
4923
  if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
4937
4924
  errors.push({
4938
- path: `${path10}.path`,
4925
+ path: `${path11}.path`,
4939
4926
  message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
4940
4927
  });
4941
4928
  }
4942
4929
  if (typeof e["auth"] !== "boolean") {
4943
- errors.push({ path: `${path10}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4930
+ errors.push({ path: `${path11}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4944
4931
  }
4945
4932
  if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
4946
4933
  errors.push({
4947
- path: `${path10}.successStatus`,
4934
+ path: `${path11}.successStatus`,
4948
4935
  message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
4949
4936
  });
4950
4937
  }
4951
- requireNonEmptyString(e["successDescription"], `${path10}.successDescription`, errors);
4938
+ requireNonEmptyString(e["successDescription"], `${path11}.successDescription`, errors);
4952
4939
  if (e["request"] !== void 0) {
4953
- validateRequestSchema(e["request"], `${path10}.request`, errors);
4940
+ validateRequestSchema(e["request"], `${path11}.request`, errors);
4954
4941
  }
4955
4942
  if (e["errors"] !== void 0) {
4956
4943
  if (!Array.isArray(e["errors"])) {
4957
- errors.push({ path: `${path10}.errors`, message: "Must be an array if present" });
4944
+ errors.push({ path: `${path11}.errors`, message: "Must be an array if present" });
4958
4945
  } else {
4959
4946
  const errs = e["errors"];
4960
4947
  if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
4961
- errors.push({ path: `${path10}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
4948
+ errors.push({ path: `${path11}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
4962
4949
  }
4963
4950
  for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
4964
- validateResponseError(errs[j2], `${path10}.errors[${j2}]`, errors);
4951
+ validateResponseError(errs[j2], `${path11}.errors[${j2}]`, errors);
4965
4952
  }
4966
4953
  }
4967
4954
  }
4968
4955
  }
4969
- function validateRequestSchema(raw, path10, errors) {
4956
+ function validateRequestSchema(raw, path11, errors) {
4970
4957
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4971
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4958
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4972
4959
  return;
4973
4960
  }
4974
4961
  const r = raw;
4975
4962
  for (const key of ["body", "query", "params"]) {
4976
4963
  if (r[key] !== void 0) {
4977
- validateFieldMap(r[key], `${path10}.${key}`, errors);
4964
+ validateFieldMap(r[key], `${path11}.${key}`, errors);
4978
4965
  }
4979
4966
  }
4980
4967
  }
4981
- function validateFieldMap(raw, path10, errors) {
4968
+ function validateFieldMap(raw, path11, errors) {
4982
4969
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4983
- errors.push({ path: path10, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
4970
+ errors.push({ path: path11, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
4984
4971
  return;
4985
4972
  }
4986
4973
  const map = raw;
4987
4974
  for (const [k2, v2] of Object.entries(map)) {
4988
4975
  if (typeof v2 !== "string") {
4989
- errors.push({ path: `${path10}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
4976
+ errors.push({ path: `${path11}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
4990
4977
  }
4991
4978
  }
4992
4979
  }
4993
- function validateResponseError(raw, path10, errors) {
4980
+ function validateResponseError(raw, path11, errors) {
4994
4981
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4995
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4982
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
4996
4983
  return;
4997
4984
  }
4998
4985
  const e = raw;
4999
4986
  if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
5000
- errors.push({ path: `${path10}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
4987
+ errors.push({ path: `${path11}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5001
4988
  }
5002
- requireNonEmptyString(e["code"], `${path10}.code`, errors);
5003
- requireNonEmptyString(e["description"], `${path10}.description`, errors);
4989
+ requireNonEmptyString(e["code"], `${path11}.code`, errors);
4990
+ requireNonEmptyString(e["description"], `${path11}.description`, errors);
5004
4991
  }
5005
- function validateBehavior(raw, path10, errors) {
4992
+ function validateBehavior(raw, path11, errors) {
5006
4993
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5007
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
4994
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
5008
4995
  return;
5009
4996
  }
5010
4997
  const b = raw;
5011
- requireNonEmptyString(b["id"], `${path10}.id`, errors);
5012
- requireNonEmptyString(b["description"], `${path10}.description`, errors);
4998
+ requireNonEmptyString(b["id"], `${path11}.id`, errors);
4999
+ requireNonEmptyString(b["description"], `${path11}.description`, errors);
5013
5000
  if (b["constraints"] !== void 0) {
5014
5001
  if (!Array.isArray(b["constraints"])) {
5015
- errors.push({ path: `${path10}.constraints`, message: "Must be an array of strings if present" });
5002
+ errors.push({ path: `${path11}.constraints`, message: "Must be an array of strings if present" });
5016
5003
  } else {
5017
5004
  const cs2 = b["constraints"];
5018
5005
  for (let j2 = 0; j2 < cs2.length; j2++) {
5019
5006
  if (typeof cs2[j2] !== "string") {
5020
- errors.push({ path: `${path10}.constraints[${j2}]`, message: "Must be a string" });
5007
+ errors.push({ path: `${path11}.constraints[${j2}]`, message: "Must be a string" });
5021
5008
  }
5022
5009
  }
5023
5010
  }
5024
5011
  }
5025
5012
  }
5026
- function validateComponent(raw, path10, errors) {
5013
+ function validateComponent(raw, path11, errors) {
5027
5014
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5028
- errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
5015
+ errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
5029
5016
  return;
5030
5017
  }
5031
5018
  const c = raw;
5032
- requireNonEmptyString(c["id"], `${path10}.id`, errors);
5033
- requireNonEmptyString(c["name"], `${path10}.name`, errors);
5034
- requireNonEmptyString(c["description"], `${path10}.description`, errors);
5019
+ requireNonEmptyString(c["id"], `${path11}.id`, errors);
5020
+ requireNonEmptyString(c["name"], `${path11}.name`, errors);
5021
+ requireNonEmptyString(c["description"], `${path11}.description`, errors);
5035
5022
  if (c["props"] !== void 0) {
5036
5023
  if (!Array.isArray(c["props"])) {
5037
- errors.push({ path: `${path10}.props`, message: "Must be an array if present" });
5024
+ errors.push({ path: `${path11}.props`, message: "Must be an array if present" });
5038
5025
  } else {
5039
5026
  const props = c["props"];
5040
5027
  for (let j2 = 0; j2 < props.length; j2++) {
5041
5028
  const p = props[j2];
5042
5029
  if (typeof p !== "object" || p === null) {
5043
- errors.push({ path: `${path10}.props[${j2}]`, message: "Must be an object" });
5030
+ errors.push({ path: `${path11}.props[${j2}]`, message: "Must be an object" });
5044
5031
  continue;
5045
5032
  }
5046
- requireNonEmptyString(p["name"], `${path10}.props[${j2}].name`, errors);
5047
- requireNonEmptyString(p["type"], `${path10}.props[${j2}].type`, errors);
5033
+ requireNonEmptyString(p["name"], `${path11}.props[${j2}].name`, errors);
5034
+ requireNonEmptyString(p["type"], `${path11}.props[${j2}].type`, errors);
5048
5035
  if (typeof p["required"] !== "boolean") {
5049
- errors.push({ path: `${path10}.props[${j2}].required`, message: "Must be boolean" });
5036
+ errors.push({ path: `${path11}.props[${j2}].required`, message: "Must be boolean" });
5050
5037
  }
5051
5038
  }
5052
5039
  }
5053
5040
  }
5054
5041
  if (c["events"] !== void 0) {
5055
5042
  if (!Array.isArray(c["events"])) {
5056
- errors.push({ path: `${path10}.events`, message: "Must be an array if present" });
5043
+ errors.push({ path: `${path11}.events`, message: "Must be an array if present" });
5057
5044
  } else {
5058
5045
  const events = c["events"];
5059
5046
  for (let j2 = 0; j2 < events.length; j2++) {
5060
5047
  const e = events[j2];
5061
5048
  if (typeof e !== "object" || e === null) {
5062
- errors.push({ path: `${path10}.events[${j2}]`, message: "Must be an object" });
5049
+ errors.push({ path: `${path11}.events[${j2}]`, message: "Must be an object" });
5063
5050
  continue;
5064
5051
  }
5065
- requireNonEmptyString(e["name"], `${path10}.events[${j2}].name`, errors);
5052
+ requireNonEmptyString(e["name"], `${path11}.events[${j2}].name`, errors);
5066
5053
  }
5067
5054
  }
5068
5055
  }
5069
5056
  if (c["state"] !== void 0) {
5070
5057
  if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
5071
- errors.push({ path: `${path10}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5058
+ errors.push({ path: `${path11}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5072
5059
  }
5073
5060
  }
5074
5061
  if (c["apiCalls"] !== void 0) {
5075
5062
  if (!Array.isArray(c["apiCalls"])) {
5076
- errors.push({ path: `${path10}.apiCalls`, message: "Must be an array of strings if present" });
5063
+ errors.push({ path: `${path11}.apiCalls`, message: "Must be an array of strings if present" });
5077
5064
  }
5078
5065
  }
5079
5066
  }
@@ -5135,10 +5122,10 @@ function crossReferenceChecks(obj, errors) {
5135
5122
  }
5136
5123
  }
5137
5124
  }
5138
- function requireNonEmptyString(v2, path10, errors) {
5125
+ function requireNonEmptyString(v2, path11, errors) {
5139
5126
  if (typeof v2 !== "string" || v2.trim().length === 0) {
5140
5127
  errors.push({
5141
- path: path10,
5128
+ path: path11,
5142
5129
  message: `Must be a non-empty string, got: ${typeLabel(v2)}`
5143
5130
  });
5144
5131
  }
@@ -5151,13 +5138,11 @@ function typeLabel(v2) {
5151
5138
 
5152
5139
  // core/token-budget.ts
5153
5140
  import chalk6 from "chalk";
5154
- var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5155
- function estimateTokens(text) {
5156
- if (!text) return 0;
5157
- const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5158
- const nonCjkLength = text.length - cjkCount;
5159
- return Math.ceil(cjkCount + nonCjkLength / 4);
5160
- }
5141
+
5142
+ // core/config-defaults.ts
5143
+ var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5144
+ var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
5145
+ var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
5161
5146
  var DEFAULT_TOKEN_BUDGETS = {
5162
5147
  gemini: 9e5,
5163
5148
  claude: 18e4,
@@ -5165,8 +5150,18 @@ var DEFAULT_TOKEN_BUDGETS = {
5165
5150
  deepseek: 6e4,
5166
5151
  default: 1e5
5167
5152
  };
5153
+
5154
+ // core/token-budget.ts
5155
+ var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5156
+ function estimateTokens(text) {
5157
+ if (!text) return 0;
5158
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5159
+ const nonCjkLength = text.length - cjkCount;
5160
+ return Math.ceil(cjkCount + nonCjkLength / 4);
5161
+ }
5162
+ var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
5168
5163
  function getDefaultBudget(providerName) {
5169
- return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
5164
+ return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
5170
5165
  }
5171
5166
 
5172
5167
  // core/dsl-extractor.ts
@@ -6019,6 +6014,101 @@ function printTaskProgress(completed, total, task, mode) {
6019
6014
  }
6020
6015
  }
6021
6016
 
6017
+ // core/fix-history.ts
6018
+ import * as fs10 from "fs-extra";
6019
+ import * as path6 from "path";
6020
+ var FIX_HISTORY_FILE = ".ai-spec-fix-history.json";
6021
+ var FIX_HISTORY_VERSION = "1.0";
6022
+ async function loadFixHistory(repoRoot) {
6023
+ const filePath = path6.join(repoRoot, FIX_HISTORY_FILE);
6024
+ if (!await fs10.pathExists(filePath)) {
6025
+ return { version: FIX_HISTORY_VERSION, entries: [] };
6026
+ }
6027
+ try {
6028
+ const data = await fs10.readJson(filePath);
6029
+ if (!data || typeof data !== "object" || !Array.isArray(data.entries)) {
6030
+ return { version: FIX_HISTORY_VERSION, entries: [] };
6031
+ }
6032
+ return {
6033
+ version: typeof data.version === "string" ? data.version : FIX_HISTORY_VERSION,
6034
+ entries: data.entries
6035
+ };
6036
+ } catch {
6037
+ return { version: FIX_HISTORY_VERSION, entries: [] };
6038
+ }
6039
+ }
6040
+ function aggregateFixPatterns(history) {
6041
+ const byKey = /* @__PURE__ */ new Map();
6042
+ for (const entry of history.entries) {
6043
+ if (!byKey.has(entry.patternKey)) byKey.set(entry.patternKey, []);
6044
+ byKey.get(entry.patternKey).push(entry);
6045
+ }
6046
+ const aggregates = [];
6047
+ for (const [patternKey, entries] of byKey) {
6048
+ entries.sort((a, b) => a.ts.localeCompare(b.ts));
6049
+ const first = entries[0];
6050
+ const last = entries[entries.length - 1];
6051
+ const uniqueRunIds = new Set(entries.map((e) => e.runId)).size;
6052
+ aggregates.push({
6053
+ patternKey,
6054
+ count: entries.length,
6055
+ firstSeen: first.ts,
6056
+ lastSeen: last.ts,
6057
+ uniqueRunIds,
6058
+ source: last.brokenImport.source,
6059
+ names: last.brokenImport.names,
6060
+ reason: last.brokenImport.reason,
6061
+ fix: {
6062
+ kind: last.fix.kind,
6063
+ target: last.fix.target,
6064
+ stage: last.fix.stage
6065
+ }
6066
+ });
6067
+ }
6068
+ aggregates.sort((a, b) => {
6069
+ if (b.count !== a.count) return b.count - a.count;
6070
+ return b.lastSeen.localeCompare(a.lastSeen);
6071
+ });
6072
+ return aggregates;
6073
+ }
6074
+ function buildHallucinationAvoidanceSection(history, opts = {}) {
6075
+ const minCount = opts.minCount ?? 1;
6076
+ const maxItems = opts.maxItems ?? 10;
6077
+ const patterns = aggregateFixPatterns(history).filter((p) => p.count >= minCount);
6078
+ if (patterns.length === 0) return null;
6079
+ const top = patterns.slice(0, maxItems);
6080
+ const lines = [
6081
+ "=== Prior Hallucinations in This Project (DO NOT REPEAT) ===",
6082
+ "",
6083
+ "The following imports were previously hallucinated by AI codegen in this",
6084
+ "project and had to be auto-fixed. When generating new files, actively avoid",
6085
+ "these exact imports \u2014 they were wrong in the past and will be wrong again.",
6086
+ ""
6087
+ ];
6088
+ for (const p of top) {
6089
+ const namesLabel = p.names.length > 0 ? `{ ${p.names.join(", ")} }` : "(no names)";
6090
+ const reasonLabel = p.reason === "file_not_found" ? "file did not exist" : "named export did not exist";
6091
+ const countLabel = p.count === 1 ? "1x" : `${p.count}x`;
6092
+ const dateLabel = p.lastSeen.slice(0, 10);
6093
+ lines.push(`\u274C Do NOT: import ${namesLabel} from '${p.source}'`);
6094
+ lines.push(` Reason: ${reasonLabel} (seen ${countLabel}, last ${dateLabel})`);
6095
+ if (p.fix.kind === "create_file") {
6096
+ lines.push(` Previously fixed by creating: ${p.fix.target}`);
6097
+ } else if (p.fix.kind === "rewrite_import") {
6098
+ lines.push(` Previously fixed by rewriting the import path`);
6099
+ } else {
6100
+ lines.push(` Previously fixed by appending to: ${p.fix.target}`);
6101
+ }
6102
+ lines.push("");
6103
+ }
6104
+ if (patterns.length > maxItems) {
6105
+ lines.push(`(${patterns.length - maxItems} more pattern(s) hidden \u2014 run \`ai-spec fix-history\` to see all)`);
6106
+ lines.push("");
6107
+ }
6108
+ lines.push("=== End of Prior Hallucinations ===");
6109
+ return lines.join("\n");
6110
+ }
6111
+
6022
6112
  // core/code-generator.ts
6023
6113
  var CodeGenerator = class {
6024
6114
  constructor(provider, mode = "claude-code") {
@@ -6083,8 +6173,8 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
6083
6173
  Files: ${t.filesToTouch.join(", ")}
6084
6174
  Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
6085
6175
  const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
6086
- const promptFile = path6.join(workingDir, ".claude-prompt.txt");
6087
- await fs10.writeFile(promptFile, promptContent, "utf-8");
6176
+ const promptFile = path7.join(workingDir, ".claude-prompt.txt");
6177
+ await fs11.writeFile(promptFile, promptContent, "utf-8");
6088
6178
  if (options.auto) {
6089
6179
  console.log(chalk10.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
6090
6180
  console.log(chalk10.gray(` Spec: ${specFilePath}`));
@@ -6177,7 +6267,7 @@ Implement ONLY this task. Do not implement other tasks.`;
6177
6267
  if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
6178
6268
  console.log(chalk10.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
6179
6269
  }
6180
- const spec = await fs10.readFile(specFilePath, "utf-8");
6270
+ const spec = await fs11.readFile(specFilePath, "utf-8");
6181
6271
  let constitutionSection = context?.constitution ? `
6182
6272
  === Project Constitution (MUST follow) ===
6183
6273
  ${context.constitution}
@@ -6204,7 +6294,24 @@ ${buildFrontendContextSection(fctx)}
6204
6294
  `;
6205
6295
  console.log(chalk10.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
6206
6296
  }
6207
- const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
6297
+ let fixHistorySection = "";
6298
+ if (options.injectFixHistory !== false) {
6299
+ try {
6300
+ const history = await loadFixHistory(workingDir);
6301
+ const section = buildHallucinationAvoidanceSection(history, {
6302
+ maxItems: options.fixHistoryInjectMax ?? 10
6303
+ });
6304
+ if (section) {
6305
+ fixHistorySection = `
6306
+ ${section}
6307
+ `;
6308
+ const patternCount = (section.match(/❌ Do NOT/g) ?? []).length;
6309
+ console.log(chalk10.cyan(` \u2713 Injected ${patternCount} prior hallucination pattern(s) from fix-history`));
6310
+ }
6311
+ } catch {
6312
+ }
6313
+ }
6314
+ const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection + fixHistorySection;
6208
6315
  const estimatedTokenCount = estimateTokens(allContextText);
6209
6316
  const budget = getDefaultBudget(this.provider.providerName);
6210
6317
  if (estimatedTokenCount > budget * 0.7) {
@@ -6223,7 +6330,7 @@ ${buildFrontendContextSection(fctx)}
6223
6330
  }
6224
6331
  const tasks = await loadTasksForSpec(specFilePath);
6225
6332
  if (tasks && tasks.length > 0) {
6226
- return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
6333
+ return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection + fixHistorySection, frontendSection, sharedConfigSection, options, systemPrompt, context);
6227
6334
  }
6228
6335
  console.log(chalk10.gray(" [1/2] Planning implementation files..."));
6229
6336
  const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
@@ -6234,7 +6341,7 @@ IMPORTANT: Check the "Frontend Project Context" section below. Extend existing h
6234
6341
 
6235
6342
  === Feature Spec ===
6236
6343
  ${spec}
6237
- ${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
6344
+ ${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}${fixHistorySection}
6238
6345
  === Project Context ===
6239
6346
  ${contextSummary}
6240
6347
 
@@ -6261,7 +6368,7 @@ Output ONLY a valid JSON array:
6261
6368
  const icon = item.action === "create" ? chalk10.green("+") : chalk10.yellow("~");
6262
6369
  console.log(` ${icon} ${item.file}: ${chalk10.gray(item.description)}`);
6263
6370
  });
6264
- const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
6371
+ const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection + fixHistorySection, systemPrompt);
6265
6372
  return files;
6266
6373
  }
6267
6374
  async runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection, frontendSection = "", sharedConfigSection = "", options = {}, systemPrompt = getCodeGenSystemPrompt(), context) {
@@ -6321,7 +6428,7 @@ Output ONLY a valid JSON array:
6321
6428
  }
6322
6429
  const filePlan = await Promise.all(
6323
6430
  task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
6324
- const exists = await fs10.pathExists(path6.join(workingDir, f));
6431
+ const exists = await fs11.pathExists(path7.join(workingDir, f));
6325
6432
  return {
6326
6433
  file: f,
6327
6434
  action: exists ? "modify" : "create",
@@ -6361,7 +6468,7 @@ ${taskContext}`,
6361
6468
  const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
6362
6469
  if (isCodeFile || isViewFile) {
6363
6470
  try {
6364
- const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path6.join(workingDir, writtenFile), "utf-8");
6471
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs11.readFile(path7.join(workingDir, writtenFile), "utf-8");
6365
6472
  generatedFileCache.set(writtenFile, content);
6366
6473
  } catch {
6367
6474
  }
@@ -6371,19 +6478,28 @@ ${taskContext}`,
6371
6478
  };
6372
6479
  const taskBatches = topoSortLayerTasks(layerTasks);
6373
6480
  const layerResults = [];
6481
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? 3);
6374
6482
  for (const batch of taskBatches) {
6375
6483
  const batchIsParallel = batch.length > 1;
6376
- const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
6377
- const settled = await Promise.allSettled(batchResultPromises);
6378
6484
  const batchResults = [];
6379
- for (let i = 0; i < settled.length; i++) {
6380
- const outcome = settled[i];
6381
- if (outcome.status === "fulfilled") {
6382
- batchResults.push(outcome.value);
6383
- } else {
6384
- const task = batch[i];
6385
- console.log(chalk10.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
6386
- batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
6485
+ for (let chunkStart = 0; chunkStart < batch.length; chunkStart += maxConcurrency) {
6486
+ const chunk = batch.slice(chunkStart, chunkStart + maxConcurrency);
6487
+ if (batchIsParallel && batch.length > maxConcurrency) {
6488
+ const chunkIdx = Math.floor(chunkStart / maxConcurrency) + 1;
6489
+ const totalChunks = Math.ceil(batch.length / maxConcurrency);
6490
+ console.log(chalk10.gray(` \u21B3 chunk ${chunkIdx}/${totalChunks} (${chunk.length} tasks, concurrency cap: ${maxConcurrency})`));
6491
+ }
6492
+ const chunkResultPromises = chunk.map((task) => executeTask(task, batchIsParallel));
6493
+ const settled = await Promise.allSettled(chunkResultPromises);
6494
+ for (let i = 0; i < settled.length; i++) {
6495
+ const outcome = settled[i];
6496
+ if (outcome.status === "fulfilled") {
6497
+ batchResults.push(outcome.value);
6498
+ } else {
6499
+ const task = chunk[i];
6500
+ console.log(chalk10.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
6501
+ batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
6502
+ }
6387
6503
  }
6388
6504
  }
6389
6505
  layerResults.push(...batchResults);
@@ -6413,7 +6529,7 @@ ${taskContext}`,
6413
6529
  const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
6414
6530
  for (const sharedFile of context.sharedConfigFiles) {
6415
6531
  if (processedSharedConfigs.has(sharedFile.path)) continue;
6416
- const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path6.basename(f).replace(/\.[jt]sx?$/, ""));
6532
+ const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path7.basename(f).replace(/\.[jt]sx?$/, ""));
6417
6533
  if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
6418
6534
  let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
6419
6535
  if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
@@ -6453,10 +6569,10 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
6453
6569
  let successCount = 0;
6454
6570
  const writtenFiles = [];
6455
6571
  for (const item of filePlan) {
6456
- const fullPath = path6.join(workingDir, item.file);
6572
+ const fullPath = path7.join(workingDir, item.file);
6457
6573
  let existingContent = "";
6458
- if (await fs10.pathExists(fullPath)) {
6459
- existingContent = await fs10.readFile(fullPath, "utf-8");
6574
+ if (await fs11.pathExists(fullPath)) {
6575
+ existingContent = await fs11.readFile(fullPath, "utf-8");
6460
6576
  }
6461
6577
  const codePrompt = `Implement this file.
6462
6578
 
@@ -6473,8 +6589,8 @@ ${existingContent || "Output only the complete file content."}`;
6473
6589
  const raw = await this.provider.generate(codePrompt, systemPrompt);
6474
6590
  const fileContent = stripCodeFences(raw);
6475
6591
  await getActiveSnapshot()?.snapshotFile(fullPath);
6476
- await fs10.ensureDir(path6.dirname(fullPath));
6477
- await fs10.writeFile(fullPath, fileContent, "utf-8");
6592
+ await fs11.ensureDir(path7.dirname(fullPath));
6593
+ await fs11.writeFile(fullPath, fileContent, "utf-8");
6478
6594
  getActiveLogger()?.fileWritten(item.file);
6479
6595
  fileSpinner.succeed(`${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)}`);
6480
6596
  successCount++;
@@ -6495,7 +6611,7 @@ ${existingContent || "Output only the complete file content."}`;
6495
6611
  // ── Mode: plan ─────────────────────────────────────────────────────────────
6496
6612
  async runPlanMode(specFilePath) {
6497
6613
  console.log(chalk10.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6498
- const spec = await fs10.readFile(specFilePath, "utf-8");
6614
+ const spec = await fs11.readFile(specFilePath, "utf-8");
6499
6615
  const plan = await this.provider.generate(
6500
6616
  `Create a detailed, step-by-step implementation plan for the following feature spec.
6501
6617
  Be specific about:
@@ -6514,13 +6630,13 @@ ${spec}`,
6514
6630
  // core/reviewer.ts
6515
6631
  import chalk12 from "chalk";
6516
6632
  import { execSync as execSync3 } from "child_process";
6517
- import * as path8 from "path";
6518
- import * as fs12 from "fs-extra";
6633
+ import * as path9 from "path";
6634
+ import * as fs13 from "fs-extra";
6519
6635
 
6520
6636
  // core/constitution-generator.ts
6521
6637
  import chalk11 from "chalk";
6522
- import * as fs11 from "fs-extra";
6523
- import * as path7 from "path";
6638
+ import * as fs12 from "fs-extra";
6639
+ import * as path8 from "path";
6524
6640
 
6525
6641
  // prompts/constitution.prompt.ts
6526
6642
  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.
@@ -6600,8 +6716,8 @@ var ConstitutionGenerator = class {
6600
6716
  return this.provider.generate(prompt, constitutionSystemPrompt);
6601
6717
  }
6602
6718
  async saveConstitution(projectRoot, content) {
6603
- const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6604
- await fs11.writeFile(filePath, content, "utf-8");
6719
+ const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6720
+ await fs12.writeFile(filePath, content, "utf-8");
6605
6721
  return filePath;
6606
6722
  }
6607
6723
  };
@@ -6627,7 +6743,7 @@ ${context.routeSummary}
6627
6743
  }
6628
6744
  if (context.schema) {
6629
6745
  parts.push(`=== Prisma Schema ===
6630
- ${context.schema.slice(0, 4e3)}
6746
+ ${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
6631
6747
  `);
6632
6748
  }
6633
6749
  if (context.errorPatterns) {
@@ -6659,9 +6775,9 @@ ${sections.join("\n")}
6659
6775
  return parts.join("\n");
6660
6776
  }
6661
6777
  async function loadConstitution(projectRoot) {
6662
- const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6663
- if (await fs11.pathExists(filePath)) {
6664
- return fs11.readFile(filePath, "utf-8");
6778
+ const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6779
+ if (await fs12.pathExists(filePath)) {
6780
+ return fs12.readFile(filePath, "utf-8");
6665
6781
  }
6666
6782
  return void 0;
6667
6783
  }
@@ -6677,10 +6793,10 @@ function printConstitutionHint(exists) {
6677
6793
 
6678
6794
  // core/reviewer.ts
6679
6795
  async function loadAccumulatedLessons(projectRoot) {
6680
- const constitutionPath = path8.join(projectRoot, CONSTITUTION_FILE);
6796
+ const constitutionPath = path9.join(projectRoot, CONSTITUTION_FILE);
6681
6797
  let content;
6682
6798
  try {
6683
- content = await fs12.readFile(constitutionPath, "utf-8");
6799
+ content = await fs13.readFile(constitutionPath, "utf-8");
6684
6800
  } catch {
6685
6801
  return null;
6686
6802
  }
@@ -6691,23 +6807,23 @@ async function loadAccumulatedLessons(projectRoot) {
6691
6807
  const nextSection = section.slice(marker.length).match(/\n## \d/);
6692
6808
  return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
6693
6809
  }
6694
- var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
6810
+ var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
6695
6811
  async function loadReviewHistory(projectRoot) {
6696
- const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6812
+ const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
6697
6813
  try {
6698
- if (await fs12.pathExists(historyPath)) {
6699
- return await fs12.readJson(historyPath);
6814
+ if (await fs13.pathExists(historyPath)) {
6815
+ return await fs13.readJson(historyPath);
6700
6816
  }
6701
6817
  } catch {
6702
6818
  }
6703
6819
  return [];
6704
6820
  }
6705
6821
  async function appendReviewHistory(projectRoot, entry) {
6706
- const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6822
+ const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
6707
6823
  const existing = await loadReviewHistory(projectRoot);
6708
6824
  const updated = [...existing, entry].slice(-20);
6709
6825
  try {
6710
- await fs12.writeJson(historyPath, updated, { spaces: 2 });
6826
+ await fs13.writeJson(historyPath, updated, { spaces: 2 });
6711
6827
  } catch {
6712
6828
  }
6713
6829
  }
@@ -6741,7 +6857,7 @@ function buildHistoryContext(history) {
6741
6857
  const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
6742
6858
  for (const entry of recent) {
6743
6859
  lines.push(`
6744
- [${entry.date}] ${path8.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6860
+ [${entry.date}] ${path9.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6745
6861
  entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
6746
6862
  }
6747
6863
  return lines.join("\n") + "\n";
@@ -6822,15 +6938,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6822
6938
  ${codeContext}`;
6823
6939
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6824
6940
  console.log(chalk12.gray(" Pass 2/3: Implementation review..."));
6941
+ const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
6825
6942
  const history = await loadReviewHistory(this.projectRoot);
6826
6943
  const historyContext = buildHistoryContext(history);
6827
6944
  const implPrompt = `Review the implementation details of this change.
6828
6945
 
6829
- === Feature Spec ===
6830
- ${specContent || "(No spec \u2014 review for general code quality)"}
6831
-
6832
- === Code ===
6833
- ${codeContext}
6946
+ === Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
6947
+ ${specDigest}
6834
6948
 
6835
6949
  === Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
6836
6950
  ${archReview}
@@ -6839,11 +6953,8 @@ ${historyContext}`;
6839
6953
  console.log(chalk12.gray(" Pass 3/3: Impact & complexity assessment..."));
6840
6954
  const impactPrompt = `Assess the impact and complexity of this change.
6841
6955
 
6842
- === Feature Spec ===
6843
- ${specContent || "(No spec \u2014 review for general code quality)"}
6844
-
6845
- === Code ===
6846
- ${codeContext}
6956
+ === Feature Spec (digest) ===
6957
+ ${specDigest}
6847
6958
 
6848
6959
  === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
6849
6960
  ${archReview}
@@ -6866,7 +6977,7 @@ ${sep}
6866
6977
  if (score > 0 && specFile) {
6867
6978
  await appendReviewHistory(this.projectRoot, {
6868
6979
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
6869
- specFile: path8.relative(this.projectRoot, specFile),
6980
+ specFile: path9.relative(this.projectRoot, specFile),
6870
6981
  score,
6871
6982
  ...complianceScore > 0 ? { complianceScore } : {},
6872
6983
  topIssues,
@@ -6911,14 +7022,14 @@ ${sep}
6911
7022
  );
6912
7023
  let filesSection = "";
6913
7024
  for (const filePath of filePaths) {
6914
- const fullPath = path8.join(workingDir, filePath);
7025
+ const fullPath = path9.join(workingDir, filePath);
6915
7026
  try {
6916
- const content = await fs12.readFile(fullPath, "utf-8");
7027
+ const content = await fs13.readFile(fullPath, "utf-8");
6917
7028
  filesSection += `
6918
7029
 
6919
7030
  === ${filePath} ===
6920
- ${content.slice(0, 3e3)}`;
6921
- if (content.length > 3e3) filesSection += `
7031
+ ${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
7032
+ if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
6922
7033
  ... (truncated, ${content.length} chars total)`;
6923
7034
  } catch {
6924
7035
  filesSection += `
@@ -6947,7 +7058,7 @@ ${content.slice(0, 3e3)}`;
6947
7058
  const color = entry.score >= 8 ? chalk12.green : entry.score >= 6 ? chalk12.yellow : chalk12.red;
6948
7059
  const impactTag = entry.impactLevel ? chalk12.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk12.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk12.yellow(entry.impactLevel) : chalk12.green(entry.impactLevel)}`) : "";
6949
7060
  const complexityTag = entry.complexityLevel ? chalk12.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk12.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk12.yellow(entry.complexityLevel) : chalk12.green(entry.complexityLevel)}`) : "";
6950
- console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
7061
+ console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path9.basename(entry.specFile)}`);
6951
7062
  }
6952
7063
  console.log(chalk12.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6953
7064
  }
@@ -7000,8 +7111,8 @@ function parseSpecAndTasks(raw) {
7000
7111
 
7001
7112
  // git/worktree.ts
7002
7113
  import { execSync as execSync4 } from "child_process";
7003
- import * as path9 from "path";
7004
- import * as fs13 from "fs-extra";
7114
+ import * as path10 from "path";
7115
+ import * as fs14 from "fs-extra";
7005
7116
  import chalk13 from "chalk";
7006
7117
  var GitWorktreeManager = class {
7007
7118
  constructor(baseDir) {
@@ -7027,12 +7138,12 @@ var GitWorktreeManager = class {
7027
7138
  async linkDependencies(worktreePath) {
7028
7139
  const candidates = ["node_modules", "vendor"];
7029
7140
  for (const dir of candidates) {
7030
- const src = path9.join(this.baseDir, dir);
7031
- const dest = path9.join(worktreePath, dir);
7032
- if (!await fs13.pathExists(src)) continue;
7033
- if (await fs13.pathExists(dest)) continue;
7141
+ const src = path10.join(this.baseDir, dir);
7142
+ const dest = path10.join(worktreePath, dir);
7143
+ if (!await fs14.pathExists(src)) continue;
7144
+ if (await fs14.pathExists(dest)) continue;
7034
7145
  try {
7035
- await fs13.ensureSymlink(src, dest, "dir");
7146
+ await fs14.ensureSymlink(src, dest, "dir");
7036
7147
  console.log(chalk13.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
7037
7148
  } catch (err) {
7038
7149
  console.log(chalk13.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
@@ -7047,11 +7158,11 @@ var GitWorktreeManager = class {
7047
7158
  }
7048
7159
  const featureName = this.sanitizeFeatureName(idea);
7049
7160
  const branchName = `feature/${featureName}`;
7050
- const repoName = path9.basename(this.baseDir);
7051
- const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
7161
+ const repoName = path10.basename(this.baseDir);
7162
+ const worktreePath = path10.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
7052
7163
  console.log(chalk13.cyan(`
7053
7164
  --- Setting up Git Worktree ---`));
7054
- if (await fs13.pathExists(worktreePath)) {
7165
+ if (await fs14.pathExists(worktreePath)) {
7055
7166
  console.log(chalk13.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
7056
7167
  await this.linkDependencies(worktreePath);
7057
7168
  return worktreePath;