ai-spec-dev 0.42.0 → 0.46.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 (49) hide show
  1. package/README.md +33 -17
  2. package/cli/commands/create.ts +232 -11
  3. package/cli/commands/init.ts +310 -107
  4. package/cli/commands/model.ts +7 -11
  5. package/cli/index.ts +1 -1
  6. package/cli/utils.ts +72 -4
  7. package/core/config-defaults.ts +44 -0
  8. package/core/constitution-generator.ts +2 -1
  9. package/core/dsl-extractor.ts +2 -1
  10. package/core/error-feedback.ts +3 -2
  11. package/core/openapi-exporter.ts +3 -2
  12. package/core/repo-store.ts +95 -0
  13. package/core/reviewer.ts +14 -13
  14. package/core/run-logger.ts +3 -4
  15. package/core/run-snapshot.ts +2 -3
  16. package/core/run-trend.ts +3 -4
  17. package/core/spec-generator.ts +27 -42
  18. package/core/token-budget.ts +3 -8
  19. package/core/vcr.ts +3 -1
  20. package/dist/cli/index.js +919 -519
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/index.mjs +912 -512
  23. package/dist/cli/index.mjs.map +1 -1
  24. package/dist/index.d.mts +3 -2
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.js +43 -53
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +43 -53
  29. package/dist/index.mjs.map +1 -1
  30. package/package.json +1 -1
  31. package/demo-backend/.ai-spec-constitution.md +0 -65
  32. package/demo-backend/package.json +0 -21
  33. package/demo-backend/prisma/schema.prisma +0 -22
  34. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
  35. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
  36. package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
  37. package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
  38. package/demo-backend/src/index.ts +0 -17
  39. package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
  40. package/demo-backend/src/routes/bookmark.routes.ts +0 -11
  41. package/demo-backend/src/routes/index.ts +0 -8
  42. package/demo-backend/src/services/bookmark.service.test.ts +0 -433
  43. package/demo-backend/src/services/bookmark.service.ts +0 -261
  44. package/demo-backend/tsconfig.json +0 -12
  45. package/demo-frontend/.ai-spec-constitution.md +0 -95
  46. package/demo-frontend/package.json +0 -23
  47. package/demo-frontend/src/App.tsx +0 -12
  48. package/demo-frontend/src/main.tsx +0 -9
  49. package/demo-frontend/tsconfig.json +0 -13
package/dist/cli/index.js CHANGED
@@ -9,6 +9,9 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __esm = (fn, res) => function __init() {
10
10
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
11
  };
12
+ var __commonJS = (cb, mod) => function __require() {
13
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
14
+ };
12
15
  var __export = (target, all) => {
13
16
  for (var name in all)
14
17
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -391,14 +394,13 @@ function createProvider(providerName, apiKey, modelName) {
391
394
  );
392
395
  }
393
396
  }
394
- var import_generative_ai, import_sdk, import_openai, import_axios, import_undici, PROVIDER_CATALOG, SUPPORTED_PROVIDERS, DEFAULT_MODELS, ENV_KEY_MAP, GeminiProvider, ClaudeProvider, OpenAICompatibleProvider, MiMoProvider, SpecGenerator;
397
+ var import_generative_ai, import_sdk, import_openai, import_undici, PROVIDER_CATALOG, SUPPORTED_PROVIDERS, DEFAULT_MODELS, ENV_KEY_MAP, GeminiProvider, ClaudeProvider, OpenAICompatibleProvider, MiMoProvider, SpecGenerator;
395
398
  var init_spec_generator = __esm({
396
399
  "core/spec-generator.ts"() {
397
400
  "use strict";
398
401
  import_generative_ai = require("@google/generative-ai");
399
402
  import_sdk = __toESM(require("@anthropic-ai/sdk"));
400
403
  import_openai = __toESM(require("openai"));
401
- import_axios = __toESM(require("axios"));
402
404
  import_undici = require("undici");
403
405
  init_spec_prompt();
404
406
  init_provider_utils();
@@ -408,7 +410,9 @@ var init_spec_generator = __esm({
408
410
  displayName: "MiMo (Xiaomi)",
409
411
  description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
410
412
  models: ["mimo-v2-pro"],
411
- envKey: "MIMO_API_KEY"
413
+ envKey: "MIMO_API_KEY",
414
+ // Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
415
+ fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
412
416
  // baseURL not used — MiMo has a dedicated provider class
413
417
  },
414
418
  gemini: {
@@ -577,8 +581,8 @@ var init_spec_generator = __esm({
577
581
  ...systemInstruction ? { system: systemInstruction } : {},
578
582
  messages: [{ role: "user", content: prompt }]
579
583
  });
580
- const block = message.content[0];
581
- if (block.type === "text") return block.text;
584
+ const textBlock = message.content.find((b) => b.type === "text");
585
+ if (textBlock) return textBlock.text;
582
586
  throw new Error("Unexpected response type from Claude API");
583
587
  },
584
588
  { label: `${this.providerName}/${this.modelName}` }
@@ -623,43 +627,29 @@ var init_spec_generator = __esm({
623
627
  }
624
628
  };
625
629
  MiMoProvider = class {
630
+ client;
626
631
  providerName = "mimo";
627
632
  modelName;
628
- apiKey;
629
- baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
630
633
  constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
631
- this.apiKey = apiKey;
634
+ const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
635
+ this.client = new import_sdk.default({ apiKey, baseURL });
632
636
  this.modelName = modelName;
633
637
  }
634
638
  async generate(prompt, systemInstruction) {
635
639
  return withReliability(
636
640
  async () => {
637
- const body = {
641
+ const stream = this.client.messages.stream({
638
642
  model: this.modelName,
639
- max_tokens: 16384,
640
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
641
- top_p: 0.95,
642
- stream: false,
643
- temperature: 1,
644
- stop_sequences: null
645
- };
646
- if (systemInstruction) {
647
- body.system = systemInstruction;
648
- }
649
- const response = await import_axios.default.post(this.baseUrl, body, {
650
- headers: {
651
- "api-key": this.apiKey,
652
- "Content-Type": "application/json"
653
- }
643
+ max_tokens: 65536,
644
+ ...systemInstruction ? { system: systemInstruction } : {},
645
+ messages: [{ role: "user", content: prompt }]
654
646
  });
655
- const data = response.data;
656
- const blocks = data?.content ?? [];
657
- const textBlock = blocks.find((b) => b.type === "text");
658
- if (textBlock?.text) return textBlock.text;
659
- if (data?.stop_reason === "max_tokens") {
660
- 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.`);
661
- }
662
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
647
+ const message = await stream.finalMessage();
648
+ const textBlock = message.content.find((b) => b.type === "text");
649
+ if (textBlock) return textBlock.text;
650
+ const thinkBlock = message.content.find((b) => b.type === "thinking");
651
+ if (thinkBlock) return thinkBlock.thinking;
652
+ return message.content.map((b) => b.text ?? "").join("");
663
653
  },
664
654
  { label: `${this.providerName}/${this.modelName}` }
665
655
  );
@@ -720,11 +710,68 @@ ${context.schema.slice(0, 3e3)}`);
720
710
  }
721
711
  });
722
712
 
713
+ // package.json
714
+ var require_package = __commonJS({
715
+ "package.json"(exports2, module2) {
716
+ module2.exports = {
717
+ name: "ai-spec-dev",
718
+ version: "0.46.0",
719
+ description: "AI-driven Development Orchestrator SDK & CLI",
720
+ main: "dist/index.js",
721
+ types: "dist/index.d.ts",
722
+ bin: {
723
+ "ai-spec": "dist/cli/index.js"
724
+ },
725
+ scripts: {
726
+ build: "tsup",
727
+ dev: "tsup --watch",
728
+ test: "vitest run",
729
+ "test:watch": "vitest"
730
+ },
731
+ keywords: [
732
+ "ai",
733
+ "spec",
734
+ "codegen",
735
+ "gemini",
736
+ "claude"
737
+ ],
738
+ author: "",
739
+ license: "MIT",
740
+ dependencies: {
741
+ "@anthropic-ai/sdk": "^0.38.0",
742
+ "@google/generative-ai": "^0.21.0",
743
+ "@inquirer/editor": "^5.0.10",
744
+ "@inquirer/prompts": "^8.3.2",
745
+ "@rollup/rollup-darwin-arm64": "^4.60.1",
746
+ axios: "^1.13.6",
747
+ chalk: "^4.1.2",
748
+ commander: "^13.1.0",
749
+ dotenv: "^16.4.7",
750
+ "fs-extra": "^11.3.0",
751
+ openai: "^6.31.0",
752
+ undici: "^7.24.4"
753
+ },
754
+ devDependencies: {
755
+ "@types/fs-extra": "^11.0.4",
756
+ "@types/glob": "^8.1.0",
757
+ "@types/node": "^22.13.5",
758
+ glob: "^13.0.6",
759
+ "ts-node": "^10.9.2",
760
+ tsup: "^8.4.0",
761
+ typescript: "^5.7.3",
762
+ vitest: "^2.1.0"
763
+ }
764
+ };
765
+ }
766
+ });
767
+
723
768
  // cli/index.ts
724
769
  var import_commander = require("commander");
725
770
  var dotenv = __toESM(require("dotenv"));
726
771
 
727
772
  // cli/commands/create.ts
773
+ var path26 = __toESM(require("path"));
774
+ var fs27 = __toESM(require("fs-extra"));
728
775
  var import_chalk26 = __toESM(require("chalk"));
729
776
  var import_prompts7 = require("@inquirer/prompts");
730
777
  init_spec_generator();
@@ -827,8 +874,8 @@ var WorkspaceLoader = class {
827
874
  const repos = [];
828
875
  for (const entry of entries) {
829
876
  const absPath = path.join(this.workspaceRoot, entry);
830
- const stat4 = await fs.stat(absPath).catch(() => null);
831
- if (!stat4 || !stat4.isDirectory()) continue;
877
+ const stat5 = await fs.stat(absPath).catch(() => null);
878
+ if (!stat5 || !stat5.isDirectory()) continue;
832
879
  if (entry.startsWith(".") || entry === "node_modules") continue;
833
880
  if (names && !names.includes(entry)) continue;
834
881
  const hasManifest = await fs.pathExists(path.join(absPath, "package.json")) || await fs.pathExists(path.join(absPath, "go.mod")) || await fs.pathExists(path.join(absPath, "Cargo.toml")) || await fs.pathExists(path.join(absPath, "pom.xml")) || await fs.pathExists(path.join(absPath, "build.gradle")) || await fs.pathExists(path.join(absPath, "requirements.txt")) || await fs.pathExists(path.join(absPath, "pyproject.toml")) || await fs.pathExists(path.join(absPath, "composer.json"));
@@ -891,6 +938,7 @@ var WorkspaceLoader = class {
891
938
  // cli/utils.ts
892
939
  var path3 = __toESM(require("path"));
893
940
  var fs3 = __toESM(require("fs-extra"));
941
+ var os2 = __toESM(require("os"));
894
942
  var import_chalk2 = __toESM(require("chalk"));
895
943
  var import_prompts = require("@inquirer/prompts");
896
944
  init_spec_generator();
@@ -937,17 +985,42 @@ async function clearKey(provider) {
937
985
 
938
986
  // cli/utils.ts
939
987
  var CONFIG_FILE = ".ai-spec.json";
988
+ var GLOBAL_CONFIG_FILE = path3.join(os2.homedir(), ".ai-spec-config.json");
989
+ async function loadGlobalConfig() {
990
+ try {
991
+ if (await fs3.pathExists(GLOBAL_CONFIG_FILE)) {
992
+ return await fs3.readJson(GLOBAL_CONFIG_FILE);
993
+ }
994
+ } catch {
995
+ }
996
+ return {};
997
+ }
998
+ async function saveGlobalConfig(config2) {
999
+ await fs3.ensureFile(GLOBAL_CONFIG_FILE);
1000
+ await fs3.writeJson(GLOBAL_CONFIG_FILE, config2, { spaces: 2 });
1001
+ }
940
1002
  async function loadConfig(dir) {
1003
+ const globalConfig = await loadGlobalConfig();
1004
+ let localConfig = {};
941
1005
  const p = path3.join(dir, CONFIG_FILE);
942
1006
  if (await fs3.pathExists(p)) {
943
- return fs3.readJson(p);
1007
+ try {
1008
+ localConfig = await fs3.readJson(p);
1009
+ } catch {
1010
+ }
944
1011
  }
945
- return {};
1012
+ return { ...globalConfig, ...localConfig };
946
1013
  }
947
1014
  async function resolveApiKey(providerName, cliKey) {
948
1015
  if (cliKey) return cliKey;
949
1016
  const envVar = ENV_KEY_MAP[providerName];
950
1017
  if (envVar && process.env[envVar]) return process.env[envVar];
1018
+ const meta = PROVIDER_CATALOG[providerName];
1019
+ if (meta?.fallbackEnvKeys) {
1020
+ for (const key of meta.fallbackEnvKeys) {
1021
+ if (process.env[key]) return process.env[key];
1022
+ }
1023
+ }
951
1024
  const savedKey = await getSavedKey(providerName);
952
1025
  if (savedKey) {
953
1026
  const masked = savedKey.slice(0, 6) + "..." + savedKey.slice(-4);
@@ -1021,7 +1094,7 @@ var pe = "\0PERIOD" + Math.random() + "\0";
1021
1094
  var is = new RegExp(fe, "g");
1022
1095
  var rs = new RegExp(ue, "g");
1023
1096
  var ns = new RegExp(qt, "g");
1024
- var os2 = new RegExp(de, "g");
1097
+ var os3 = new RegExp(de, "g");
1025
1098
  var hs = new RegExp(pe, "g");
1026
1099
  var as = /\\\\/g;
1027
1100
  var ls = /\\{/g;
@@ -1036,7 +1109,7 @@ function ps(n7) {
1036
1109
  return n7.replace(as, fe).replace(ls, ue).replace(cs, qt).replace(fs4, de).replace(us, pe);
1037
1110
  }
1038
1111
  function ms(n7) {
1039
- return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(os2, ",").replace(hs, ".");
1112
+ return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(os3, ",").replace(hs, ".");
1040
1113
  }
1041
1114
  function me(n7) {
1042
1115
  if (!n7) return [""];
@@ -3966,11 +4039,11 @@ Ze.glob = Ze;
3966
4039
  // core/global-constitution.ts
3967
4040
  var fs5 = __toESM(require("fs-extra"));
3968
4041
  var path4 = __toESM(require("path"));
3969
- var os3 = __toESM(require("os"));
4042
+ var os4 = __toESM(require("os"));
3970
4043
  var GLOBAL_CONSTITUTION_FILE = ".ai-spec-global-constitution.md";
3971
4044
  var SEARCH_ROOTS = [
3972
4045
  // Workspace root is injected at runtime — see loadGlobalConstitution()
3973
- os3.homedir()
4046
+ os4.homedir()
3974
4047
  ];
3975
4048
  async function loadGlobalConstitution(extraRoots = []) {
3976
4049
  const roots = [...extraRoots, ...SEARCH_ROOTS];
@@ -3999,7 +4072,7 @@ function mergeConstitutions(globalContent, projectContent) {
3999
4072
  }
4000
4073
  return parts.join("\n");
4001
4074
  }
4002
- async function saveGlobalConstitution(content, targetDir = os3.homedir()) {
4075
+ async function saveGlobalConstitution(content, targetDir = os4.homedir()) {
4003
4076
  const filePath = path4.join(targetDir, GLOBAL_CONSTITUTION_FILE);
4004
4077
  await fs5.writeFile(filePath, content, "utf-8");
4005
4078
  return filePath;
@@ -5032,32 +5105,32 @@ function validateDsl(raw) {
5032
5105
  }
5033
5106
  return { valid: true, dsl: raw };
5034
5107
  }
5035
- function validateFeature(raw, path42, errors) {
5108
+ function validateFeature(raw, path43, errors) {
5036
5109
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5037
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5110
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5038
5111
  return;
5039
5112
  }
5040
5113
  const f = raw;
5041
- requireNonEmptyString(f["id"], `${path42}.id`, errors);
5042
- requireNonEmptyString(f["title"], `${path42}.title`, errors);
5043
- requireNonEmptyString(f["description"], `${path42}.description`, errors);
5114
+ requireNonEmptyString(f["id"], `${path43}.id`, errors);
5115
+ requireNonEmptyString(f["title"], `${path43}.title`, errors);
5116
+ requireNonEmptyString(f["description"], `${path43}.description`, errors);
5044
5117
  }
5045
- function validateModel(raw, path42, errors) {
5118
+ function validateModel(raw, path43, errors) {
5046
5119
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5047
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5120
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5048
5121
  return;
5049
5122
  }
5050
5123
  const m = raw;
5051
- requireNonEmptyString(m["name"], `${path42}.name`, errors);
5124
+ requireNonEmptyString(m["name"], `${path43}.name`, errors);
5052
5125
  if (!Array.isArray(m["fields"])) {
5053
- errors.push({ path: `${path42}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
5126
+ errors.push({ path: `${path43}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
5054
5127
  } else {
5055
5128
  const fields = m["fields"];
5056
5129
  if (fields.length > MAX_FIELDS_PER_MODEL) {
5057
- errors.push({ path: `${path42}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
5130
+ errors.push({ path: `${path43}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
5058
5131
  }
5059
5132
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
5060
- validateModelField(fields[j2], `${path42}.fields[${j2}]`, errors);
5133
+ validateModelField(fields[j2], `${path43}.fields[${j2}]`, errors);
5061
5134
  }
5062
5135
  const seenFieldNames = /* @__PURE__ */ new Set();
5063
5136
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
@@ -5066,7 +5139,7 @@ function validateModel(raw, path42, errors) {
5066
5139
  const name = f["name"];
5067
5140
  if (seenFieldNames.has(name)) {
5068
5141
  errors.push({
5069
- path: `${path42}.fields[${j2}].name`,
5142
+ path: `${path43}.fields[${j2}].name`,
5070
5143
  message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
5071
5144
  });
5072
5145
  } else {
@@ -5077,184 +5150,184 @@ function validateModel(raw, path42, errors) {
5077
5150
  }
5078
5151
  if (m["relations"] !== void 0) {
5079
5152
  if (!Array.isArray(m["relations"])) {
5080
- errors.push({ path: `${path42}.relations`, message: "Must be an array of strings if present" });
5153
+ errors.push({ path: `${path43}.relations`, message: "Must be an array of strings if present" });
5081
5154
  } else {
5082
5155
  const rels = m["relations"];
5083
5156
  for (let j2 = 0; j2 < rels.length; j2++) {
5084
5157
  if (typeof rels[j2] !== "string") {
5085
- errors.push({ path: `${path42}.relations[${j2}]`, message: "Must be a string" });
5158
+ errors.push({ path: `${path43}.relations[${j2}]`, message: "Must be a string" });
5086
5159
  }
5087
5160
  }
5088
5161
  }
5089
5162
  }
5090
5163
  }
5091
- function validateModelField(raw, path42, errors) {
5164
+ function validateModelField(raw, path43, errors) {
5092
5165
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5093
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5166
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5094
5167
  return;
5095
5168
  }
5096
5169
  const f = raw;
5097
- requireNonEmptyString(f["name"], `${path42}.name`, errors);
5098
- requireNonEmptyString(f["type"], `${path42}.type`, errors);
5170
+ requireNonEmptyString(f["name"], `${path43}.name`, errors);
5171
+ requireNonEmptyString(f["type"], `${path43}.type`, errors);
5099
5172
  if (typeof f["required"] !== "boolean") {
5100
- errors.push({ path: `${path42}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
5173
+ errors.push({ path: `${path43}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
5101
5174
  }
5102
5175
  }
5103
- function validateEndpoint(raw, path42, errors) {
5176
+ function validateEndpoint(raw, path43, errors) {
5104
5177
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5105
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5178
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5106
5179
  return;
5107
5180
  }
5108
5181
  const e = raw;
5109
- requireNonEmptyString(e["id"], `${path42}.id`, errors);
5110
- requireNonEmptyString(e["description"], `${path42}.description`, errors);
5182
+ requireNonEmptyString(e["id"], `${path43}.id`, errors);
5183
+ requireNonEmptyString(e["description"], `${path43}.description`, errors);
5111
5184
  if (!VALID_METHODS.includes(e["method"])) {
5112
5185
  errors.push({
5113
- path: `${path42}.method`,
5186
+ path: `${path43}.method`,
5114
5187
  message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
5115
5188
  });
5116
5189
  }
5117
5190
  if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
5118
5191
  errors.push({
5119
- path: `${path42}.path`,
5192
+ path: `${path43}.path`,
5120
5193
  message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
5121
5194
  });
5122
5195
  }
5123
5196
  if (typeof e["auth"] !== "boolean") {
5124
- errors.push({ path: `${path42}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
5197
+ errors.push({ path: `${path43}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
5125
5198
  }
5126
5199
  if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
5127
5200
  errors.push({
5128
- path: `${path42}.successStatus`,
5201
+ path: `${path43}.successStatus`,
5129
5202
  message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
5130
5203
  });
5131
5204
  }
5132
- requireNonEmptyString(e["successDescription"], `${path42}.successDescription`, errors);
5205
+ requireNonEmptyString(e["successDescription"], `${path43}.successDescription`, errors);
5133
5206
  if (e["request"] !== void 0) {
5134
- validateRequestSchema(e["request"], `${path42}.request`, errors);
5207
+ validateRequestSchema(e["request"], `${path43}.request`, errors);
5135
5208
  }
5136
5209
  if (e["errors"] !== void 0) {
5137
5210
  if (!Array.isArray(e["errors"])) {
5138
- errors.push({ path: `${path42}.errors`, message: "Must be an array if present" });
5211
+ errors.push({ path: `${path43}.errors`, message: "Must be an array if present" });
5139
5212
  } else {
5140
5213
  const errs = e["errors"];
5141
5214
  if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
5142
- errors.push({ path: `${path42}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
5215
+ errors.push({ path: `${path43}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
5143
5216
  }
5144
5217
  for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
5145
- validateResponseError(errs[j2], `${path42}.errors[${j2}]`, errors);
5218
+ validateResponseError(errs[j2], `${path43}.errors[${j2}]`, errors);
5146
5219
  }
5147
5220
  }
5148
5221
  }
5149
5222
  }
5150
- function validateRequestSchema(raw, path42, errors) {
5223
+ function validateRequestSchema(raw, path43, errors) {
5151
5224
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5152
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5225
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5153
5226
  return;
5154
5227
  }
5155
5228
  const r = raw;
5156
5229
  for (const key of ["body", "query", "params"]) {
5157
5230
  if (r[key] !== void 0) {
5158
- validateFieldMap(r[key], `${path42}.${key}`, errors);
5231
+ validateFieldMap(r[key], `${path43}.${key}`, errors);
5159
5232
  }
5160
5233
  }
5161
5234
  }
5162
- function validateFieldMap(raw, path42, errors) {
5235
+ function validateFieldMap(raw, path43, errors) {
5163
5236
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5164
- errors.push({ path: path42, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
5237
+ errors.push({ path: path43, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
5165
5238
  return;
5166
5239
  }
5167
5240
  const map = raw;
5168
5241
  for (const [k2, v2] of Object.entries(map)) {
5169
5242
  if (typeof v2 !== "string") {
5170
- errors.push({ path: `${path42}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
5243
+ errors.push({ path: `${path43}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
5171
5244
  }
5172
5245
  }
5173
5246
  }
5174
- function validateResponseError(raw, path42, errors) {
5247
+ function validateResponseError(raw, path43, errors) {
5175
5248
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5176
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5249
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5177
5250
  return;
5178
5251
  }
5179
5252
  const e = raw;
5180
5253
  if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
5181
- errors.push({ path: `${path42}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5254
+ errors.push({ path: `${path43}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5182
5255
  }
5183
- requireNonEmptyString(e["code"], `${path42}.code`, errors);
5184
- requireNonEmptyString(e["description"], `${path42}.description`, errors);
5256
+ requireNonEmptyString(e["code"], `${path43}.code`, errors);
5257
+ requireNonEmptyString(e["description"], `${path43}.description`, errors);
5185
5258
  }
5186
- function validateBehavior(raw, path42, errors) {
5259
+ function validateBehavior(raw, path43, errors) {
5187
5260
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5188
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5261
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5189
5262
  return;
5190
5263
  }
5191
5264
  const b = raw;
5192
- requireNonEmptyString(b["id"], `${path42}.id`, errors);
5193
- requireNonEmptyString(b["description"], `${path42}.description`, errors);
5265
+ requireNonEmptyString(b["id"], `${path43}.id`, errors);
5266
+ requireNonEmptyString(b["description"], `${path43}.description`, errors);
5194
5267
  if (b["constraints"] !== void 0) {
5195
5268
  if (!Array.isArray(b["constraints"])) {
5196
- errors.push({ path: `${path42}.constraints`, message: "Must be an array of strings if present" });
5269
+ errors.push({ path: `${path43}.constraints`, message: "Must be an array of strings if present" });
5197
5270
  } else {
5198
5271
  const cs2 = b["constraints"];
5199
5272
  for (let j2 = 0; j2 < cs2.length; j2++) {
5200
5273
  if (typeof cs2[j2] !== "string") {
5201
- errors.push({ path: `${path42}.constraints[${j2}]`, message: "Must be a string" });
5274
+ errors.push({ path: `${path43}.constraints[${j2}]`, message: "Must be a string" });
5202
5275
  }
5203
5276
  }
5204
5277
  }
5205
5278
  }
5206
5279
  }
5207
- function validateComponent(raw, path42, errors) {
5280
+ function validateComponent(raw, path43, errors) {
5208
5281
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5209
- errors.push({ path: path42, message: `Must be an object, got: ${typeLabel(raw)}` });
5282
+ errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
5210
5283
  return;
5211
5284
  }
5212
5285
  const c = raw;
5213
- requireNonEmptyString(c["id"], `${path42}.id`, errors);
5214
- requireNonEmptyString(c["name"], `${path42}.name`, errors);
5215
- requireNonEmptyString(c["description"], `${path42}.description`, errors);
5286
+ requireNonEmptyString(c["id"], `${path43}.id`, errors);
5287
+ requireNonEmptyString(c["name"], `${path43}.name`, errors);
5288
+ requireNonEmptyString(c["description"], `${path43}.description`, errors);
5216
5289
  if (c["props"] !== void 0) {
5217
5290
  if (!Array.isArray(c["props"])) {
5218
- errors.push({ path: `${path42}.props`, message: "Must be an array if present" });
5291
+ errors.push({ path: `${path43}.props`, message: "Must be an array if present" });
5219
5292
  } else {
5220
5293
  const props = c["props"];
5221
5294
  for (let j2 = 0; j2 < props.length; j2++) {
5222
5295
  const p = props[j2];
5223
5296
  if (typeof p !== "object" || p === null) {
5224
- errors.push({ path: `${path42}.props[${j2}]`, message: "Must be an object" });
5297
+ errors.push({ path: `${path43}.props[${j2}]`, message: "Must be an object" });
5225
5298
  continue;
5226
5299
  }
5227
- requireNonEmptyString(p["name"], `${path42}.props[${j2}].name`, errors);
5228
- requireNonEmptyString(p["type"], `${path42}.props[${j2}].type`, errors);
5300
+ requireNonEmptyString(p["name"], `${path43}.props[${j2}].name`, errors);
5301
+ requireNonEmptyString(p["type"], `${path43}.props[${j2}].type`, errors);
5229
5302
  if (typeof p["required"] !== "boolean") {
5230
- errors.push({ path: `${path42}.props[${j2}].required`, message: "Must be boolean" });
5303
+ errors.push({ path: `${path43}.props[${j2}].required`, message: "Must be boolean" });
5231
5304
  }
5232
5305
  }
5233
5306
  }
5234
5307
  }
5235
5308
  if (c["events"] !== void 0) {
5236
5309
  if (!Array.isArray(c["events"])) {
5237
- errors.push({ path: `${path42}.events`, message: "Must be an array if present" });
5310
+ errors.push({ path: `${path43}.events`, message: "Must be an array if present" });
5238
5311
  } else {
5239
5312
  const events = c["events"];
5240
5313
  for (let j2 = 0; j2 < events.length; j2++) {
5241
5314
  const e = events[j2];
5242
5315
  if (typeof e !== "object" || e === null) {
5243
- errors.push({ path: `${path42}.events[${j2}]`, message: "Must be an object" });
5316
+ errors.push({ path: `${path43}.events[${j2}]`, message: "Must be an object" });
5244
5317
  continue;
5245
5318
  }
5246
- requireNonEmptyString(e["name"], `${path42}.events[${j2}].name`, errors);
5319
+ requireNonEmptyString(e["name"], `${path43}.events[${j2}].name`, errors);
5247
5320
  }
5248
5321
  }
5249
5322
  }
5250
5323
  if (c["state"] !== void 0) {
5251
5324
  if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
5252
- errors.push({ path: `${path42}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5325
+ errors.push({ path: `${path43}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5253
5326
  }
5254
5327
  }
5255
5328
  if (c["apiCalls"] !== void 0) {
5256
5329
  if (!Array.isArray(c["apiCalls"])) {
5257
- errors.push({ path: `${path42}.apiCalls`, message: "Must be an array of strings if present" });
5330
+ errors.push({ path: `${path43}.apiCalls`, message: "Must be an array of strings if present" });
5258
5331
  }
5259
5332
  }
5260
5333
  }
@@ -5316,10 +5389,10 @@ function crossReferenceChecks(obj, errors) {
5316
5389
  }
5317
5390
  }
5318
5391
  }
5319
- function requireNonEmptyString(v2, path42, errors) {
5392
+ function requireNonEmptyString(v2, path43, errors) {
5320
5393
  if (typeof v2 !== "string" || v2.trim().length === 0) {
5321
5394
  errors.push({
5322
- path: path42,
5395
+ path: path43,
5323
5396
  message: `Must be a non-empty string, got: ${typeLabel(v2)}`
5324
5397
  });
5325
5398
  }
@@ -5530,13 +5603,18 @@ Output ONLY the corrected JSON. No explanation.`;
5530
5603
 
5531
5604
  // core/token-budget.ts
5532
5605
  var import_chalk5 = __toESM(require("chalk"));
5533
- var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5534
- function estimateTokens(text) {
5535
- if (!text) return 0;
5536
- const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5537
- const nonCjkLength = text.length - cjkCount;
5538
- return Math.ceil(cjkCount + nonCjkLength / 4);
5539
- }
5606
+
5607
+ // core/config-defaults.ts
5608
+ var DEFAULT_LOG_DIR = ".ai-spec-logs";
5609
+ var DEFAULT_VCR_DIR = ".ai-spec-vcr";
5610
+ var DEFAULT_BACKUP_DIR = ".ai-spec-backup";
5611
+ var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5612
+ var DEFAULT_OPENAPI_SERVER_URL = "http://localhost:3000";
5613
+ var DEFAULT_MAX_COMMAND_OUTPUT_CHARS = 3e4;
5614
+ var DEFAULT_MAX_FIX_FILE_CHARS = 6e4;
5615
+ var DEFAULT_DSL_MAX_RETRIES = 2;
5616
+ var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
5617
+ var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
5540
5618
  var DEFAULT_TOKEN_BUDGETS = {
5541
5619
  gemini: 9e5,
5542
5620
  claude: 18e4,
@@ -5544,8 +5622,18 @@ var DEFAULT_TOKEN_BUDGETS = {
5544
5622
  deepseek: 6e4,
5545
5623
  default: 1e5
5546
5624
  };
5625
+
5626
+ // core/token-budget.ts
5627
+ var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5628
+ function estimateTokens(text) {
5629
+ if (!text) return 0;
5630
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5631
+ const nonCjkLength = text.length - cjkCount;
5632
+ return Math.ceil(cjkCount + nonCjkLength / 4);
5633
+ }
5634
+ var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
5547
5635
  function getDefaultBudget(providerName) {
5548
- return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
5636
+ return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
5549
5637
  }
5550
5638
 
5551
5639
  // core/safe-json.ts
@@ -5616,7 +5704,7 @@ function sanitizeDsl(raw) {
5616
5704
  }
5617
5705
  return dsl;
5618
5706
  }
5619
- var MAX_RETRIES = 2;
5707
+ var MAX_RETRIES = DEFAULT_DSL_MAX_RETRIES;
5620
5708
  var DEFAULT_MAX_SPEC_CHARS = 12e3;
5621
5709
  function dslFilePath(specFilePath) {
5622
5710
  const dir = path7.dirname(specFilePath);
@@ -6340,12 +6428,11 @@ Shared component structure patterns:`);
6340
6428
  // core/run-snapshot.ts
6341
6429
  var fs10 = __toESM(require("fs-extra"));
6342
6430
  var path9 = __toESM(require("path"));
6343
- var BACKUP_DIR = ".ai-spec-backup";
6344
6431
  var RunSnapshot = class {
6345
6432
  constructor(workingDir, runId) {
6346
6433
  this.workingDir = workingDir;
6347
6434
  this.runId = runId;
6348
- this.backupRoot = path9.join(workingDir, BACKUP_DIR, runId);
6435
+ this.backupRoot = path9.join(workingDir, DEFAULT_BACKUP_DIR, runId);
6349
6436
  }
6350
6437
  backupRoot;
6351
6438
  snapshotted = /* @__PURE__ */ new Set();
@@ -6401,7 +6488,6 @@ function getActiveSnapshot() {
6401
6488
  var fs11 = __toESM(require("fs-extra"));
6402
6489
  var path10 = __toESM(require("path"));
6403
6490
  var import_chalk7 = __toESM(require("chalk"));
6404
- var LOG_DIR = ".ai-spec-logs";
6405
6491
  function appendJsonlLine(filePath, record) {
6406
6492
  try {
6407
6493
  fs11.appendFileSync(filePath, JSON.stringify(record) + "\n");
@@ -6472,8 +6558,8 @@ var RunLogger = class {
6472
6558
  this.workingDir = workingDir;
6473
6559
  this.runId = runId;
6474
6560
  this.startMs = Date.now();
6475
- this.logPath = path10.join(workingDir, LOG_DIR, `${runId}.json`);
6476
- this.jsonlPath = path10.join(workingDir, LOG_DIR, `${runId}.jsonl`);
6561
+ this.logPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.json`);
6562
+ this.jsonlPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.jsonl`);
6477
6563
  this.log = {
6478
6564
  runId,
6479
6565
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7415,7 +7501,7 @@ ${context.routeSummary}
7415
7501
  }
7416
7502
  if (context.schema) {
7417
7503
  parts.push(`=== Prisma Schema ===
7418
- ${context.schema.slice(0, 4e3)}
7504
+ ${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
7419
7505
  `);
7420
7506
  }
7421
7507
  if (context.errorPatterns) {
@@ -7463,7 +7549,7 @@ async function loadAccumulatedLessons(projectRoot) {
7463
7549
  const nextSection = section.slice(marker.length).match(/\n## \d/);
7464
7550
  return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
7465
7551
  }
7466
- var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
7552
+ var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
7467
7553
  async function loadReviewHistory(projectRoot) {
7468
7554
  const historyPath = path13.join(projectRoot, REVIEW_HISTORY_FILE);
7469
7555
  try {
@@ -7594,15 +7680,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
7594
7680
  ${codeContext}`;
7595
7681
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
7596
7682
  console.log(import_chalk11.default.gray(" Pass 2/3: Implementation review..."));
7683
+ const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
7597
7684
  const history = await loadReviewHistory(this.projectRoot);
7598
7685
  const historyContext = buildHistoryContext(history);
7599
7686
  const implPrompt = `Review the implementation details of this change.
7600
7687
 
7601
- === Feature Spec ===
7602
- ${specContent || "(No spec \u2014 review for general code quality)"}
7603
-
7604
- === Code ===
7605
- ${codeContext}
7688
+ === Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
7689
+ ${specDigest}
7606
7690
 
7607
7691
  === Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
7608
7692
  ${archReview}
@@ -7611,11 +7695,8 @@ ${historyContext}`;
7611
7695
  console.log(import_chalk11.default.gray(" Pass 3/3: Impact & complexity assessment..."));
7612
7696
  const impactPrompt = `Assess the impact and complexity of this change.
7613
7697
 
7614
- === Feature Spec ===
7615
- ${specContent || "(No spec \u2014 review for general code quality)"}
7616
-
7617
- === Code ===
7618
- ${codeContext}
7698
+ === Feature Spec (digest) ===
7699
+ ${specDigest}
7619
7700
 
7620
7701
  === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
7621
7702
  ${archReview}
@@ -7689,8 +7770,8 @@ ${sep}
7689
7770
  filesSection += `
7690
7771
 
7691
7772
  === ${filePath} ===
7692
- ${content.slice(0, 3e3)}`;
7693
- if (content.length > 3e3) filesSection += `
7773
+ ${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
7774
+ if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
7694
7775
  ... (truncated, ${content.length} chars total)`;
7695
7776
  } catch {
7696
7777
  filesSection += `
@@ -8295,8 +8376,8 @@ var import_child_process5 = require("child_process");
8295
8376
  var fs18 = __toESM(require("fs-extra"));
8296
8377
  var path17 = __toESM(require("path"));
8297
8378
  init_cli_ui();
8298
- var MAX_COMMAND_OUTPUT_CHARS = 5e4;
8299
- var MAX_FIX_FILE_CHARS = 6e4;
8379
+ var MAX_COMMAND_OUTPUT_CHARS = DEFAULT_MAX_COMMAND_OUTPUT_CHARS;
8380
+ var MAX_FIX_FILE_CHARS = DEFAULT_MAX_FIX_FILE_CHARS;
8300
8381
  function runCommand(cmd, cwd) {
8301
8382
  try {
8302
8383
  const output = (0, import_child_process5.execSync)(cmd, { cwd, encoding: "utf-8", timeout: 6e4 });
@@ -9505,8 +9586,8 @@ async function findLatestDslFile(projectDir) {
9505
9586
  const entries = await fs22.readdir(dir);
9506
9587
  for (const entry of entries) {
9507
9588
  const abs = path21.join(dir, entry);
9508
- const stat4 = await fs22.stat(abs);
9509
- if (stat4.isDirectory()) {
9589
+ const stat5 = await fs22.stat(abs);
9590
+ if (stat5.isDirectory()) {
9510
9591
  await scan(abs);
9511
9592
  } else if (entry.endsWith(".dsl.json")) {
9512
9593
  allFiles.push(abs);
@@ -11244,7 +11325,7 @@ ${optionsText}`;
11244
11325
  var import_crypto2 = require("crypto");
11245
11326
  var fs24 = __toESM(require("fs-extra"));
11246
11327
  var path23 = __toESM(require("path"));
11247
- var VCR_DIR = ".ai-spec-vcr";
11328
+ var VCR_DIR = DEFAULT_VCR_DIR;
11248
11329
  var VcrRecordingProvider = class {
11249
11330
  constructor(inner) {
11250
11331
  this.inner = inner;
@@ -11942,7 +12023,147 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
11942
12023
  }
11943
12024
  }
11944
12025
 
12026
+ // core/repo-store.ts
12027
+ var fs26 = __toESM(require("fs-extra"));
12028
+ var path25 = __toESM(require("path"));
12029
+ var os5 = __toESM(require("os"));
12030
+ var REPO_STORE_FILE = path25.join(os5.homedir(), ".ai-spec-repos.json");
12031
+ async function readStore2() {
12032
+ try {
12033
+ if (await fs26.pathExists(REPO_STORE_FILE)) {
12034
+ return await fs26.readJson(REPO_STORE_FILE);
12035
+ }
12036
+ } catch (err) {
12037
+ console.warn(`Warning: Could not read repo store at ${REPO_STORE_FILE}: ${err.message}.`);
12038
+ }
12039
+ return { repos: [] };
12040
+ }
12041
+ async function writeStore2(store) {
12042
+ await fs26.ensureFile(REPO_STORE_FILE);
12043
+ await fs26.writeJson(REPO_STORE_FILE, store, { spaces: 2 });
12044
+ }
12045
+ async function getRegisteredRepos() {
12046
+ const store = await readStore2();
12047
+ return store.repos;
12048
+ }
12049
+ async function registerRepo(repo) {
12050
+ const store = await readStore2();
12051
+ const idx = store.repos.findIndex((r) => r.path === repo.path);
12052
+ if (idx >= 0) {
12053
+ store.repos[idx] = repo;
12054
+ } else {
12055
+ store.repos.push(repo);
12056
+ }
12057
+ await writeStore2(store);
12058
+ }
12059
+
11945
12060
  // cli/commands/create.ts
12061
+ init_spec_generator();
12062
+ async function promptRepoRole() {
12063
+ return (0, import_prompts7.select)({
12064
+ message: "What type of repo is this?",
12065
+ choices: [
12066
+ { name: "Frontend", value: "frontend" },
12067
+ { name: "Backend", value: "backend" },
12068
+ { name: "Mobile", value: "mobile" },
12069
+ { name: "Shared / Other", value: "shared" }
12070
+ ]
12071
+ });
12072
+ }
12073
+ async function quickRegisterRepo(provider) {
12074
+ const roleOverride = await promptRepoRole();
12075
+ const roleLabels = {
12076
+ frontend: "frontend",
12077
+ backend: "backend",
12078
+ mobile: "mobile",
12079
+ shared: "shared"
12080
+ };
12081
+ const raw = await (0, import_prompts7.input)({
12082
+ message: `Enter your ${roleLabels[roleOverride]} repo path (absolute path):`,
12083
+ validate: (v2) => {
12084
+ const trimmed = v2.trim();
12085
+ if (trimmed.length === 0) return "Path cannot be empty";
12086
+ if (!path26.isAbsolute(trimmed)) return "Please provide an absolute path";
12087
+ return true;
12088
+ }
12089
+ });
12090
+ const cleaned = raw.trim().replace(/\\ /g, " ");
12091
+ const resolved = path26.resolve(cleaned);
12092
+ if (!await fs27.pathExists(resolved)) {
12093
+ console.log(import_chalk26.default.red(` Path does not exist: ${resolved}`));
12094
+ return quickRegisterRepo(provider);
12095
+ }
12096
+ const { type, role: detectedRole } = await detectRepoType(resolved);
12097
+ const role = roleOverride ?? detectedRole;
12098
+ const repoName = path26.basename(resolved);
12099
+ console.log(import_chalk26.default.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
12100
+ const constitutionPath = path26.join(resolved, CONSTITUTION_FILE);
12101
+ let hasConstitution = await fs27.pathExists(constitutionPath);
12102
+ if (!hasConstitution) {
12103
+ console.log(import_chalk26.default.blue(` Generating constitution for ${repoName}...`));
12104
+ try {
12105
+ const gen = new ConstitutionGenerator(provider);
12106
+ const content = await gen.generate(resolved);
12107
+ await gen.saveConstitution(resolved, content);
12108
+ hasConstitution = true;
12109
+ console.log(import_chalk26.default.green(` \u2714 Constitution saved`));
12110
+ } catch (err) {
12111
+ console.log(import_chalk26.default.yellow(` \u26A0 Constitution failed: ${err.message}`));
12112
+ }
12113
+ }
12114
+ const entry = {
12115
+ name: repoName,
12116
+ path: resolved,
12117
+ type,
12118
+ role,
12119
+ hasConstitution,
12120
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
12121
+ };
12122
+ await registerRepo(entry);
12123
+ console.log(import_chalk26.default.green(` \u2714 Repo registered: ${repoName}`));
12124
+ return entry;
12125
+ }
12126
+ async function selectRepos(registeredRepos, provider) {
12127
+ const ADD_NEW = "__add_new__";
12128
+ const choices = [
12129
+ ...registeredRepos.map((r) => ({
12130
+ name: `${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`,
12131
+ value: r.path
12132
+ })),
12133
+ { name: import_chalk26.default.cyan("+ Add new repo"), value: ADD_NEW }
12134
+ ];
12135
+ const selected = await (0, import_prompts7.checkbox)({
12136
+ message: "Select repo(s) for this feature (space to toggle, enter to confirm):",
12137
+ choices,
12138
+ required: true
12139
+ });
12140
+ const result = [];
12141
+ for (const val of selected) {
12142
+ if (val === ADD_NEW) {
12143
+ const newRepo = await quickRegisterRepo(provider);
12144
+ result.push(newRepo);
12145
+ } else {
12146
+ const repo = registeredRepos.find((r) => r.path === val);
12147
+ if (repo) result.push(repo);
12148
+ }
12149
+ }
12150
+ if (result.length === 0) {
12151
+ console.log(import_chalk26.default.yellow(" No repos selected. Please select at least one."));
12152
+ return selectRepos(registeredRepos, provider);
12153
+ }
12154
+ return result;
12155
+ }
12156
+ function buildWorkspaceConfig(repos) {
12157
+ return {
12158
+ name: "ai-spec-workspace",
12159
+ repos: repos.map((r) => ({
12160
+ name: r.name,
12161
+ path: r.path,
12162
+ type: r.type,
12163
+ role: r.role
12164
+ }))
12165
+ };
12166
+ }
11946
12167
  function registerCreate(program2) {
11947
12168
  program2.command("create").description("Generate a feature spec and kick off code generation").argument("[idea]", "Feature idea in natural language (prompted if omitted)").option(
11948
12169
  "--provider <name>",
@@ -11965,24 +12186,75 @@ function registerCreate(program2) {
11965
12186
  });
11966
12187
  }
11967
12188
  const workspaceLoader = new WorkspaceLoader(currentDir);
11968
- const workspaceConfig = await workspaceLoader.load();
11969
- if (workspaceConfig) {
12189
+ const existingWorkspaceConfig = await workspaceLoader.load();
12190
+ if (existingWorkspaceConfig) {
11970
12191
  console.log(import_chalk26.default.cyan(`
11971
- [Workspace] Detected workspace: ${workspaceConfig.name}`));
11972
- console.log(import_chalk26.default.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
11973
- const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
12192
+ [Workspace] Detected workspace: ${existingWorkspaceConfig.name}`));
12193
+ console.log(import_chalk26.default.gray(` Repos: ${existingWorkspaceConfig.repos.map((r) => r.name).join(", ")}`));
12194
+ const pipelineResults = await runMultiRepoPipeline(idea, existingWorkspaceConfig, opts, currentDir, config2);
11974
12195
  if (opts.serve) {
11975
12196
  await handleAutoServe(pipelineResults);
11976
12197
  }
11977
12198
  return;
11978
12199
  }
11979
- await runSingleRepoPipeline(idea, opts, currentDir, config2);
12200
+ const providerName = opts.provider || config2.provider || "gemini";
12201
+ const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
12202
+ const apiKey = await resolveApiKey(providerName, opts.key);
12203
+ const specProvider = createProvider(providerName, apiKey, modelName);
12204
+ const registeredRepos = await getRegisteredRepos();
12205
+ if (registeredRepos.length === 0) {
12206
+ console.log(import_chalk26.default.yellow("\n No repos registered. Please register repos first."));
12207
+ console.log(import_chalk26.default.gray(" Run: ai-spec init"));
12208
+ console.log(import_chalk26.default.gray(" Or add a repo now:\n"));
12209
+ const addNow = await (0, import_prompts7.select)({
12210
+ message: "Add a repo now?",
12211
+ choices: [
12212
+ { name: "Yes \u2014 register a repo and continue", value: "yes" },
12213
+ { name: "No \u2014 exit", value: "no" }
12214
+ ]
12215
+ });
12216
+ if (addNow === "no") {
12217
+ process.exit(0);
12218
+ }
12219
+ const newRepo = await quickRegisterRepo(specProvider);
12220
+ registeredRepos.push(newRepo);
12221
+ }
12222
+ const validRepos = [];
12223
+ for (const r of registeredRepos) {
12224
+ if (await fs27.pathExists(r.path)) {
12225
+ validRepos.push(r);
12226
+ } else {
12227
+ console.log(import_chalk26.default.yellow(` \u26A0 Skipping ${r.name}: path not found (${r.path})`));
12228
+ }
12229
+ }
12230
+ if (validRepos.length === 0) {
12231
+ console.log(import_chalk26.default.red(" No valid repos available. Run: ai-spec init"));
12232
+ process.exit(1);
12233
+ }
12234
+ const selectedRepos = await selectRepos(validRepos, specProvider);
12235
+ if (selectedRepos.length === 1) {
12236
+ const repo = selectedRepos[0];
12237
+ console.log(import_chalk26.default.cyan(`
12238
+ [Repo] ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
12239
+ await runSingleRepoPipeline(idea, opts, repo.path, config2);
12240
+ } else {
12241
+ const workspaceConfig = buildWorkspaceConfig(selectedRepos);
12242
+ console.log(import_chalk26.default.cyan(`
12243
+ [Workspace] ${selectedRepos.length} repo(s) selected:`));
12244
+ for (const repo of selectedRepos) {
12245
+ console.log(import_chalk26.default.gray(` ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
12246
+ }
12247
+ const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
12248
+ if (opts.serve) {
12249
+ await handleAutoServe(pipelineResults);
12250
+ }
12251
+ }
11980
12252
  });
11981
12253
  }
11982
12254
 
11983
12255
  // cli/commands/review.ts
11984
- var path25 = __toESM(require("path"));
11985
- var fs26 = __toESM(require("fs-extra"));
12256
+ var path27 = __toESM(require("path"));
12257
+ var fs28 = __toESM(require("fs-extra"));
11986
12258
  var import_chalk27 = __toESM(require("chalk"));
11987
12259
  init_spec_generator();
11988
12260
  function registerReview(program2) {
@@ -12000,17 +12272,17 @@ function registerReview(program2) {
12000
12272
  const reviewer = new CodeReviewer(provider, currentDir);
12001
12273
  let specContent = "";
12002
12274
  let resolvedSpecFile;
12003
- if (specFile && await fs26.pathExists(specFile)) {
12004
- specContent = await fs26.readFile(specFile, "utf-8");
12275
+ if (specFile && await fs28.pathExists(specFile)) {
12276
+ specContent = await fs28.readFile(specFile, "utf-8");
12005
12277
  resolvedSpecFile = specFile;
12006
12278
  console.log(import_chalk27.default.gray(`Using spec: ${specFile}`));
12007
12279
  } else {
12008
- const specsDir = path25.join(currentDir, "specs");
12009
- if (await fs26.pathExists(specsDir)) {
12010
- const files = (await fs26.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
12280
+ const specsDir = path27.join(currentDir, "specs");
12281
+ if (await fs28.pathExists(specsDir)) {
12282
+ const files = (await fs28.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
12011
12283
  if (files.length > 0) {
12012
- const latest = path25.join(specsDir, files[0]);
12013
- specContent = await fs26.readFile(latest, "utf-8");
12284
+ const latest = path27.join(specsDir, files[0]);
12285
+ specContent = await fs28.readFile(latest, "utf-8");
12014
12286
  resolvedSpecFile = latest;
12015
12287
  console.log(import_chalk27.default.gray(`Auto-detected spec: specs/${files[0]}`));
12016
12288
  }
@@ -12025,9 +12297,10 @@ function registerReview(program2) {
12025
12297
  }
12026
12298
 
12027
12299
  // cli/commands/init.ts
12028
- var path27 = __toESM(require("path"));
12029
- var fs28 = __toESM(require("fs-extra"));
12300
+ var path28 = __toESM(require("path"));
12301
+ var fs29 = __toESM(require("fs-extra"));
12030
12302
  var import_chalk28 = __toESM(require("chalk"));
12303
+ var import_prompts8 = require("@inquirer/prompts");
12031
12304
  init_spec_generator();
12032
12305
 
12033
12306
  // prompts/global-constitution.prompt.ts
@@ -12089,240 +12362,139 @@ ${summary}
12089
12362
  return parts.join("\n");
12090
12363
  }
12091
12364
 
12092
- // core/project-index.ts
12093
- var fs27 = __toESM(require("fs-extra"));
12094
- var path26 = __toESM(require("path"));
12095
- var INDEX_FILE = ".ai-spec-index.json";
12096
- var KEY_DEPS = [
12097
- // Frameworks
12098
- "express",
12099
- "fastify",
12100
- "koa",
12101
- "@nestjs/core",
12102
- "hapi",
12103
- "next",
12104
- "react",
12105
- "vue",
12106
- "nuxt",
12107
- "svelte",
12108
- "react-native",
12109
- "expo",
12110
- // DB / ORM
12111
- "prisma",
12112
- "@prisma/client",
12113
- "mongoose",
12114
- "typeorm",
12115
- "sequelize",
12116
- "drizzle-orm",
12117
- // Auth
12118
- "jsonwebtoken",
12119
- "passport",
12120
- "next-auth",
12121
- "@clerk/nextjs",
12122
- // Build / Lang
12123
- "typescript",
12124
- "vite",
12125
- "webpack",
12126
- "esbuild",
12127
- "turbo",
12128
- // Testing
12129
- "jest",
12130
- "vitest",
12131
- "mocha",
12132
- "cypress",
12133
- "playwright",
12134
- // Infra
12135
- "redis",
12136
- "bull",
12137
- "socket.io",
12138
- "graphql",
12139
- "@trpc/server"
12140
- ];
12141
- var SKIP_DIRS = /* @__PURE__ */ new Set([
12142
- "node_modules",
12143
- ".git",
12144
- ".svn",
12145
- "dist",
12146
- "build",
12147
- "out",
12148
- ".next",
12149
- ".nuxt",
12150
- "coverage",
12151
- ".turbo",
12152
- ".cache",
12153
- "__pycache__",
12154
- "vendor",
12155
- ".ai-spec-vcr",
12156
- ".ai-spec-logs",
12157
- "specs"
12158
- ]);
12159
- var MANIFEST_FILES = [
12160
- "package.json",
12161
- "go.mod",
12162
- "Cargo.toml",
12163
- "pom.xml",
12164
- "build.gradle",
12165
- "build.gradle.kts",
12166
- "requirements.txt",
12167
- "pyproject.toml",
12168
- "setup.py",
12169
- "composer.json"
12170
- ];
12171
- async function isProjectRoot(absPath) {
12172
- for (const manifest of MANIFEST_FILES) {
12173
- if (await fs27.pathExists(path26.join(absPath, manifest))) return true;
12365
+ // cli/commands/init.ts
12366
+ var ROLE_LABELS = {
12367
+ frontend: "frontend",
12368
+ backend: "backend",
12369
+ mobile: "mobile",
12370
+ shared: "shared"
12371
+ };
12372
+ async function promptRepoRole2() {
12373
+ return (0, import_prompts8.select)({
12374
+ message: "What type of repo is this?",
12375
+ choices: [
12376
+ { name: "Frontend", value: "frontend" },
12377
+ { name: "Backend", value: "backend" },
12378
+ { name: "Mobile", value: "mobile" },
12379
+ { name: "Shared / Other", value: "shared" }
12380
+ ]
12381
+ });
12382
+ }
12383
+ async function promptRepoPath(role) {
12384
+ const label = ROLE_LABELS[role];
12385
+ const raw = await (0, import_prompts8.input)({
12386
+ message: `Enter your ${label} repo path (absolute path):`,
12387
+ validate: (v2) => {
12388
+ const trimmed = v2.trim();
12389
+ if (trimmed.length === 0) return "Path cannot be empty";
12390
+ if (!path28.isAbsolute(trimmed)) return "Please provide an absolute path";
12391
+ return true;
12392
+ }
12393
+ });
12394
+ const cleaned = raw.trim().replace(/\\ /g, " ");
12395
+ const resolved = path28.resolve(cleaned);
12396
+ if (!await fs29.pathExists(resolved)) {
12397
+ console.log(import_chalk28.default.red(` Path does not exist: ${resolved}`));
12398
+ return promptRepoPath(role);
12399
+ }
12400
+ const stat5 = await fs29.stat(resolved);
12401
+ if (!stat5.isDirectory()) {
12402
+ console.log(import_chalk28.default.red(` Not a directory: ${resolved}`));
12403
+ return promptRepoPath(role);
12404
+ }
12405
+ return resolved;
12406
+ }
12407
+ async function registerSingleRepo(repoPath, provider, roleOverride) {
12408
+ const { type, role: detectedRole } = await detectRepoType(repoPath);
12409
+ const role = roleOverride ?? detectedRole;
12410
+ const repoName = path28.basename(repoPath);
12411
+ console.log(import_chalk28.default.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
12412
+ const constitutionPath = path28.join(repoPath, CONSTITUTION_FILE);
12413
+ let hasConstitution = await fs29.pathExists(constitutionPath);
12414
+ if (!hasConstitution) {
12415
+ console.log(import_chalk28.default.blue(` Generating project constitution for ${repoName}...`));
12416
+ try {
12417
+ const gen = new ConstitutionGenerator(provider);
12418
+ const content = await gen.generate(repoPath);
12419
+ await gen.saveConstitution(repoPath, content);
12420
+ hasConstitution = true;
12421
+ console.log(import_chalk28.default.green(` \u2714 Constitution saved: ${constitutionPath}`));
12422
+ } catch (err) {
12423
+ console.log(import_chalk28.default.yellow(` \u26A0 Constitution generation failed: ${err.message}`));
12424
+ }
12425
+ } else {
12426
+ console.log(import_chalk28.default.green(` \u2714 Constitution already exists: ${constitutionPath}`));
12427
+ }
12428
+ const entry = {
12429
+ name: repoName,
12430
+ path: repoPath,
12431
+ type,
12432
+ role,
12433
+ hasConstitution,
12434
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
12435
+ };
12436
+ await registerRepo(entry);
12437
+ return entry;
12438
+ }
12439
+ async function buildProjectSummaries(repos) {
12440
+ const summaries = [];
12441
+ for (const repo of repos) {
12442
+ if (!await fs29.pathExists(repo.path)) continue;
12443
+ const lines = [
12444
+ `Type: ${repo.type} (${repo.role})`
12445
+ ];
12446
+ try {
12447
+ const loader = new ContextLoader(repo.path);
12448
+ const ctx = await loader.loadProjectContext();
12449
+ lines.push(`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`);
12450
+ lines.push(`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`);
12451
+ } catch {
12452
+ lines.push(`Tech stack: ${repo.type}`);
12453
+ }
12454
+ if (repo.hasConstitution) {
12455
+ try {
12456
+ const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
12457
+ const raw = await fs29.readFile(constitutionPath, "utf-8");
12458
+ lines.push("", "Constitution excerpt:", raw.slice(0, 2e3));
12459
+ } catch {
12460
+ }
12461
+ }
12462
+ summaries.push({ name: repo.name, summary: lines.join("\n") });
12174
12463
  }
12175
- return false;
12464
+ return summaries;
12176
12465
  }
12177
- async function extractTechStack(absPath, type) {
12178
- const stack = [];
12179
- if (type === "go") stack.push("go");
12180
- if (type === "rust") stack.push("rust");
12181
- if (type === "java") stack.push("java");
12182
- if (type === "python") stack.push("python");
12183
- if (type === "php") stack.push("php");
12184
- const pkgPath = path26.join(absPath, "package.json");
12185
- if (!await fs27.pathExists(pkgPath)) return stack;
12186
- let pkg = {};
12466
+ async function generateGlobalConstitution(provider, repos, currentDir) {
12467
+ console.log(import_chalk28.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12468
+ console.log(import_chalk28.default.gray(` Based on ${repos.length} registered repo(s)`));
12469
+ const summaries = await buildProjectSummaries(repos);
12470
+ if (summaries.length === 0) {
12471
+ console.log(import_chalk28.default.yellow(" No valid repos found \u2014 skipping global constitution."));
12472
+ return;
12473
+ }
12474
+ const prompt = buildGlobalConstitutionPrompt(summaries);
12475
+ let globalConstitution;
12187
12476
  try {
12188
- pkg = await fs27.readJson(pkgPath);
12189
- } catch {
12190
- return stack;
12477
+ globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
12478
+ } catch (err) {
12479
+ console.error(import_chalk28.default.red(` \u2718 Failed to generate global constitution: ${err.message}`));
12480
+ return;
12191
12481
  }
12192
- const allDeps = {
12193
- ...pkg.dependencies ?? {},
12194
- ...pkg.devDependencies ?? {}
12195
- };
12196
- const depKeys = new Set(Object.keys(allDeps));
12197
- for (const dep of KEY_DEPS) {
12198
- if (depKeys.has(dep)) stack.push(dep);
12482
+ const saved = await saveGlobalConstitution(globalConstitution, currentDir);
12483
+ console.log(import_chalk28.default.green(` \u2714 Global constitution saved: ${saved}`));
12484
+ console.log(import_chalk28.default.gray(" Project constitutions will be merged with this at runtime."));
12485
+ const lines = globalConstitution.split("\n");
12486
+ console.log(import_chalk28.default.bold("\n Preview:"));
12487
+ console.log(import_chalk28.default.gray(lines.slice(0, 10).join("\n")));
12488
+ if (lines.length > 10) {
12489
+ console.log(import_chalk28.default.gray(` ... (${lines.length} lines total)`));
12199
12490
  }
12200
- return stack;
12201
12491
  }
12202
- async function discoverProjects(rootDir, maxDepth) {
12203
- const found = [];
12204
- async function walk(absDir, depth) {
12205
- if (depth > maxDepth) return;
12206
- let entries;
12207
- try {
12208
- entries = await fs27.readdir(absDir, { withFileTypes: true });
12209
- } catch {
12210
- return;
12211
- }
12212
- for (const entry of entries) {
12213
- if (!entry.isDirectory()) continue;
12214
- if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
12215
- const childAbs = path26.join(absDir, entry.name);
12216
- const gitPath = path26.join(childAbs, ".git");
12217
- if (await fs27.pathExists(gitPath)) {
12218
- const gitStat = await fs27.stat(gitPath);
12219
- if (gitStat.isFile()) continue;
12220
- }
12221
- if (await isProjectRoot(childAbs)) {
12222
- found.push(path26.relative(rootDir, childAbs));
12223
- } else {
12224
- await walk(childAbs, depth + 1);
12225
- }
12226
- }
12227
- }
12228
- await walk(rootDir, 0);
12229
- return found;
12230
- }
12231
- async function loadIndex(scanRoot) {
12232
- const filePath = path26.join(scanRoot, INDEX_FILE);
12233
- try {
12234
- return await fs27.readJson(filePath);
12235
- } catch {
12236
- return null;
12237
- }
12238
- }
12239
- async function saveIndex(scanRoot, index) {
12240
- const filePath = path26.join(scanRoot, INDEX_FILE);
12241
- await fs27.writeJson(filePath, index, { spaces: 2 });
12242
- return filePath;
12243
- }
12244
- async function runScan(scanRoot, maxDepth = 2) {
12245
- const now = (/* @__PURE__ */ new Date()).toISOString();
12246
- const existing = await loadIndex(scanRoot);
12247
- const existingMap = new Map(
12248
- (existing?.projects ?? []).map((p) => [p.path, p])
12249
- );
12250
- const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
12251
- const added = [];
12252
- const updated = [];
12253
- const unchanged = [];
12254
- const seenPaths = /* @__PURE__ */ new Set();
12255
- for (const relPath of discoveredPaths) {
12256
- const absPath = path26.join(scanRoot, relPath);
12257
- seenPaths.add(relPath);
12258
- const { type, role } = await detectRepoType(absPath);
12259
- const techStack = await extractTechStack(absPath, type);
12260
- const hasConstitution = await fs27.pathExists(path26.join(absPath, CONSTITUTION_FILE));
12261
- const hasWorkspace = await fs27.pathExists(path26.join(absPath, WORKSPACE_CONFIG_FILE));
12262
- const name = path26.basename(relPath);
12263
- const prev = existingMap.get(relPath);
12264
- if (!prev) {
12265
- const entry = {
12266
- name,
12267
- path: relPath,
12268
- type,
12269
- role,
12270
- techStack,
12271
- hasConstitution,
12272
- hasWorkspace,
12273
- firstSeen: now,
12274
- lastSeen: now
12275
- };
12276
- added.push(entry);
12277
- existingMap.set(relPath, entry);
12278
- } else {
12279
- const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
12280
- const entry = {
12281
- ...prev,
12282
- type,
12283
- role,
12284
- techStack,
12285
- hasConstitution,
12286
- hasWorkspace,
12287
- lastSeen: now,
12288
- missing: void 0
12289
- // clear missing flag if it came back
12290
- };
12291
- existingMap.set(relPath, entry);
12292
- if (changed) {
12293
- updated.push(entry);
12294
- } else {
12295
- unchanged.push(entry);
12296
- }
12297
- }
12298
- }
12299
- const nowMissing = [];
12300
- for (const [relPath, entry] of existingMap) {
12301
- if (!seenPaths.has(relPath) && !entry.missing) {
12302
- const gone = { ...entry, missing: true };
12303
- existingMap.set(relPath, gone);
12304
- nowMissing.push(gone);
12305
- }
12306
- }
12307
- const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
12308
- const index = {
12309
- scanRoot,
12310
- lastScanned: now,
12311
- projects
12312
- };
12313
- return { index, added, updated, unchanged, nowMissing };
12314
- }
12315
-
12316
- // cli/commands/init.ts
12317
12492
  function registerInit(program2) {
12318
- program2.command("init").description(`Analyze codebase and generate Project Constitution (${CONSTITUTION_FILE})`).option(
12493
+ program2.command("init").description("Setup workspace: register repos, generate constitutions").option(
12319
12494
  "--provider <name>",
12320
12495
  `AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
12321
12496
  void 0
12322
- ).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing constitution").option(
12323
- "--global",
12324
- `Generate a Global Constitution (~/${GLOBAL_CONSTITUTION_FILE}) instead of a project-level one`
12325
- ).option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules (prune & rebase)").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").action(async (opts) => {
12497
+ ).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing constitutions").option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").option("--add-repo", "Add a new repo to the registered list").action(async (opts) => {
12326
12498
  const currentDir = process.cwd();
12327
12499
  const config2 = await loadConfig(currentDir);
12328
12500
  const providerName = opts.provider || config2.provider || "gemini";
@@ -12341,7 +12513,7 @@ function registerInit(program2) {
12341
12513
  console.log(import_chalk28.default.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
12342
12514
  console.log(import_chalk28.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
12343
12515
  if (result.backupPath) {
12344
- console.log(import_chalk28.default.gray(` Backup: ${path27.basename(result.backupPath)}`));
12516
+ console.log(import_chalk28.default.gray(` Backup: ${path28.basename(result.backupPath)}`));
12345
12517
  }
12346
12518
  }
12347
12519
  } catch (err) {
@@ -12350,119 +12522,125 @@ function registerInit(program2) {
12350
12522
  }
12351
12523
  return;
12352
12524
  }
12353
- if (opts.global) {
12354
- const existing = await loadGlobalConstitution([currentDir]);
12355
- if (existing && !opts.force) {
12356
- console.log(import_chalk28.default.yellow(`
12357
- Global constitution already exists at: ${existing.source}`));
12358
- console.log(import_chalk28.default.gray(" Use --force to overwrite it."));
12359
- return;
12525
+ if (opts.addRepo) {
12526
+ console.log(import_chalk28.default.blue("\n\u2500\u2500\u2500 Register New Repo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12527
+ const role = await promptRepoRole2();
12528
+ const repoPath = await promptRepoPath(role);
12529
+ const entry = await registerSingleRepo(repoPath, provider, role);
12530
+ console.log(import_chalk28.default.green(`
12531
+ \u2714 Repo registered: ${entry.name} (${entry.type} / ${entry.role})`));
12532
+ console.log(import_chalk28.default.gray(` Saved to: ${REPO_STORE_FILE}`));
12533
+ const updateGlobal = await (0, import_prompts8.confirm)({
12534
+ message: "Update global constitution with this repo's context?",
12535
+ default: true
12536
+ });
12537
+ if (updateGlobal) {
12538
+ const allRepos2 = await getRegisteredRepos();
12539
+ await generateGlobalConstitution(provider, allRepos2, currentDir);
12360
12540
  }
12361
- console.log(import_chalk28.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12362
- console.log(import_chalk28.default.gray(` Provider: ${providerName}/${modelName}`));
12363
- const projectSummaries = [];
12364
- const index = await loadIndex(currentDir);
12365
- if (index && index.projects.length > 0) {
12366
- const active = index.projects.filter((p) => !p.missing);
12367
- console.log(import_chalk28.default.gray(` Found project index: ${active.length} project(s) \u2014 reading constitutions...`));
12368
- for (const entry of active) {
12369
- const absPath = path27.join(currentDir, entry.path);
12370
- const lines = [
12371
- `Type: ${entry.type} (${entry.role})`,
12372
- `Tech stack: ${entry.techStack.join(", ") || "unknown"}`
12373
- ];
12374
- if (entry.hasConstitution) {
12375
- try {
12376
- const constitutionPath2 = path27.join(absPath, CONSTITUTION_FILE);
12377
- const raw = await fs28.readFile(constitutionPath2, "utf-8");
12378
- const excerpt = raw.slice(0, 2e3);
12379
- lines.push("", "Constitution excerpt:", excerpt);
12380
- } catch {
12381
- }
12382
- }
12383
- projectSummaries.push({ name: entry.name, summary: lines.join("\n") });
12541
+ return;
12542
+ }
12543
+ console.log(import_chalk28.default.blue("\n" + "\u2500".repeat(52)));
12544
+ console.log(import_chalk28.default.bold(" ai-spec init \u2014 Workspace Setup"));
12545
+ console.log(import_chalk28.default.blue("\u2500".repeat(52)));
12546
+ console.log(import_chalk28.default.gray(` Provider: ${providerName}/${modelName}
12547
+ `));
12548
+ const existingRepos = await getRegisteredRepos();
12549
+ if (existingRepos.length > 0) {
12550
+ console.log(import_chalk28.default.cyan(" Registered repos:"));
12551
+ for (const r of existingRepos) {
12552
+ const constitutionIcon = r.hasConstitution ? import_chalk28.default.green("\u2714") : import_chalk28.default.gray("\u25CB");
12553
+ console.log(import_chalk28.default.gray(` ${constitutionIcon} ${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`));
12554
+ }
12555
+ console.log();
12556
+ }
12557
+ const action = existingRepos.length > 0 ? await (0, import_prompts8.select)({
12558
+ message: "What would you like to do?",
12559
+ choices: [
12560
+ { name: "Add new repo(s)", value: "add" },
12561
+ { name: "Re-generate constitutions for existing repos", value: "regen" },
12562
+ { name: "Skip \u2014 proceed to global constitution", value: "skip" }
12563
+ ]
12564
+ }) : "add";
12565
+ const newRepos = [];
12566
+ if (action === "add") {
12567
+ let addMore = true;
12568
+ while (addMore) {
12569
+ console.log(import_chalk28.default.blue(`
12570
+ \u2500\u2500 Register Repo #${existingRepos.length + newRepos.length + 1} \u2500\u2500`));
12571
+ const role = await promptRepoRole2();
12572
+ const repoPath = await promptRepoPath(role);
12573
+ const alreadyRegistered = [...existingRepos, ...newRepos].find((r) => r.path === repoPath);
12574
+ if (alreadyRegistered) {
12575
+ console.log(import_chalk28.default.yellow(` Already registered: ${alreadyRegistered.name}`));
12576
+ } else {
12577
+ const entry = await registerSingleRepo(repoPath, provider, role);
12578
+ newRepos.push(entry);
12579
+ console.log(import_chalk28.default.green(` \u2714 Registered: ${entry.name}`));
12384
12580
  }
12385
- } else {
12386
- console.log(import_chalk28.default.yellow(" No project index found. Run `ai-spec scan` first for better results."));
12387
- console.log(import_chalk28.default.gray(" Falling back: scanning current directory only..."));
12388
- const loader = new ContextLoader(currentDir);
12389
- const ctx = await loader.loadProjectContext();
12390
- projectSummaries.push({
12391
- name: path27.basename(currentDir),
12392
- summary: [
12393
- `Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
12394
- `Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
12395
- ].join("\n")
12581
+ addMore = await (0, import_prompts8.confirm)({
12582
+ message: "Add another repo?",
12583
+ default: false
12396
12584
  });
12397
12585
  }
12398
- console.log(import_chalk28.default.gray(` Generating from ${projectSummaries.length} project(s)...`));
12399
- const prompt = buildGlobalConstitutionPrompt(projectSummaries);
12400
- let globalConstitution;
12401
- try {
12402
- globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
12403
- } catch (err) {
12404
- console.error(import_chalk28.default.red(" \u2718 Failed to generate global constitution:"), err);
12405
- process.exit(1);
12406
- }
12407
- const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
12408
- console.log(import_chalk28.default.green(`
12409
- \u2714 Global constitution saved: ${saved2}`));
12410
- console.log(import_chalk28.default.gray(" This will be automatically merged into all project constitutions in this workspace."));
12411
- console.log(import_chalk28.default.gray(" Project-level rules always override global rules.\n"));
12412
- console.log(import_chalk28.default.bold(" Preview:"));
12413
- console.log(import_chalk28.default.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
12414
- if (globalConstitution.split("\n").length > 12) {
12415
- console.log(import_chalk28.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
12586
+ }
12587
+ if (action === "regen") {
12588
+ console.log(import_chalk28.default.blue("\n Re-generating project constitutions..."));
12589
+ for (const repo of existingRepos) {
12590
+ if (!await fs29.pathExists(repo.path)) {
12591
+ console.log(import_chalk28.default.yellow(` \u26A0 ${repo.name}: path not found \u2014 skipping`));
12592
+ continue;
12593
+ }
12594
+ const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
12595
+ if (await fs29.pathExists(constitutionPath) && !opts.force) {
12596
+ console.log(import_chalk28.default.gray(` ${repo.name}: constitution exists (use --force to overwrite)`));
12597
+ continue;
12598
+ }
12599
+ console.log(import_chalk28.default.blue(` ${repo.name}: generating constitution...`));
12600
+ try {
12601
+ const gen = new ConstitutionGenerator(provider);
12602
+ const content = await gen.generate(repo.path);
12603
+ await gen.saveConstitution(repo.path, content);
12604
+ console.log(import_chalk28.default.green(` \u2714 ${repo.name}: constitution saved`));
12605
+ } catch (err) {
12606
+ console.log(import_chalk28.default.yellow(` \u26A0 ${repo.name}: failed \u2014 ${err.message}`));
12607
+ }
12416
12608
  }
12417
- return;
12418
12609
  }
12419
- const constitutionPath = path27.join(currentDir, CONSTITUTION_FILE);
12420
- if (!opts.force && await fs28.pathExists(constitutionPath)) {
12421
- console.log(import_chalk28.default.yellow(`
12422
- ${CONSTITUTION_FILE} already exists.`));
12423
- console.log(import_chalk28.default.gray(" Use --force to overwrite it."));
12424
- console.log(import_chalk28.default.gray(` Or edit it directly: ${constitutionPath}`));
12610
+ const allRepos = await getRegisteredRepos();
12611
+ if (allRepos.length === 0) {
12612
+ console.log(import_chalk28.default.yellow("\n No repos registered. Run `ai-spec init` again to add repos."));
12425
12613
  return;
12426
12614
  }
12427
- console.log(import_chalk28.default.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12428
- console.log(import_chalk28.default.gray(` Provider: ${providerName}/${modelName}`));
12429
- console.log(import_chalk28.default.gray(" Analyzing codebase..."));
12430
- const generator = new ConstitutionGenerator(provider);
12431
- let constitution;
12432
- try {
12433
- constitution = await generator.generate(currentDir);
12434
- } catch (err) {
12435
- console.error(import_chalk28.default.red(" \u2718 Failed to generate constitution:"), err);
12436
- process.exit(1);
12437
- }
12438
- const saved = await generator.saveConstitution(currentDir, constitution);
12439
- const globalResult = await loadGlobalConstitution([path27.dirname(currentDir)]);
12440
- if (globalResult) {
12441
- console.log(import_chalk28.default.cyan(`
12442
- \u2139 Global constitution detected: ${globalResult.source}`));
12443
- console.log(import_chalk28.default.gray(" It will be merged with this project constitution at runtime."));
12444
- console.log(import_chalk28.default.gray(" Project rules take priority over global rules."));
12445
- }
12446
- console.log(import_chalk28.default.green(`
12447
- \u2714 Constitution saved: ${saved}`));
12448
- console.log(import_chalk28.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
12449
- console.log(import_chalk28.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
12450
- console.log(import_chalk28.default.bold(" Preview:"));
12451
- console.log(import_chalk28.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
12452
- if (constitution.split("\n").length > 15) {
12453
- console.log(import_chalk28.default.gray(` ... (${constitution.split("\n").length} lines total)`));
12454
- }
12615
+ const existingGlobal = await loadGlobalConstitution([currentDir]);
12616
+ const shouldGenerateGlobal = !existingGlobal || opts.force || newRepos.length > 0 ? true : await (0, import_prompts8.confirm)({
12617
+ message: "Global constitution exists. Re-generate it?",
12618
+ default: false
12619
+ });
12620
+ if (shouldGenerateGlobal) {
12621
+ await generateGlobalConstitution(provider, allRepos, currentDir);
12622
+ }
12623
+ console.log(import_chalk28.default.bold.green("\n\u2714 Init complete!"));
12624
+ console.log(import_chalk28.default.gray(` Repos registered: ${allRepos.length}`));
12625
+ for (const r of allRepos) {
12626
+ const icon = r.hasConstitution ? import_chalk28.default.green("\u2714") : import_chalk28.default.gray("\u25CB");
12627
+ console.log(import_chalk28.default.gray(` ${icon} ${r.name} (${r.type}/${r.role})`));
12628
+ }
12629
+ console.log(import_chalk28.default.gray(`
12630
+ Repo store: ${REPO_STORE_FILE}`));
12631
+ console.log(import_chalk28.default.gray(` Next step: ai-spec create "your feature idea"`));
12632
+ process.exit(0);
12455
12633
  });
12456
12634
  }
12457
12635
 
12458
12636
  // cli/commands/config.ts
12459
- var path28 = __toESM(require("path"));
12460
- var fs29 = __toESM(require("fs-extra"));
12637
+ var path29 = __toESM(require("path"));
12638
+ var fs30 = __toESM(require("fs-extra"));
12461
12639
  var import_chalk29 = __toESM(require("chalk"));
12462
12640
  function registerConfig(program2) {
12463
12641
  program2.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)").option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)").option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
12464
12642
  const currentDir = process.cwd();
12465
- const configPath = path28.join(currentDir, CONFIG_FILE);
12643
+ const configPath = path29.join(currentDir, CONFIG_FILE);
12466
12644
  if (opts.clearKeys) {
12467
12645
  await clearAllKeys();
12468
12646
  console.log(import_chalk29.default.green(`\u2714 All saved API keys cleared.`));
@@ -12474,7 +12652,7 @@ function registerConfig(program2) {
12474
12652
  return;
12475
12653
  }
12476
12654
  if (opts.listKeys) {
12477
- const store = await fs29.readJson(KEY_STORE_FILE).catch(() => ({}));
12655
+ const store = await fs30.readJson(KEY_STORE_FILE).catch(() => ({}));
12478
12656
  const providers = Object.keys(store);
12479
12657
  if (providers.length === 0) {
12480
12658
  console.log(import_chalk29.default.gray("No saved API keys."));
@@ -12490,7 +12668,7 @@ File: ${KEY_STORE_FILE}`));
12490
12668
  return;
12491
12669
  }
12492
12670
  if (opts.reset) {
12493
- await fs29.writeJson(configPath, {}, { spaces: 2 });
12671
+ await fs30.writeJson(configPath, {}, { spaces: 2 });
12494
12672
  console.log(import_chalk29.default.green(`\u2714 Config reset: ${configPath}`));
12495
12673
  return;
12496
12674
  }
@@ -12534,22 +12712,18 @@ File: ${KEY_STORE_FILE}`));
12534
12712
  }
12535
12713
  updated.maxErrorCycles = cycles;
12536
12714
  }
12537
- await fs29.writeJson(configPath, updated, { spaces: 2 });
12715
+ await fs30.writeJson(configPath, updated, { spaces: 2 });
12538
12716
  console.log(import_chalk29.default.green(`\u2714 Config saved to ${configPath}`));
12539
12717
  console.log(JSON.stringify(updated, null, 2));
12540
12718
  });
12541
12719
  }
12542
12720
 
12543
12721
  // cli/commands/model.ts
12544
- var path29 = __toESM(require("path"));
12545
- var fs30 = __toESM(require("fs-extra"));
12546
12722
  var import_chalk30 = __toESM(require("chalk"));
12547
- var import_prompts8 = require("@inquirer/prompts");
12723
+ var import_prompts9 = require("@inquirer/prompts");
12548
12724
  init_spec_generator();
12549
12725
  function registerModel(program2) {
12550
12726
  program2.command("model").description("Interactively switch the active AI provider/model and save to .ai-spec.json").option("--list", "List all available providers and models").action(async (opts) => {
12551
- const currentDir = process.cwd();
12552
- const configPath = path29.join(currentDir, CONFIG_FILE);
12553
12727
  if (opts.list) {
12554
12728
  console.log(import_chalk30.default.bold("\nAvailable providers & models:\n"));
12555
12729
  for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
@@ -12566,8 +12740,9 @@ function registerModel(program2) {
12566
12740
  }
12567
12741
  return;
12568
12742
  }
12569
- const existing = await loadConfig(currentDir);
12743
+ const existing = await loadGlobalConfig();
12570
12744
  console.log(import_chalk30.default.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12745
+ console.log(import_chalk30.default.gray(` Config: ${GLOBAL_CONFIG_FILE}`));
12571
12746
  if (Object.keys(existing).length > 0) {
12572
12747
  console.log(
12573
12748
  import_chalk30.default.gray(
@@ -12576,7 +12751,7 @@ function registerModel(program2) {
12576
12751
  );
12577
12752
  }
12578
12753
  console.log();
12579
- const target = await (0, import_prompts8.select)({
12754
+ const target = await (0, import_prompts9.select)({
12580
12755
  message: "Configure model for:",
12581
12756
  choices: [
12582
12757
  { name: "Spec generation (used for spec writing & refinement)", value: "spec" },
@@ -12585,7 +12760,7 @@ function registerModel(program2) {
12585
12760
  ]
12586
12761
  });
12587
12762
  async function pickProviderAndModel(label) {
12588
- const providerKey = await (0, import_prompts8.select)({
12763
+ const providerKey = await (0, import_prompts9.select)({
12589
12764
  message: `${label} \u2014 select provider:`,
12590
12765
  choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
12591
12766
  name: `${meta2.displayName.padEnd(22)} ${import_chalk30.default.gray(meta2.description)}`,
@@ -12598,12 +12773,12 @@ function registerModel(program2) {
12598
12773
  ...meta.models.map((m) => ({ name: m, value: m })),
12599
12774
  { name: import_chalk30.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
12600
12775
  ];
12601
- let chosenModel = await (0, import_prompts8.select)({
12776
+ let chosenModel = await (0, import_prompts9.select)({
12602
12777
  message: `${label} \u2014 select model (${meta.displayName}):`,
12603
12778
  choices: modelChoices
12604
12779
  });
12605
12780
  if (chosenModel === "__custom__") {
12606
- chosenModel = await (0, import_prompts8.input)({
12781
+ chosenModel = await (0, import_prompts9.input)({
12607
12782
  message: "Enter model name:",
12608
12783
  validate: (v2) => v2.trim().length > 0 || "Model name cannot be empty"
12609
12784
  });
@@ -12648,14 +12823,14 @@ function registerModel(program2) {
12648
12823
  )
12649
12824
  );
12650
12825
  }
12651
- const ok = await (0, import_prompts8.confirm)({ message: "Save to .ai-spec.json?", default: true });
12826
+ const ok = await (0, import_prompts9.confirm)({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
12652
12827
  if (!ok) {
12653
12828
  console.log(import_chalk30.default.gray(" Cancelled."));
12654
12829
  return;
12655
12830
  }
12656
- await fs30.writeJson(configPath, updated, { spaces: 2 });
12831
+ await saveGlobalConfig(updated);
12657
12832
  console.log(import_chalk30.default.green(`
12658
- \u2714 Saved to ${configPath}`));
12833
+ \u2714 Saved to ${GLOBAL_CONFIG_FILE}`));
12659
12834
  const providerToCheck = updated.provider ?? "gemini";
12660
12835
  const envKey = ENV_KEY_MAP[providerToCheck];
12661
12836
  if (envKey && !process.env[envKey]) {
@@ -12672,14 +12847,14 @@ function registerModel(program2) {
12672
12847
  var path30 = __toESM(require("path"));
12673
12848
  var fs31 = __toESM(require("fs-extra"));
12674
12849
  var import_chalk31 = __toESM(require("chalk"));
12675
- var import_prompts9 = require("@inquirer/prompts");
12850
+ var import_prompts10 = require("@inquirer/prompts");
12676
12851
  function registerWorkspace(program2) {
12677
12852
  const workspaceCmd = program2.command("workspace").description("Manage multi-repo workspace configuration");
12678
12853
  workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
12679
12854
  const currentDir = process.cwd();
12680
12855
  const configPath = path30.join(currentDir, WORKSPACE_CONFIG_FILE);
12681
12856
  if (await fs31.pathExists(configPath)) {
12682
- const overwrite = await (0, import_prompts9.confirm)({
12857
+ const overwrite = await (0, import_prompts10.confirm)({
12683
12858
  message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
12684
12859
  default: false
12685
12860
  });
@@ -12689,12 +12864,12 @@ function registerWorkspace(program2) {
12689
12864
  }
12690
12865
  }
12691
12866
  console.log(import_chalk31.default.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12692
- const workspaceName = await (0, import_prompts9.input)({
12867
+ const workspaceName = await (0, import_prompts10.input)({
12693
12868
  message: "Workspace name:",
12694
12869
  validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
12695
12870
  });
12696
12871
  const repos = [];
12697
- const useAutoScan = await (0, import_prompts9.confirm)({
12872
+ const useAutoScan = await (0, import_prompts10.confirm)({
12698
12873
  message: "Auto-scan sibling directories for repos?",
12699
12874
  default: true
12700
12875
  });
@@ -12708,7 +12883,7 @@ function registerWorkspace(program2) {
12708
12883
  for (const r of detected) {
12709
12884
  console.log(import_chalk31.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
12710
12885
  }
12711
- const keepAll = await (0, import_prompts9.confirm)({
12886
+ const keepAll = await (0, import_prompts10.confirm)({
12712
12887
  message: `Include all ${detected.length} detected repo(s)?`,
12713
12888
  default: true
12714
12889
  });
@@ -12716,7 +12891,7 @@ function registerWorkspace(program2) {
12716
12891
  repos.push(...detected);
12717
12892
  } else {
12718
12893
  for (const r of detected) {
12719
- const keep = await (0, import_prompts9.confirm)({
12894
+ const keep = await (0, import_prompts10.confirm)({
12720
12895
  message: `Include "${r.name}" (${r.role}, ${r.type})?`,
12721
12896
  default: true
12722
12897
  });
@@ -12740,14 +12915,14 @@ function registerWorkspace(program2) {
12740
12915
  { name: "react-native (React Native mobile)", value: "react-native" },
12741
12916
  { name: "unknown", value: "unknown" }
12742
12917
  ];
12743
- let addMore = await (0, import_prompts9.confirm)({
12918
+ let addMore = await (0, import_prompts10.confirm)({
12744
12919
  message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
12745
12920
  default: repos.length === 0
12746
12921
  });
12747
12922
  while (addMore) {
12748
12923
  console.log(import_chalk31.default.cyan(`
12749
12924
  Adding repo #${repos.length + 1}`));
12750
- const repoName = await (0, import_prompts9.input)({
12925
+ const repoName = await (0, import_prompts10.input)({
12751
12926
  message: "Repo name (e.g. api, web, app):",
12752
12927
  validate: (v2) => {
12753
12928
  if (!v2.trim()) return "Name cannot be empty";
@@ -12755,7 +12930,7 @@ function registerWorkspace(program2) {
12755
12930
  return true;
12756
12931
  }
12757
12932
  });
12758
- const repoPath = await (0, import_prompts9.input)({
12933
+ const repoPath = await (0, import_prompts10.input)({
12759
12934
  message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
12760
12935
  default: `./${repoName}`
12761
12936
  });
@@ -12770,12 +12945,12 @@ function registerWorkspace(program2) {
12770
12945
  } else {
12771
12946
  console.log(import_chalk31.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
12772
12947
  }
12773
- const repoType = await (0, import_prompts9.select)({
12948
+ const repoType = await (0, import_prompts10.select)({
12774
12949
  message: `Repo type for "${repoName}":`,
12775
12950
  choices: repoTypeChoices,
12776
12951
  default: detectedType
12777
12952
  });
12778
- const repoRole = await (0, import_prompts9.select)({
12953
+ const repoRole = await (0, import_prompts10.select)({
12779
12954
  message: `Repo role for "${repoName}":`,
12780
12955
  choices: [
12781
12956
  { name: "backend", value: "backend" },
@@ -12792,7 +12967,7 @@ function registerWorkspace(program2) {
12792
12967
  role: repoRole
12793
12968
  });
12794
12969
  console.log(import_chalk31.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
12795
- addMore = await (0, import_prompts9.confirm)({
12970
+ addMore = await (0, import_prompts10.confirm)({
12796
12971
  message: "Add another repo?",
12797
12972
  default: false
12798
12973
  });
@@ -12803,7 +12978,7 @@ function registerWorkspace(program2) {
12803
12978
  for (const r of repos) {
12804
12979
  console.log(import_chalk31.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
12805
12980
  }
12806
- const ok = await (0, import_prompts9.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
12981
+ const ok = await (0, import_prompts10.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
12807
12982
  if (!ok) {
12808
12983
  console.log(import_chalk31.default.gray(" Cancelled."));
12809
12984
  return;
@@ -12847,7 +13022,7 @@ Workspace: ${config2.name}`));
12847
13022
  var path32 = __toESM(require("path"));
12848
13023
  var fs33 = __toESM(require("fs-extra"));
12849
13024
  var import_chalk33 = __toESM(require("chalk"));
12850
- var import_prompts10 = require("@inquirer/prompts");
13025
+ var import_prompts11 = require("@inquirer/prompts");
12851
13026
  init_spec_generator();
12852
13027
 
12853
13028
  // core/spec-updater.ts
@@ -13082,7 +13257,7 @@ function registerUpdate(program2) {
13082
13257
  const currentDir = process.cwd();
13083
13258
  const config2 = await loadConfig(currentDir);
13084
13259
  if (!change) {
13085
- change = await (0, import_prompts10.input)({
13260
+ change = await (0, import_prompts11.input)({
13086
13261
  message: "Describe the change you want to make:",
13087
13262
  validate: (v2) => v2.trim().length > 0 || "Change description cannot be empty"
13088
13263
  });
@@ -13400,7 +13575,7 @@ function buildYamlDoc(obj) {
13400
13575
  return `${k2}: ${valStr}`;
13401
13576
  }).join("\n") + "\n";
13402
13577
  }
13403
- function dslToOpenApi(dsl, serverUrl = "http://localhost:3000") {
13578
+ function dslToOpenApi(dsl, serverUrl = DEFAULT_OPENAPI_SERVER_URL) {
13404
13579
  const info = {
13405
13580
  title: dsl.feature.title,
13406
13581
  description: dsl.feature.description,
@@ -13447,7 +13622,7 @@ function dslToOpenApi(dsl, serverUrl = "http://localhost:3000") {
13447
13622
  }
13448
13623
  async function exportOpenApi(dsl, projectDir, opts = {}) {
13449
13624
  const format = opts.format ?? "yaml";
13450
- const serverUrl = opts.serverUrl ?? "http://localhost:3000";
13625
+ const serverUrl = opts.serverUrl ?? DEFAULT_OPENAPI_SERVER_URL;
13451
13626
  const defaultName = `openapi.${format}`;
13452
13627
  const outputPath = opts.outputPath ? path33.isAbsolute(opts.outputPath) ? opts.outputPath : path33.join(projectDir, opts.outputPath) : path33.join(projectDir, defaultName);
13453
13628
  const doc = dslToOpenApi(dsl, serverUrl);
@@ -13643,12 +13818,12 @@ function registerMock(program2) {
13643
13818
 
13644
13819
  // cli/commands/learn.ts
13645
13820
  var import_chalk36 = __toESM(require("chalk"));
13646
- var import_prompts11 = require("@inquirer/prompts");
13821
+ var import_prompts12 = require("@inquirer/prompts");
13647
13822
  function registerLearn(program2) {
13648
13823
  program2.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
13649
13824
  const currentDir = process.cwd();
13650
13825
  if (!lesson) {
13651
- lesson = await (0, import_prompts11.input)({
13826
+ lesson = await (0, import_prompts12.input)({
13652
13827
  message: "What lesson or engineering decision should be recorded?",
13653
13828
  validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
13654
13829
  });
@@ -13690,9 +13865,8 @@ var import_chalk39 = __toESM(require("chalk"));
13690
13865
  var fs37 = __toESM(require("fs-extra"));
13691
13866
  var path36 = __toESM(require("path"));
13692
13867
  var import_chalk38 = __toESM(require("chalk"));
13693
- var LOG_DIR2 = ".ai-spec-logs";
13694
13868
  async function loadRunLogs(workingDir) {
13695
- const logDir = path36.join(workingDir, LOG_DIR2);
13869
+ const logDir = path36.join(workingDir, DEFAULT_LOG_DIR);
13696
13870
  if (!await fs37.pathExists(logDir)) return [];
13697
13871
  const files = await fs37.readdir(logDir);
13698
13872
  const jsonFiles = new Set(files.filter((f) => f.endsWith(".json")));
@@ -13837,7 +14011,7 @@ function printTrendReport(report, workingDir) {
13837
14011
  ` ${import_chalk38.default.gray(formatDate(e.startedAt))} ${bar}${scoreStr} ${hash} ${dur}${errMark} ${spec}`
13838
14012
  );
13839
14013
  }
13840
- const logRelDir = path36.relative(workingDir, path36.join(workingDir, LOG_DIR2));
14014
+ const logRelDir = path36.relative(workingDir, path36.join(workingDir, DEFAULT_LOG_DIR));
13841
14015
  console.log(import_chalk38.default.gray(`
13842
14016
  ${entries.length} run(s) shown \xB7 logs: ${logRelDir}/`));
13843
14017
  console.log(import_chalk38.default.cyan("\u2500".repeat(63)));
@@ -14496,7 +14670,233 @@ function registerVcr(program2) {
14496
14670
 
14497
14671
  // cli/commands/scan.ts
14498
14672
  var import_chalk44 = __toESM(require("chalk"));
14673
+ var path42 = __toESM(require("path"));
14674
+
14675
+ // core/project-index.ts
14676
+ var fs42 = __toESM(require("fs-extra"));
14499
14677
  var path41 = __toESM(require("path"));
14678
+ var INDEX_FILE = ".ai-spec-index.json";
14679
+ var KEY_DEPS = [
14680
+ // Frameworks
14681
+ "express",
14682
+ "fastify",
14683
+ "koa",
14684
+ "@nestjs/core",
14685
+ "hapi",
14686
+ "next",
14687
+ "react",
14688
+ "vue",
14689
+ "nuxt",
14690
+ "svelte",
14691
+ "react-native",
14692
+ "expo",
14693
+ // DB / ORM
14694
+ "prisma",
14695
+ "@prisma/client",
14696
+ "mongoose",
14697
+ "typeorm",
14698
+ "sequelize",
14699
+ "drizzle-orm",
14700
+ // Auth
14701
+ "jsonwebtoken",
14702
+ "passport",
14703
+ "next-auth",
14704
+ "@clerk/nextjs",
14705
+ // Build / Lang
14706
+ "typescript",
14707
+ "vite",
14708
+ "webpack",
14709
+ "esbuild",
14710
+ "turbo",
14711
+ // Testing
14712
+ "jest",
14713
+ "vitest",
14714
+ "mocha",
14715
+ "cypress",
14716
+ "playwright",
14717
+ // Infra
14718
+ "redis",
14719
+ "bull",
14720
+ "socket.io",
14721
+ "graphql",
14722
+ "@trpc/server"
14723
+ ];
14724
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
14725
+ "node_modules",
14726
+ ".git",
14727
+ ".svn",
14728
+ "dist",
14729
+ "build",
14730
+ "out",
14731
+ ".next",
14732
+ ".nuxt",
14733
+ "coverage",
14734
+ ".turbo",
14735
+ ".cache",
14736
+ "__pycache__",
14737
+ "vendor",
14738
+ ".ai-spec-vcr",
14739
+ ".ai-spec-logs",
14740
+ "specs"
14741
+ ]);
14742
+ var MANIFEST_FILES = [
14743
+ "package.json",
14744
+ "go.mod",
14745
+ "Cargo.toml",
14746
+ "pom.xml",
14747
+ "build.gradle",
14748
+ "build.gradle.kts",
14749
+ "requirements.txt",
14750
+ "pyproject.toml",
14751
+ "setup.py",
14752
+ "composer.json"
14753
+ ];
14754
+ async function isProjectRoot(absPath) {
14755
+ for (const manifest of MANIFEST_FILES) {
14756
+ if (await fs42.pathExists(path41.join(absPath, manifest))) return true;
14757
+ }
14758
+ return false;
14759
+ }
14760
+ async function extractTechStack(absPath, type) {
14761
+ const stack = [];
14762
+ if (type === "go") stack.push("go");
14763
+ if (type === "rust") stack.push("rust");
14764
+ if (type === "java") stack.push("java");
14765
+ if (type === "python") stack.push("python");
14766
+ if (type === "php") stack.push("php");
14767
+ const pkgPath = path41.join(absPath, "package.json");
14768
+ if (!await fs42.pathExists(pkgPath)) return stack;
14769
+ let pkg = {};
14770
+ try {
14771
+ pkg = await fs42.readJson(pkgPath);
14772
+ } catch {
14773
+ return stack;
14774
+ }
14775
+ const allDeps = {
14776
+ ...pkg.dependencies ?? {},
14777
+ ...pkg.devDependencies ?? {}
14778
+ };
14779
+ const depKeys = new Set(Object.keys(allDeps));
14780
+ for (const dep of KEY_DEPS) {
14781
+ if (depKeys.has(dep)) stack.push(dep);
14782
+ }
14783
+ return stack;
14784
+ }
14785
+ async function discoverProjects(rootDir, maxDepth) {
14786
+ const found = [];
14787
+ async function walk(absDir, depth) {
14788
+ if (depth > maxDepth) return;
14789
+ let entries;
14790
+ try {
14791
+ entries = await fs42.readdir(absDir, { withFileTypes: true });
14792
+ } catch {
14793
+ return;
14794
+ }
14795
+ for (const entry of entries) {
14796
+ if (!entry.isDirectory()) continue;
14797
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
14798
+ const childAbs = path41.join(absDir, entry.name);
14799
+ const gitPath = path41.join(childAbs, ".git");
14800
+ if (await fs42.pathExists(gitPath)) {
14801
+ const gitStat = await fs42.stat(gitPath);
14802
+ if (gitStat.isFile()) continue;
14803
+ }
14804
+ if (await isProjectRoot(childAbs)) {
14805
+ found.push(path41.relative(rootDir, childAbs));
14806
+ } else {
14807
+ await walk(childAbs, depth + 1);
14808
+ }
14809
+ }
14810
+ }
14811
+ await walk(rootDir, 0);
14812
+ return found;
14813
+ }
14814
+ async function loadIndex(scanRoot) {
14815
+ const filePath = path41.join(scanRoot, INDEX_FILE);
14816
+ try {
14817
+ return await fs42.readJson(filePath);
14818
+ } catch {
14819
+ return null;
14820
+ }
14821
+ }
14822
+ async function saveIndex(scanRoot, index) {
14823
+ const filePath = path41.join(scanRoot, INDEX_FILE);
14824
+ await fs42.writeJson(filePath, index, { spaces: 2 });
14825
+ return filePath;
14826
+ }
14827
+ async function runScan(scanRoot, maxDepth = 2) {
14828
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14829
+ const existing = await loadIndex(scanRoot);
14830
+ const existingMap = new Map(
14831
+ (existing?.projects ?? []).map((p) => [p.path, p])
14832
+ );
14833
+ const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
14834
+ const added = [];
14835
+ const updated = [];
14836
+ const unchanged = [];
14837
+ const seenPaths = /* @__PURE__ */ new Set();
14838
+ for (const relPath of discoveredPaths) {
14839
+ const absPath = path41.join(scanRoot, relPath);
14840
+ seenPaths.add(relPath);
14841
+ const { type, role } = await detectRepoType(absPath);
14842
+ const techStack = await extractTechStack(absPath, type);
14843
+ const hasConstitution = await fs42.pathExists(path41.join(absPath, CONSTITUTION_FILE));
14844
+ const hasWorkspace = await fs42.pathExists(path41.join(absPath, WORKSPACE_CONFIG_FILE));
14845
+ const name = path41.basename(relPath);
14846
+ const prev = existingMap.get(relPath);
14847
+ if (!prev) {
14848
+ const entry = {
14849
+ name,
14850
+ path: relPath,
14851
+ type,
14852
+ role,
14853
+ techStack,
14854
+ hasConstitution,
14855
+ hasWorkspace,
14856
+ firstSeen: now,
14857
+ lastSeen: now
14858
+ };
14859
+ added.push(entry);
14860
+ existingMap.set(relPath, entry);
14861
+ } else {
14862
+ const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
14863
+ const entry = {
14864
+ ...prev,
14865
+ type,
14866
+ role,
14867
+ techStack,
14868
+ hasConstitution,
14869
+ hasWorkspace,
14870
+ lastSeen: now,
14871
+ missing: void 0
14872
+ // clear missing flag if it came back
14873
+ };
14874
+ existingMap.set(relPath, entry);
14875
+ if (changed) {
14876
+ updated.push(entry);
14877
+ } else {
14878
+ unchanged.push(entry);
14879
+ }
14880
+ }
14881
+ }
14882
+ const nowMissing = [];
14883
+ for (const [relPath, entry] of existingMap) {
14884
+ if (!seenPaths.has(relPath) && !entry.missing) {
14885
+ const gone = { ...entry, missing: true };
14886
+ existingMap.set(relPath, gone);
14887
+ nowMissing.push(gone);
14888
+ }
14889
+ }
14890
+ const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
14891
+ const index = {
14892
+ scanRoot,
14893
+ lastScanned: now,
14894
+ projects
14895
+ };
14896
+ return { index, added, updated, unchanged, nowMissing };
14897
+ }
14898
+
14899
+ // cli/commands/scan.ts
14500
14900
  var ROLE_COLOR = {
14501
14901
  backend: import_chalk44.default.blue,
14502
14902
  frontend: import_chalk44.default.green,
@@ -14570,7 +14970,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
14570
14970
  }
14571
14971
  console.log(import_chalk44.default.cyan("\n\u2500".repeat(52)));
14572
14972
  console.log(import_chalk44.default.gray(" \xA7C = has constitution W = workspace root"));
14573
- console.log(import_chalk44.default.gray(` Index saved : ${path41.relative(cwd, path41.join(cwd, INDEX_FILE))}`));
14973
+ console.log(import_chalk44.default.gray(` Index saved : ${path42.relative(cwd, path42.join(cwd, INDEX_FILE))}`));
14574
14974
  console.log(import_chalk44.default.gray(` Next steps : ai-spec scan --list | ai-spec init [--global]`));
14575
14975
  });
14576
14976
  }
@@ -14578,7 +14978,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
14578
14978
  // cli/index.ts
14579
14979
  dotenv.config();
14580
14980
  var program = new import_commander.Command();
14581
- program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
14981
+ program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(require_package().version);
14582
14982
  registerCreate(program);
14583
14983
  registerReview(program);
14584
14984
  registerInit(program);