ai-spec-dev 0.30.1 → 0.33.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.
package/dist/cli/index.js CHANGED
@@ -488,9 +488,9 @@ var init_workspace_loader = __esm({
488
488
 
489
489
  // cli/index.ts
490
490
  var import_commander = require("commander");
491
- var path22 = __toESM(require("path"));
492
- var fs23 = __toESM(require("fs-extra"));
493
- var import_chalk18 = __toESM(require("chalk"));
491
+ var path23 = __toESM(require("path"));
492
+ var fs24 = __toESM(require("fs-extra"));
493
+ var import_chalk21 = __toESM(require("chalk"));
494
494
  var dotenv = __toESM(require("dotenv"));
495
495
  var import_prompts3 = require("@inquirer/prompts");
496
496
 
@@ -4870,221 +4870,221 @@ function validateDsl(raw) {
4870
4870
  }
4871
4871
  return { valid: true, dsl: raw };
4872
4872
  }
4873
- function validateFeature(raw, path23, errors) {
4873
+ function validateFeature(raw, path24, errors) {
4874
4874
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4875
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
4875
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4876
4876
  return;
4877
4877
  }
4878
4878
  const f = raw;
4879
- requireNonEmptyString(f["id"], `${path23}.id`, errors);
4880
- requireNonEmptyString(f["title"], `${path23}.title`, errors);
4881
- requireNonEmptyString(f["description"], `${path23}.description`, errors);
4879
+ requireNonEmptyString(f["id"], `${path24}.id`, errors);
4880
+ requireNonEmptyString(f["title"], `${path24}.title`, errors);
4881
+ requireNonEmptyString(f["description"], `${path24}.description`, errors);
4882
4882
  }
4883
- function validateModel(raw, path23, errors) {
4883
+ function validateModel(raw, path24, errors) {
4884
4884
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4885
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
4885
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4886
4886
  return;
4887
4887
  }
4888
4888
  const m = raw;
4889
- requireNonEmptyString(m["name"], `${path23}.name`, errors);
4889
+ requireNonEmptyString(m["name"], `${path24}.name`, errors);
4890
4890
  if (!Array.isArray(m["fields"])) {
4891
- errors.push({ path: `${path23}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4891
+ errors.push({ path: `${path24}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
4892
4892
  } else {
4893
4893
  const fields = m["fields"];
4894
4894
  if (fields.length > MAX_FIELDS_PER_MODEL) {
4895
- errors.push({ path: `${path23}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4895
+ errors.push({ path: `${path24}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
4896
4896
  }
4897
4897
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4898
- validateModelField(fields[j2], `${path23}.fields[${j2}]`, errors);
4898
+ validateModelField(fields[j2], `${path24}.fields[${j2}]`, errors);
4899
4899
  }
4900
4900
  }
4901
4901
  if (m["relations"] !== void 0) {
4902
4902
  if (!Array.isArray(m["relations"])) {
4903
- errors.push({ path: `${path23}.relations`, message: "Must be an array of strings if present" });
4903
+ errors.push({ path: `${path24}.relations`, message: "Must be an array of strings if present" });
4904
4904
  } else {
4905
4905
  const rels = m["relations"];
4906
4906
  for (let j2 = 0; j2 < rels.length; j2++) {
4907
4907
  if (typeof rels[j2] !== "string") {
4908
- errors.push({ path: `${path23}.relations[${j2}]`, message: "Must be a string" });
4908
+ errors.push({ path: `${path24}.relations[${j2}]`, message: "Must be a string" });
4909
4909
  }
4910
4910
  }
4911
4911
  }
4912
4912
  }
4913
4913
  }
4914
- function validateModelField(raw, path23, errors) {
4914
+ function validateModelField(raw, path24, errors) {
4915
4915
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4916
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
4916
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4917
4917
  return;
4918
4918
  }
4919
4919
  const f = raw;
4920
- requireNonEmptyString(f["name"], `${path23}.name`, errors);
4921
- requireNonEmptyString(f["type"], `${path23}.type`, errors);
4920
+ requireNonEmptyString(f["name"], `${path24}.name`, errors);
4921
+ requireNonEmptyString(f["type"], `${path24}.type`, errors);
4922
4922
  if (typeof f["required"] !== "boolean") {
4923
- errors.push({ path: `${path23}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4923
+ errors.push({ path: `${path24}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
4924
4924
  }
4925
4925
  }
4926
- function validateEndpoint(raw, path23, errors) {
4926
+ function validateEndpoint(raw, path24, errors) {
4927
4927
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4928
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
4928
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4929
4929
  return;
4930
4930
  }
4931
4931
  const e = raw;
4932
- requireNonEmptyString(e["id"], `${path23}.id`, errors);
4933
- requireNonEmptyString(e["description"], `${path23}.description`, errors);
4932
+ requireNonEmptyString(e["id"], `${path24}.id`, errors);
4933
+ requireNonEmptyString(e["description"], `${path24}.description`, errors);
4934
4934
  if (!VALID_METHODS.includes(e["method"])) {
4935
4935
  errors.push({
4936
- path: `${path23}.method`,
4936
+ path: `${path24}.method`,
4937
4937
  message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
4938
4938
  });
4939
4939
  }
4940
4940
  if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
4941
4941
  errors.push({
4942
- path: `${path23}.path`,
4942
+ path: `${path24}.path`,
4943
4943
  message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
4944
4944
  });
4945
4945
  }
4946
4946
  if (typeof e["auth"] !== "boolean") {
4947
- errors.push({ path: `${path23}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4947
+ errors.push({ path: `${path24}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
4948
4948
  }
4949
4949
  if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
4950
4950
  errors.push({
4951
- path: `${path23}.successStatus`,
4951
+ path: `${path24}.successStatus`,
4952
4952
  message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
4953
4953
  });
4954
4954
  }
4955
- requireNonEmptyString(e["successDescription"], `${path23}.successDescription`, errors);
4955
+ requireNonEmptyString(e["successDescription"], `${path24}.successDescription`, errors);
4956
4956
  if (e["request"] !== void 0) {
4957
- validateRequestSchema(e["request"], `${path23}.request`, errors);
4957
+ validateRequestSchema(e["request"], `${path24}.request`, errors);
4958
4958
  }
4959
4959
  if (e["errors"] !== void 0) {
4960
4960
  if (!Array.isArray(e["errors"])) {
4961
- errors.push({ path: `${path23}.errors`, message: "Must be an array if present" });
4961
+ errors.push({ path: `${path24}.errors`, message: "Must be an array if present" });
4962
4962
  } else {
4963
4963
  const errs = e["errors"];
4964
4964
  if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
4965
- errors.push({ path: `${path23}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
4965
+ errors.push({ path: `${path24}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
4966
4966
  }
4967
4967
  for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
4968
- validateResponseError(errs[j2], `${path23}.errors[${j2}]`, errors);
4968
+ validateResponseError(errs[j2], `${path24}.errors[${j2}]`, errors);
4969
4969
  }
4970
4970
  }
4971
4971
  }
4972
4972
  }
4973
- function validateRequestSchema(raw, path23, errors) {
4973
+ function validateRequestSchema(raw, path24, errors) {
4974
4974
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4975
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
4975
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
4976
4976
  return;
4977
4977
  }
4978
4978
  const r = raw;
4979
4979
  for (const key of ["body", "query", "params"]) {
4980
4980
  if (r[key] !== void 0) {
4981
- validateFieldMap(r[key], `${path23}.${key}`, errors);
4981
+ validateFieldMap(r[key], `${path24}.${key}`, errors);
4982
4982
  }
4983
4983
  }
4984
4984
  }
4985
- function validateFieldMap(raw, path23, errors) {
4985
+ function validateFieldMap(raw, path24, errors) {
4986
4986
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4987
- errors.push({ path: path23, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
4987
+ errors.push({ path: path24, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
4988
4988
  return;
4989
4989
  }
4990
4990
  const map = raw;
4991
4991
  for (const [k2, v2] of Object.entries(map)) {
4992
4992
  if (typeof v2 !== "string") {
4993
- errors.push({ path: `${path23}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
4993
+ errors.push({ path: `${path24}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
4994
4994
  }
4995
4995
  }
4996
4996
  }
4997
- function validateResponseError(raw, path23, errors) {
4997
+ function validateResponseError(raw, path24, errors) {
4998
4998
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4999
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
4999
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
5000
5000
  return;
5001
5001
  }
5002
5002
  const e = raw;
5003
5003
  if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
5004
- errors.push({ path: `${path23}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5004
+ errors.push({ path: `${path24}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
5005
5005
  }
5006
- requireNonEmptyString(e["code"], `${path23}.code`, errors);
5007
- requireNonEmptyString(e["description"], `${path23}.description`, errors);
5006
+ requireNonEmptyString(e["code"], `${path24}.code`, errors);
5007
+ requireNonEmptyString(e["description"], `${path24}.description`, errors);
5008
5008
  }
5009
- function validateBehavior(raw, path23, errors) {
5009
+ function validateBehavior(raw, path24, errors) {
5010
5010
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5011
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
5011
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
5012
5012
  return;
5013
5013
  }
5014
5014
  const b = raw;
5015
- requireNonEmptyString(b["id"], `${path23}.id`, errors);
5016
- requireNonEmptyString(b["description"], `${path23}.description`, errors);
5015
+ requireNonEmptyString(b["id"], `${path24}.id`, errors);
5016
+ requireNonEmptyString(b["description"], `${path24}.description`, errors);
5017
5017
  if (b["constraints"] !== void 0) {
5018
5018
  if (!Array.isArray(b["constraints"])) {
5019
- errors.push({ path: `${path23}.constraints`, message: "Must be an array of strings if present" });
5019
+ errors.push({ path: `${path24}.constraints`, message: "Must be an array of strings if present" });
5020
5020
  } else {
5021
5021
  const cs2 = b["constraints"];
5022
5022
  for (let j2 = 0; j2 < cs2.length; j2++) {
5023
5023
  if (typeof cs2[j2] !== "string") {
5024
- errors.push({ path: `${path23}.constraints[${j2}]`, message: "Must be a string" });
5024
+ errors.push({ path: `${path24}.constraints[${j2}]`, message: "Must be a string" });
5025
5025
  }
5026
5026
  }
5027
5027
  }
5028
5028
  }
5029
5029
  }
5030
- function validateComponent(raw, path23, errors) {
5030
+ function validateComponent(raw, path24, errors) {
5031
5031
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
5032
- errors.push({ path: path23, message: `Must be an object, got: ${typeLabel(raw)}` });
5032
+ errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
5033
5033
  return;
5034
5034
  }
5035
5035
  const c = raw;
5036
- requireNonEmptyString(c["id"], `${path23}.id`, errors);
5037
- requireNonEmptyString(c["name"], `${path23}.name`, errors);
5038
- requireNonEmptyString(c["description"], `${path23}.description`, errors);
5036
+ requireNonEmptyString(c["id"], `${path24}.id`, errors);
5037
+ requireNonEmptyString(c["name"], `${path24}.name`, errors);
5038
+ requireNonEmptyString(c["description"], `${path24}.description`, errors);
5039
5039
  if (c["props"] !== void 0) {
5040
5040
  if (!Array.isArray(c["props"])) {
5041
- errors.push({ path: `${path23}.props`, message: "Must be an array if present" });
5041
+ errors.push({ path: `${path24}.props`, message: "Must be an array if present" });
5042
5042
  } else {
5043
5043
  const props = c["props"];
5044
5044
  for (let j2 = 0; j2 < props.length; j2++) {
5045
5045
  const p = props[j2];
5046
5046
  if (typeof p !== "object" || p === null) {
5047
- errors.push({ path: `${path23}.props[${j2}]`, message: "Must be an object" });
5047
+ errors.push({ path: `${path24}.props[${j2}]`, message: "Must be an object" });
5048
5048
  continue;
5049
5049
  }
5050
- requireNonEmptyString(p["name"], `${path23}.props[${j2}].name`, errors);
5051
- requireNonEmptyString(p["type"], `${path23}.props[${j2}].type`, errors);
5050
+ requireNonEmptyString(p["name"], `${path24}.props[${j2}].name`, errors);
5051
+ requireNonEmptyString(p["type"], `${path24}.props[${j2}].type`, errors);
5052
5052
  if (typeof p["required"] !== "boolean") {
5053
- errors.push({ path: `${path23}.props[${j2}].required`, message: "Must be boolean" });
5053
+ errors.push({ path: `${path24}.props[${j2}].required`, message: "Must be boolean" });
5054
5054
  }
5055
5055
  }
5056
5056
  }
5057
5057
  }
5058
5058
  if (c["events"] !== void 0) {
5059
5059
  if (!Array.isArray(c["events"])) {
5060
- errors.push({ path: `${path23}.events`, message: "Must be an array if present" });
5060
+ errors.push({ path: `${path24}.events`, message: "Must be an array if present" });
5061
5061
  } else {
5062
5062
  const events = c["events"];
5063
5063
  for (let j2 = 0; j2 < events.length; j2++) {
5064
5064
  const e = events[j2];
5065
5065
  if (typeof e !== "object" || e === null) {
5066
- errors.push({ path: `${path23}.events[${j2}]`, message: "Must be an object" });
5066
+ errors.push({ path: `${path24}.events[${j2}]`, message: "Must be an object" });
5067
5067
  continue;
5068
5068
  }
5069
- requireNonEmptyString(e["name"], `${path23}.events[${j2}].name`, errors);
5069
+ requireNonEmptyString(e["name"], `${path24}.events[${j2}].name`, errors);
5070
5070
  }
5071
5071
  }
5072
5072
  }
5073
5073
  if (c["state"] !== void 0) {
5074
5074
  if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
5075
- errors.push({ path: `${path23}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5075
+ errors.push({ path: `${path24}.state`, message: "Must be a flat object (Record<string, string>) if present" });
5076
5076
  }
5077
5077
  }
5078
5078
  if (c["apiCalls"] !== void 0) {
5079
5079
  if (!Array.isArray(c["apiCalls"])) {
5080
- errors.push({ path: `${path23}.apiCalls`, message: "Must be an array of strings if present" });
5080
+ errors.push({ path: `${path24}.apiCalls`, message: "Must be an array of strings if present" });
5081
5081
  }
5082
5082
  }
5083
5083
  }
5084
- function requireNonEmptyString(v2, path23, errors) {
5084
+ function requireNonEmptyString(v2, path24, errors) {
5085
5085
  if (typeof v2 !== "string" || v2.trim().length === 0) {
5086
5086
  errors.push({
5087
- path: path23,
5087
+ path: path24,
5088
5088
  message: `Must be a non-empty string, got: ${typeLabel(v2)}`
5089
5089
  });
5090
5090
  }
@@ -6046,8 +6046,8 @@ var RunSnapshot = class {
6046
6046
  const fullPath = path7.isAbsolute(filePath) ? filePath : path7.join(this.workingDir, filePath);
6047
6047
  if (!await fs8.pathExists(fullPath)) return;
6048
6048
  if (this.snapshotted.has(fullPath)) return;
6049
- const relative7 = path7.relative(this.workingDir, fullPath);
6050
- const dest = path7.join(this.backupRoot, relative7);
6049
+ const relative8 = path7.relative(this.workingDir, fullPath);
6050
+ const dest = path7.join(this.backupRoot, relative8);
6051
6051
  await fs8.ensureDir(path7.dirname(dest));
6052
6052
  await fs8.copy(fullPath, dest);
6053
6053
  this.snapshotted.add(fullPath);
@@ -6062,11 +6062,11 @@ var RunSnapshot = class {
6062
6062
  if (entry.isDirectory()) {
6063
6063
  await walk(full);
6064
6064
  } else {
6065
- const relative7 = path7.relative(this.backupRoot, full);
6066
- const dest = path7.join(this.workingDir, relative7);
6065
+ const relative8 = path7.relative(this.backupRoot, full);
6066
+ const dest = path7.join(this.workingDir, relative8);
6067
6067
  await fs8.ensureDir(path7.dirname(dest));
6068
6068
  await fs8.copy(full, dest, { overwrite: true });
6069
- restored.push(relative7);
6069
+ restored.push(relative8);
6070
6070
  }
6071
6071
  }
6072
6072
  };
@@ -6127,6 +6127,16 @@ var RunLogger = class {
6127
6127
  this.log.errors.push(`[${event}] ${error}`);
6128
6128
  this.flush();
6129
6129
  }
6130
+ /** Record the prompt hash for this run (call once at run start). */
6131
+ setPromptHash(hash) {
6132
+ this.log.promptHash = hash;
6133
+ this.flush();
6134
+ }
6135
+ /** Record the harness self-eval score (call once at run end). */
6136
+ setHarnessScore(score) {
6137
+ this.log.harnessScore = score;
6138
+ this.flush();
6139
+ }
6130
6140
  fileWritten(filePath) {
6131
6141
  if (!this.log.filesWritten.includes(filePath)) {
6132
6142
  this.log.filesWritten.push(filePath);
@@ -9911,13 +9921,439 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
9911
9921
  return outputPath;
9912
9922
  }
9913
9923
 
9924
+ // core/prompt-hasher.ts
9925
+ var import_crypto = require("crypto");
9926
+ init_codegen_prompt();
9927
+ init_codegen_prompt();
9928
+ function computePromptHash() {
9929
+ const segments = [
9930
+ codeGenSystemPrompt,
9931
+ dslSystemPrompt,
9932
+ specPrompt,
9933
+ reviewArchitectureSystemPrompt,
9934
+ reviewImplementationSystemPrompt,
9935
+ reviewImpactComplexitySystemPrompt
9936
+ ];
9937
+ return (0, import_crypto.createHash)("sha256").update(segments.join("\0")).digest("hex").slice(0, 8);
9938
+ }
9939
+
9940
+ // core/self-evaluator.ts
9941
+ var import_chalk18 = __toESM(require("chalk"));
9942
+ var ENDPOINT_LAYER_PATTERNS = [
9943
+ /src\/api/,
9944
+ /src\/routes?/,
9945
+ /src\/controller/,
9946
+ /src\/handler/,
9947
+ /src\/endpoints?/
9948
+ ];
9949
+ var MODEL_LAYER_PATTERNS = [
9950
+ /src\/model/,
9951
+ /src\/schema/,
9952
+ /src\/entit/,
9953
+ /src\/db/,
9954
+ /prisma/,
9955
+ /src\/data/,
9956
+ /src\/domain/
9957
+ ];
9958
+ function extractReviewScore(reviewText) {
9959
+ const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
9960
+ return match ? parseFloat(match[1]) : null;
9961
+ }
9962
+ function modelNameTokens(name) {
9963
+ const lower = name.toLowerCase();
9964
+ const parts = name.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "").split("-").filter(Boolean);
9965
+ const tokens = /* @__PURE__ */ new Set();
9966
+ tokens.add(lower);
9967
+ if (parts.length > 1) {
9968
+ tokens.add(parts.join("-"));
9969
+ tokens.add(parts.join("_"));
9970
+ }
9971
+ return [...tokens];
9972
+ }
9973
+ function runSelfEval(opts) {
9974
+ const { dsl, generatedFiles, compilePassed, reviewText, promptHash, logger } = opts;
9975
+ const endpointsTotal = dsl?.endpoints?.length ?? 0;
9976
+ const modelsTotal = dsl?.models?.length ?? 0;
9977
+ const endpointLayerCovered = generatedFiles.some(
9978
+ (f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
9979
+ );
9980
+ const endpointLayerFiles = generatedFiles.filter(
9981
+ (f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
9982
+ ).length;
9983
+ const modelLayerCovered = generatedFiles.some(
9984
+ (f) => MODEL_LAYER_PATTERNS.some((p) => p.test(f))
9985
+ );
9986
+ let modelNameMatched = 0;
9987
+ if (modelsTotal > 0 && dsl?.models) {
9988
+ for (const model of dsl.models) {
9989
+ const tokens = modelNameTokens(model.name);
9990
+ const found = generatedFiles.some((f) => {
9991
+ const lf = f.toLowerCase();
9992
+ return tokens.some((t) => lf.includes(t));
9993
+ });
9994
+ if (found) modelNameMatched++;
9995
+ }
9996
+ }
9997
+ const modelNameCoverage = modelsTotal > 0 ? modelNameMatched / modelsTotal : 1;
9998
+ let dslCoverageScore = 10;
9999
+ if (generatedFiles.length === 0) {
10000
+ dslCoverageScore = 0;
10001
+ } else {
10002
+ if (endpointsTotal > 0 && !endpointLayerCovered) dslCoverageScore -= 4;
10003
+ if (modelsTotal > 0 && !modelLayerCovered) dslCoverageScore -= 3;
10004
+ if (modelsTotal > 0 && modelLayerCovered) {
10005
+ if (modelNameCoverage < 0.5) dslCoverageScore -= 2;
10006
+ else if (modelNameCoverage < 0.8) dslCoverageScore -= 1;
10007
+ }
10008
+ if (endpointsTotal >= 5 && endpointLayerCovered && endpointLayerFiles < 2) {
10009
+ dslCoverageScore -= 1;
10010
+ }
10011
+ }
10012
+ dslCoverageScore = Math.max(0, Math.min(10, dslCoverageScore));
10013
+ const compileScore = compilePassed ? 10 : 5;
10014
+ const reviewScore = reviewText ? extractReviewScore(reviewText) : null;
10015
+ const harnessScore = reviewScore !== null ? Math.round((dslCoverageScore * 0.4 + compileScore * 0.3 + reviewScore * 0.3) * 10) / 10 : Math.round((dslCoverageScore * 0.55 + compileScore * 0.45) * 10) / 10;
10016
+ const result = {
10017
+ dslCoverageScore,
10018
+ compileScore,
10019
+ reviewScore,
10020
+ harnessScore,
10021
+ promptHash,
10022
+ detail: {
10023
+ endpointsTotal,
10024
+ endpointLayerCovered,
10025
+ endpointLayerFiles,
10026
+ modelsTotal,
10027
+ modelLayerCovered,
10028
+ modelNameCoverage: Math.round(modelNameCoverage * 100) / 100,
10029
+ modelNameMatched,
10030
+ filesWritten: generatedFiles.length
10031
+ }
10032
+ };
10033
+ logger.setHarnessScore(harnessScore);
10034
+ logger.stageEnd("self_eval", {
10035
+ harnessScore,
10036
+ dslCoverageScore,
10037
+ compileScore,
10038
+ reviewScore: reviewScore ?? void 0,
10039
+ promptHash,
10040
+ modelNameCoverage: result.detail.modelNameCoverage,
10041
+ modelNameMatched: result.detail.modelNameMatched,
10042
+ endpointLayerFiles: result.detail.endpointLayerFiles
10043
+ });
10044
+ return result;
10045
+ }
10046
+ function printSelfEval(result) {
10047
+ const scoreColor2 = result.harnessScore >= 8 ? import_chalk18.default.green : result.harnessScore >= 6 ? import_chalk18.default.yellow : import_chalk18.default.red;
10048
+ const filled = Math.round(result.harnessScore);
10049
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
10050
+ const compileTag = result.compileScore === 10 ? import_chalk18.default.green("pass") : import_chalk18.default.yellow("partial");
10051
+ const reviewTag = result.reviewScore !== null ? `Review: ${result.reviewScore}/10` : import_chalk18.default.gray("Review: skipped");
10052
+ let modelCoverageTag = "";
10053
+ if (result.detail.modelsTotal > 0) {
10054
+ const pct = Math.round(result.detail.modelNameCoverage * 100);
10055
+ const tag = `Models: ${result.detail.modelNameMatched}/${result.detail.modelsTotal} (${pct}%)`;
10056
+ modelCoverageTag = pct >= 80 ? import_chalk18.default.green(tag) : pct >= 50 ? import_chalk18.default.yellow(tag) : import_chalk18.default.red(tag);
10057
+ }
10058
+ console.log(import_chalk18.default.cyan("\n\u2500\u2500\u2500 Harness Self-Eval \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10059
+ console.log(` Score : ${scoreColor2(`[${bar}] ${result.harnessScore}/10`)}`);
10060
+ console.log(
10061
+ ` DSL : ${scoreColor2(String(result.dslCoverageScore) + "/10")} Compile: ${compileTag} ${reviewTag}`
10062
+ );
10063
+ if (modelCoverageTag) {
10064
+ console.log(
10065
+ ` Detail : ${modelCoverageTag} ` + import_chalk18.default.gray(`Endpoints: ${result.detail.endpointsTotal} Files: ${result.detail.filesWritten}`)
10066
+ );
10067
+ }
10068
+ console.log(import_chalk18.default.gray(` Prompt : ${result.promptHash}`));
10069
+ console.log(import_chalk18.default.cyan("\u2500".repeat(49)));
10070
+ }
10071
+
10072
+ // core/run-trend.ts
10073
+ var fs23 = __toESM(require("fs-extra"));
10074
+ var path22 = __toESM(require("path"));
10075
+ var import_chalk19 = __toESM(require("chalk"));
10076
+ var LOG_DIR2 = ".ai-spec-logs";
10077
+ async function loadRunLogs(workingDir) {
10078
+ const logDir = path22.join(workingDir, LOG_DIR2);
10079
+ if (!await fs23.pathExists(logDir)) return [];
10080
+ const files = await fs23.readdir(logDir);
10081
+ const jsonFiles = files.filter((f) => f.endsWith(".json")).sort().reverse();
10082
+ const logs = [];
10083
+ for (const file of jsonFiles) {
10084
+ try {
10085
+ const log = await fs23.readJson(path22.join(logDir, file));
10086
+ if (log.runId && log.startedAt) {
10087
+ logs.push(log);
10088
+ }
10089
+ } catch {
10090
+ }
10091
+ }
10092
+ return logs;
10093
+ }
10094
+ function buildTrendReport(logs, opts = {}) {
10095
+ let entries = logs.map((log) => ({
10096
+ runId: log.runId,
10097
+ startedAt: log.startedAt,
10098
+ promptHash: log.promptHash ?? null,
10099
+ harnessScore: log.harnessScore ?? null,
10100
+ specPath: log.specPath ?? null,
10101
+ provider: log.provider ?? null,
10102
+ model: log.model ?? null,
10103
+ filesWritten: log.filesWritten?.length ?? 0,
10104
+ totalDurationMs: log.totalDurationMs ?? null,
10105
+ errors: log.errors?.length ?? 0
10106
+ }));
10107
+ entries = entries.filter((e) => e.harnessScore !== null);
10108
+ if (opts.promptFilter) {
10109
+ entries = entries.filter(
10110
+ (e) => e.promptHash?.startsWith(opts.promptFilter)
10111
+ );
10112
+ }
10113
+ if (opts.last && opts.last > 0) {
10114
+ entries = entries.slice(0, opts.last);
10115
+ }
10116
+ const groupMap = /* @__PURE__ */ new Map();
10117
+ for (const e of entries) {
10118
+ const key = e.promptHash ?? "(none)";
10119
+ if (!groupMap.has(key)) groupMap.set(key, []);
10120
+ groupMap.get(key).push(e);
10121
+ }
10122
+ const currentHash = entries[0]?.promptHash ?? null;
10123
+ const promptGroups = [];
10124
+ for (const [hash, group] of groupMap.entries()) {
10125
+ const scores = group.map((e) => e.harnessScore);
10126
+ promptGroups.push({
10127
+ promptHash: hash,
10128
+ runs: group.length,
10129
+ avg: Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 10) / 10,
10130
+ best: Math.max(...scores),
10131
+ worst: Math.min(...scores),
10132
+ firstSeen: group[group.length - 1].startedAt,
10133
+ lastSeen: group[0].startedAt,
10134
+ isCurrent: hash === currentHash
10135
+ });
10136
+ }
10137
+ promptGroups.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
10138
+ return { entries, promptGroups, totalRuns: entries.length };
10139
+ }
10140
+ function scoreBar2(score) {
10141
+ const filled = Math.round(score);
10142
+ return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
10143
+ }
10144
+ function scoreColor(score, text) {
10145
+ if (score >= 8) return import_chalk19.default.green(text);
10146
+ if (score >= 6) return import_chalk19.default.yellow(text);
10147
+ return import_chalk19.default.red(text);
10148
+ }
10149
+ function formatDate(iso) {
10150
+ return iso.slice(0, 10);
10151
+ }
10152
+ function formatDuration(ms2) {
10153
+ if (ms2 === null) return " \u2014 ";
10154
+ const s = Math.round(ms2 / 1e3);
10155
+ if (s < 60) return `${s}s`;
10156
+ return `${Math.floor(s / 60)}m${s % 60}s`;
10157
+ }
10158
+ function shortSpec(specPath) {
10159
+ if (!specPath) return import_chalk19.default.gray("\u2014");
10160
+ return path22.basename(specPath);
10161
+ }
10162
+ function printTrendReport(report, workingDir) {
10163
+ const { entries, promptGroups } = report;
10164
+ console.log(import_chalk19.default.cyan("\n\u2500\u2500\u2500 Harness Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10165
+ if (entries.length === 0) {
10166
+ console.log(import_chalk19.default.gray(" No scored runs found. Run `ai-spec create` to start tracking."));
10167
+ console.log(import_chalk19.default.cyan("\u2500".repeat(63)));
10168
+ return;
10169
+ }
10170
+ if (promptGroups.length > 0) {
10171
+ console.log(import_chalk19.default.bold("\n Prompt Versions:\n"));
10172
+ const colWidths = {
10173
+ hash: 10,
10174
+ runs: 5,
10175
+ avg: 5,
10176
+ best: 5,
10177
+ worst: 5
10178
+ };
10179
+ console.log(
10180
+ import_chalk19.default.gray(
10181
+ " " + "Hash ".padEnd(colWidths.hash) + " " + "Runs ".padStart(colWidths.runs) + " Avg Best Worst Last seen"
10182
+ )
10183
+ );
10184
+ console.log(import_chalk19.default.gray(" " + "\u2500".repeat(55)));
10185
+ for (const g of promptGroups) {
10186
+ const currentMark = g.isCurrent ? import_chalk19.default.cyan(" \u25C0 current") : "";
10187
+ const avgStr = scoreColor(g.avg, g.avg.toFixed(1).padStart(5));
10188
+ const bestStr = import_chalk19.default.green(g.best.toFixed(1).padStart(5));
10189
+ const worstStr = g.worst < 6 ? import_chalk19.default.red(g.worst.toFixed(1).padStart(5)) : import_chalk19.default.yellow(g.worst.toFixed(1).padStart(5));
10190
+ console.log(
10191
+ " " + import_chalk19.default.white(g.promptHash.padEnd(colWidths.hash)) + " " + import_chalk19.default.gray(String(g.runs).padStart(colWidths.runs)) + " " + avgStr + " " + bestStr + " " + worstStr + " " + import_chalk19.default.gray(formatDate(g.lastSeen)) + currentMark
10192
+ );
10193
+ }
10194
+ }
10195
+ console.log(import_chalk19.default.bold("\n Run History:\n"));
10196
+ for (const e of entries) {
10197
+ const score = e.harnessScore;
10198
+ const bar = scoreColor(score, `[${scoreBar2(score)}]`);
10199
+ const scoreStr = scoreColor(score, score.toFixed(1).padStart(4));
10200
+ const hash = e.promptHash ? import_chalk19.default.gray(e.promptHash) : import_chalk19.default.gray("(no hash)");
10201
+ const dur = import_chalk19.default.gray(formatDuration(e.totalDurationMs));
10202
+ const errMark = e.errors > 0 ? import_chalk19.default.yellow(` \u26A0${e.errors}err`) : "";
10203
+ const spec = import_chalk19.default.gray(shortSpec(e.specPath));
10204
+ console.log(
10205
+ ` ${import_chalk19.default.gray(formatDate(e.startedAt))} ${bar}${scoreStr} ${hash} ${dur}${errMark} ${spec}`
10206
+ );
10207
+ }
10208
+ const logRelDir = path22.relative(workingDir, path22.join(workingDir, LOG_DIR2));
10209
+ console.log(import_chalk19.default.gray(`
10210
+ ${entries.length} run(s) shown \xB7 logs: ${logRelDir}/`));
10211
+ console.log(import_chalk19.default.cyan("\u2500".repeat(63)));
10212
+ }
10213
+
10214
+ // core/dsl-feedback.ts
10215
+ var import_chalk20 = __toESM(require("chalk"));
10216
+ function assessDslRichness(dsl) {
10217
+ const gaps = [];
10218
+ if (dsl.endpoints.length === 0 && dsl.models.length === 0) {
10219
+ gaps.push({
10220
+ code: "no_models_no_endpoints",
10221
+ message: "DSL has no endpoints and no models \u2014 spec may be too abstract for structured extraction",
10222
+ hint: "Please add explicit API endpoint definitions (method, path, request/response) and any data models that this feature requires."
10223
+ });
10224
+ return gaps;
10225
+ }
10226
+ const GENERIC_DESC_KEYWORDS = ["handles", "processes", "manages", "\u64CD\u4F5C", "\u5904\u7406", "\u7BA1\u7406"];
10227
+ const GENERIC_DESC_MIN_LEN = 15;
10228
+ for (const ep of dsl.endpoints) {
10229
+ const desc = (ep.description ?? "").trim();
10230
+ const isGeneric = desc.length < GENERIC_DESC_MIN_LEN || GENERIC_DESC_KEYWORDS.some((kw) => desc.toLowerCase().startsWith(kw));
10231
+ if (isGeneric) {
10232
+ gaps.push({
10233
+ code: "generic_endpoint_desc",
10234
+ message: `Endpoint ${ep.method} ${ep.path} has a vague description: "${desc}"`,
10235
+ hint: `Clarify what ${ep.method} ${ep.path} does: what inputs are required, what the success response contains, and what business rule it enforces.`
10236
+ });
10237
+ }
10238
+ }
10239
+ const endpointsWithoutErrors = dsl.endpoints.filter(
10240
+ (ep) => !ep.errors || ep.errors.length === 0
10241
+ );
10242
+ if (endpointsWithoutErrors.length > 0 && dsl.endpoints.length >= 2) {
10243
+ gaps.push({
10244
+ code: "missing_errors",
10245
+ message: `${endpointsWithoutErrors.length}/${dsl.endpoints.length} endpoints have no error definitions`,
10246
+ hint: `For each endpoint, specify at least the main error cases: e.g. 400 validation errors, 401 auth failures, 404 not found, 409 conflict. Include an error code (e.g. INVALID_INPUT) and description for each.`
10247
+ });
10248
+ }
10249
+ for (const model of dsl.models) {
10250
+ if (!model.fields || model.fields.length < 2) {
10251
+ gaps.push({
10252
+ code: "sparse_model",
10253
+ message: `Model "${model.name}" has only ${model.fields?.length ?? 0} field(s) \u2014 likely incomplete`,
10254
+ hint: `List all fields for "${model.name}" with their types and whether they are required. Include at minimum an id, created_at, and the core domain fields this model needs.`
10255
+ });
10256
+ }
10257
+ }
10258
+ return gaps;
10259
+ }
10260
+ function buildDslGapRefinementPrompt(spec, gaps) {
10261
+ const gapList = gaps.map((g, i) => `${i + 1}. [${g.code}] ${g.message}
10262
+ \u2192 ${g.hint}`).join("\n\n");
10263
+ return `The following feature spec has been structurally analysed. The DSL extracted from it was found to be incomplete in these specific areas:
10264
+
10265
+ ${gapList}
10266
+
10267
+ Your task: revise the spec below to address ONLY the gaps listed above.
10268
+ - Do NOT change the overall feature scope or business logic.
10269
+ - Do NOT rewrite sections that are already complete.
10270
+ - Add missing error cases, clarify vague endpoint descriptions, complete sparse model field lists.
10271
+ - Output ONLY the complete revised Markdown spec. No preamble, no explanation.
10272
+
10273
+ === Current Spec ===
10274
+ ${spec}`;
10275
+ }
10276
+ function extractStructuralFindings(reviewText) {
10277
+ const parts = reviewText.split(/─{20,}/);
10278
+ const pass1Text = parts[0] ?? "";
10279
+ const pass1Score = extractPassScore(pass1Text);
10280
+ if (pass1Score !== null && pass1Score >= 8) return [];
10281
+ const findings = [];
10282
+ if (/缺少认证|missing auth|auth.*false|未加认证|鉴权.*缺|endpoint.*public.*should/i.test(pass1Text)) {
10283
+ const match = pass1Text.match(/[^。\n]*(?:缺少认证|missing auth|auth.*false|未加认证|鉴权.*缺|endpoint.*public.*should)[^。\n]*/i);
10284
+ findings.push({
10285
+ category: "auth_design",
10286
+ description: match ? match[0].trim() : "One or more endpoints may have incorrect authentication requirements"
10287
+ });
10288
+ }
10289
+ if (/接口设计.*问题|接口.*不合理|API design|response.*missing|request.*missing|接口.*缺少/i.test(pass1Text)) {
10290
+ const match = pass1Text.match(/[^。\n]*(?:接口设计.*问题|接口.*不合理|API design|response.*missing|接口.*缺少)[^。\n]*/i);
10291
+ findings.push({
10292
+ category: "api_contract",
10293
+ description: match ? match[0].trim() : "API contract design may have issues"
10294
+ });
10295
+ }
10296
+ if (/模型.*缺少字段|model.*missing field|数据结构.*问题|schema.*incomplete|字段.*missing/i.test(pass1Text)) {
10297
+ const match = pass1Text.match(/[^。\n]*(?:模型.*缺少字段|model.*missing field|数据结构.*问题|schema.*incomplete)[^。\n]*/i);
10298
+ findings.push({
10299
+ category: "model_design",
10300
+ description: match ? match[0].trim() : "Data model design may be incomplete"
10301
+ });
10302
+ }
10303
+ if (/层级.*违反|layer.*violation|business logic.*controller|controller.*service.*混|分层.*问题/i.test(pass1Text)) {
10304
+ const match = pass1Text.match(/[^。\n]*(?:层级.*违反|layer.*violation|business logic.*controller|分层.*问题)[^。\n]*/i);
10305
+ findings.push({
10306
+ category: "layer_violation",
10307
+ description: match ? match[0].trim() : "Layer separation may be violated in the generated code"
10308
+ });
10309
+ }
10310
+ return findings;
10311
+ }
10312
+ function extractPassScore(text) {
10313
+ const m = text.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
10314
+ return m ? parseFloat(m[1]) : null;
10315
+ }
10316
+ function buildStructuralAmendmentPrompt(spec, findings) {
10317
+ const findingList = findings.map((f, i) => `${i + 1}. [${f.category}] ${f.description}`).join("\n");
10318
+ return `A code review of the feature built from this spec found the following DESIGN-LEVEL issues.
10319
+ These are problems in the spec/contract itself, not in the implementation.
10320
+
10321
+ === Structural Findings ===
10322
+ ${findingList}
10323
+
10324
+ Your task:
10325
+ - Revise the spec below to correct the design issues listed above.
10326
+ - Do NOT change the feature scope, business logic, or sections unrelated to these findings.
10327
+ - Be minimal: only change what is necessary to fix the design issues.
10328
+ - Output ONLY the complete revised Markdown spec. No preamble, no explanation.
10329
+
10330
+ === Current Spec ===
10331
+ ${spec}`;
10332
+ }
10333
+ function printDslGaps(gaps) {
10334
+ console.log(import_chalk20.default.yellow("\n \u26A0 DSL Completeness Check \u2014 gaps detected:"));
10335
+ for (const gap of gaps) {
10336
+ console.log(import_chalk20.default.yellow(` \xB7 ${gap.message}`));
10337
+ }
10338
+ console.log(import_chalk20.default.gray(" \u2192 A targeted spec refinement can fill these gaps before codegen."));
10339
+ }
10340
+ function printStructuralFindings(findings) {
10341
+ console.log(import_chalk20.default.yellow("\n \u26A0 Review \u2014 structural (design-level) issues found:"));
10342
+ for (const f of findings) {
10343
+ const label = import_chalk20.default.gray(`[${f.category}]`);
10344
+ console.log(` ${label} ${f.description}`);
10345
+ }
10346
+ console.log(import_chalk20.default.gray(" \u2192 These are contract issues in the Spec/DSL, not just implementation problems."));
10347
+ console.log(import_chalk20.default.gray(" \u2192 Fixing the spec now means the next run generates correct code from the start."));
10348
+ }
10349
+
9914
10350
  // cli/index.ts
9915
10351
  dotenv.config();
9916
10352
  var CONFIG_FILE = ".ai-spec.json";
9917
10353
  async function loadConfig(dir) {
9918
- const p = path22.join(dir, CONFIG_FILE);
9919
- if (await fs23.pathExists(p)) {
9920
- return fs23.readJson(p);
10354
+ const p = path23.join(dir, CONFIG_FILE);
10355
+ if (await fs24.pathExists(p)) {
10356
+ return fs24.readJson(p);
9921
10357
  }
9922
10358
  return {};
9923
10359
  }
@@ -9942,20 +10378,20 @@ async function resolveApiKey(providerName, cliKey) {
9942
10378
  validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
9943
10379
  });
9944
10380
  await saveKey(providerName, newKey.trim());
9945
- console.log(import_chalk18.default.gray(` Key saved to ${KEY_STORE_FILE}`));
10381
+ console.log(import_chalk21.default.gray(` Key saved to ${KEY_STORE_FILE}`));
9946
10382
  return newKey.trim();
9947
10383
  }
9948
10384
  function printBanner(opts) {
9949
- console.log(import_chalk18.default.blue("\n" + "\u2500".repeat(52)));
9950
- console.log(import_chalk18.default.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
9951
- console.log(import_chalk18.default.blue("\u2500".repeat(52)));
9952
- console.log(import_chalk18.default.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
10385
+ console.log(import_chalk21.default.blue("\n" + "\u2500".repeat(52)));
10386
+ console.log(import_chalk21.default.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
10387
+ console.log(import_chalk21.default.blue("\u2500".repeat(52)));
10388
+ console.log(import_chalk21.default.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
9953
10389
  console.log(
9954
- import_chalk18.default.gray(
10390
+ import_chalk21.default.gray(
9955
10391
  ` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
9956
10392
  )
9957
10393
  );
9958
- console.log(import_chalk18.default.blue("\u2500".repeat(52) + "\n"));
10394
+ console.log(import_chalk21.default.blue("\u2500".repeat(52) + "\n"));
9959
10395
  }
9960
10396
  var program = new import_commander.Command();
9961
10397
  program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
@@ -9982,42 +10418,42 @@ program.command("create").description("Generate a feature spec and kick off code
9982
10418
  const workspaceLoader = new WorkspaceLoader(currentDir);
9983
10419
  const workspaceConfig = await workspaceLoader.load();
9984
10420
  if (workspaceConfig) {
9985
- console.log(import_chalk18.default.cyan(`
10421
+ console.log(import_chalk21.default.cyan(`
9986
10422
  [Workspace] Detected workspace: ${workspaceConfig.name}`));
9987
- console.log(import_chalk18.default.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
10423
+ console.log(import_chalk21.default.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
9988
10424
  const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
9989
10425
  if (opts.serve) {
9990
- console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10426
+ console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
9991
10427
  const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
9992
10428
  const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
9993
10429
  if (!backendResult) {
9994
- console.log(import_chalk18.default.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
10430
+ console.log(import_chalk21.default.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
9995
10431
  } else {
9996
10432
  const mockPort = 3001;
9997
10433
  const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
9998
- const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
9999
- console.log(import_chalk18.default.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
10434
+ const serverJsPath = path23.join(backendResult.repoAbsPath, "mock", "server.js");
10435
+ console.log(import_chalk21.default.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
10000
10436
  const pid = startMockServerBackground(serverJsPath, mockPort);
10001
- console.log(import_chalk18.default.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
10437
+ console.log(import_chalk21.default.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
10002
10438
  if (frontendResult) {
10003
10439
  const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
10004
10440
  await saveMockServerPid(frontendResult.repoAbsPath, pid);
10005
10441
  if (proxyResult.applied) {
10006
- console.log(import_chalk18.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
10007
- console.log(import_chalk18.default.bold.cyan(`
10442
+ console.log(import_chalk21.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
10443
+ console.log(import_chalk21.default.bold.cyan(`
10008
10444
  Ready! Run your frontend dev server:`));
10009
- console.log(import_chalk18.default.white(` cd ${frontendResult.repoAbsPath}`));
10010
- console.log(import_chalk18.default.white(` ${proxyResult.devCommand}`));
10011
- console.log(import_chalk18.default.gray(`
10445
+ console.log(import_chalk21.default.white(` cd ${frontendResult.repoAbsPath}`));
10446
+ console.log(import_chalk21.default.white(` ${proxyResult.devCommand}`));
10447
+ console.log(import_chalk21.default.gray(`
10012
10448
  When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
10013
10449
  } else {
10014
- console.log(import_chalk18.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
10015
- if (proxyResult.note) console.log(import_chalk18.default.gray(` ${proxyResult.note}`));
10016
- console.log(import_chalk18.default.gray(` Mock server: http://localhost:${mockPort}`));
10450
+ console.log(import_chalk21.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
10451
+ if (proxyResult.note) console.log(import_chalk21.default.gray(` ${proxyResult.note}`));
10452
+ console.log(import_chalk21.default.gray(` Mock server: http://localhost:${mockPort}`));
10017
10453
  }
10018
10454
  } else {
10019
- console.log(import_chalk18.default.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
10020
- console.log(import_chalk18.default.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
10455
+ console.log(import_chalk21.default.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
10456
+ console.log(import_chalk21.default.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
10021
10457
  }
10022
10458
  }
10023
10459
  }
@@ -10038,7 +10474,7 @@ program.command("create").description("Generate a feature spec and kick off code
10038
10474
  codegenModel: codegenModelName
10039
10475
  });
10040
10476
  const runId = generateRunId();
10041
- console.log(import_chalk18.default.gray(` Run ID: ${runId}`));
10477
+ console.log(import_chalk21.default.gray(` Run ID: ${runId}`));
10042
10478
  const runSnapshot = new RunSnapshot(currentDir, runId);
10043
10479
  setActiveSnapshot(runSnapshot);
10044
10480
  const runLogger = new RunLogger(currentDir, runId, {
@@ -10046,25 +10482,27 @@ program.command("create").description("Generate a feature spec and kick off code
10046
10482
  model: specModelName
10047
10483
  });
10048
10484
  setActiveLogger(runLogger);
10049
- console.log(import_chalk18.default.blue("[1/6] Loading project context..."));
10485
+ const promptHash = computePromptHash();
10486
+ runLogger.setPromptHash(promptHash);
10487
+ console.log(import_chalk21.default.blue("[1/6] Loading project context..."));
10050
10488
  runLogger.stageStart("context_load");
10051
10489
  const loader = new ContextLoader(currentDir);
10052
10490
  const context = await loader.loadProjectContext();
10053
10491
  const { type: detectedRepoType } = await detectRepoType(currentDir);
10054
10492
  runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
10055
- console.log(import_chalk18.default.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10056
- console.log(import_chalk18.default.gray(` Dependencies: ${context.dependencies.length} packages`));
10057
- console.log(import_chalk18.default.gray(` API files : ${context.apiStructure.length} files`));
10493
+ console.log(import_chalk21.default.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10494
+ console.log(import_chalk21.default.gray(` Dependencies: ${context.dependencies.length} packages`));
10495
+ console.log(import_chalk21.default.gray(` API files : ${context.apiStructure.length} files`));
10058
10496
  if (context.schema) {
10059
- console.log(import_chalk18.default.gray(` Prisma schema: found`));
10497
+ console.log(import_chalk21.default.gray(` Prisma schema: found`));
10060
10498
  }
10061
10499
  if (context.constitution) {
10062
- console.log(import_chalk18.default.green(` Constitution : found (.ai-spec-constitution.md)`));
10500
+ console.log(import_chalk21.default.green(` Constitution : found (.ai-spec-constitution.md)`));
10063
10501
  if (context.constitution.length > 6e3) {
10064
- console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10502
+ console.log(import_chalk21.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10065
10503
  }
10066
10504
  } else {
10067
- console.log(import_chalk18.default.yellow(" Constitution : not found \u2014 auto-generating..."));
10505
+ console.log(import_chalk21.default.yellow(" Constitution : not found \u2014 auto-generating..."));
10068
10506
  try {
10069
10507
  const constitutionGen = new ConstitutionGenerator(
10070
10508
  createProvider(specProviderName, specApiKey, specModelName)
@@ -10072,12 +10510,12 @@ program.command("create").description("Generate a feature spec and kick off code
10072
10510
  const constitutionContent = await constitutionGen.generate(currentDir);
10073
10511
  await constitutionGen.saveConstitution(currentDir, constitutionContent);
10074
10512
  context.constitution = constitutionContent;
10075
- console.log(import_chalk18.default.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
10513
+ console.log(import_chalk21.default.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
10076
10514
  } catch (err) {
10077
- console.log(import_chalk18.default.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
10515
+ console.log(import_chalk21.default.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
10078
10516
  }
10079
10517
  }
10080
- console.log(import_chalk18.default.blue(`
10518
+ console.log(import_chalk21.default.blue(`
10081
10519
  [2/6] Generating spec with ${specProviderName}/${specModelName}...`));
10082
10520
  const specProvider = createProvider(specProviderName, specApiKey, specModelName);
10083
10521
  let initialSpec;
@@ -10087,30 +10525,30 @@ program.command("create").description("Generate a feature spec and kick off code
10087
10525
  if (opts.skipTasks) {
10088
10526
  const generator = new SpecGenerator(specProvider);
10089
10527
  initialSpec = await generator.generateSpec(idea, context);
10090
- console.log(import_chalk18.default.green(" \u2714 Spec generated."));
10528
+ console.log(import_chalk21.default.green(" \u2714 Spec generated."));
10091
10529
  } else {
10092
10530
  const result = await generateSpecWithTasks(specProvider, idea, context);
10093
10531
  initialSpec = result.spec;
10094
10532
  initialTasks = result.tasks;
10095
- console.log(import_chalk18.default.green(` \u2714 Spec generated.`));
10533
+ console.log(import_chalk21.default.green(` \u2714 Spec generated.`));
10096
10534
  if (initialTasks.length > 0) {
10097
- console.log(import_chalk18.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
10535
+ console.log(import_chalk21.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
10098
10536
  } else {
10099
- console.log(import_chalk18.default.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
10537
+ console.log(import_chalk21.default.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
10100
10538
  }
10101
10539
  }
10102
10540
  runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
10103
10541
  } catch (err) {
10104
10542
  runLogger.stageFail("spec_gen", err.message);
10105
- console.error(import_chalk18.default.red(" \u2718 Spec generation failed:"), err);
10543
+ console.error(import_chalk21.default.red(" \u2718 Spec generation failed:"), err);
10106
10544
  process.exit(1);
10107
10545
  }
10108
10546
  let finalSpec;
10109
10547
  if (opts.fast) {
10110
- console.log(import_chalk18.default.gray("\n[3/6] Skipping refinement (--fast)."));
10548
+ console.log(import_chalk21.default.gray("\n[3/6] Skipping refinement (--fast)."));
10111
10549
  finalSpec = initialSpec;
10112
10550
  } else {
10113
- console.log(import_chalk18.default.blue("\n[3/6] Interactive spec refinement..."));
10551
+ console.log(import_chalk21.default.blue("\n[3/6] Interactive spec refinement..."));
10114
10552
  runLogger.stageStart("spec_refine");
10115
10553
  const refiner = new SpecRefiner(specProvider);
10116
10554
  finalSpec = await refiner.refineLoop(initialSpec);
@@ -10121,7 +10559,7 @@ program.command("create").description("Generate a feature spec and kick off code
10121
10559
  const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
10122
10560
  if (shouldRunAssessment) {
10123
10561
  if (!opts.auto) {
10124
- console.log(import_chalk18.default.blue("\n[3.4/6] Spec quality assessment..."));
10562
+ console.log(import_chalk21.default.blue("\n[3.4/6] Spec quality assessment..."));
10125
10563
  }
10126
10564
  runLogger.stageStart("spec_assess");
10127
10565
  const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
@@ -10130,45 +10568,45 @@ program.command("create").description("Generate a feature spec and kick off code
10130
10568
  if (!opts.auto) printSpecAssessment(assessment);
10131
10569
  if (minScore > 0 && assessment.overallScore < minScore) {
10132
10570
  if (opts.force) {
10133
- console.log(import_chalk18.default.yellow(`
10571
+ console.log(import_chalk21.default.yellow(`
10134
10572
  \u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
10135
10573
  } else {
10136
10574
  runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
10137
- console.log(import_chalk18.default.red(`
10575
+ console.log(import_chalk21.default.red(`
10138
10576
  \u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
10139
10577
  if (!opts.auto) {
10140
- console.log(import_chalk18.default.gray(` Address the issues above and re-run, or use --force to bypass.`));
10578
+ console.log(import_chalk21.default.gray(` Address the issues above and re-run, or use --force to bypass.`));
10141
10579
  } else {
10142
- console.log(import_chalk18.default.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
10580
+ console.log(import_chalk21.default.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
10143
10581
  }
10144
- console.log(import_chalk18.default.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
10582
+ console.log(import_chalk21.default.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
10145
10583
  process.exit(1);
10146
10584
  }
10147
10585
  }
10148
10586
  } else {
10149
10587
  runLogger.stageEnd("spec_assess", { skipped: true });
10150
10588
  if (!opts.auto) {
10151
- console.log(import_chalk18.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
10589
+ console.log(import_chalk21.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
10152
10590
  }
10153
10591
  }
10154
10592
  }
10155
10593
  if (!opts.auto) {
10156
- console.log(import_chalk18.default.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
10594
+ console.log(import_chalk21.default.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
10157
10595
  const specLines = finalSpec.split("\n").length;
10158
10596
  const specWords = finalSpec.split(/\s+/).length;
10159
10597
  const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
10160
- console.log(import_chalk18.default.gray(` Spec length : ${specLines} lines / ${specWords} words`));
10161
- if (taskCountHint) console.log(import_chalk18.default.gray(taskCountHint));
10162
- const previewSpecsDir = path22.join(currentDir, "specs");
10598
+ console.log(import_chalk21.default.gray(` Spec length : ${specLines} lines / ${specWords} words`));
10599
+ if (taskCountHint) console.log(import_chalk21.default.gray(taskCountHint));
10600
+ const previewSpecsDir = path23.join(currentDir, "specs");
10163
10601
  const slug = featureSlug;
10164
10602
  const prevVersion = await findLatestVersion(previewSpecsDir, slug);
10165
10603
  if (prevVersion) {
10166
- console.log(import_chalk18.default.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
10604
+ console.log(import_chalk21.default.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
10167
10605
  const diff = computeDiff(prevVersion.content, finalSpec);
10168
- console.log(import_chalk18.default.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10606
+ console.log(import_chalk21.default.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10169
10607
  printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
10170
10608
  printDiff(diff);
10171
- console.log(import_chalk18.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10609
+ console.log(import_chalk21.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10172
10610
  }
10173
10611
  const gate = await (0, import_prompts3.select)({
10174
10612
  message: "Ready to proceed to code generation?",
@@ -10179,9 +10617,9 @@ program.command("create").description("Generate a feature spec and kick off code
10179
10617
  ]
10180
10618
  });
10181
10619
  if (gate === "view") {
10182
- console.log(import_chalk18.default.cyan("\n" + "\u2500".repeat(52)));
10620
+ console.log(import_chalk21.default.cyan("\n" + "\u2500".repeat(52)));
10183
10621
  console.log(finalSpec);
10184
- console.log(import_chalk18.default.cyan("\u2500".repeat(52) + "\n"));
10622
+ console.log(import_chalk21.default.cyan("\u2500".repeat(52) + "\n"));
10185
10623
  const confirm22 = await (0, import_prompts3.select)({
10186
10624
  message: "Proceed to code generation?",
10187
10625
  choices: [
@@ -10190,92 +10628,135 @@ program.command("create").description("Generate a feature spec and kick off code
10190
10628
  ]
10191
10629
  });
10192
10630
  if (confirm22 === "abort") {
10193
- console.log(import_chalk18.default.yellow(" Aborted. Spec was NOT saved."));
10631
+ console.log(import_chalk21.default.yellow(" Aborted. Spec was NOT saved."));
10194
10632
  process.exit(0);
10195
10633
  }
10196
10634
  } else if (gate === "abort") {
10197
- console.log(import_chalk18.default.yellow(" Aborted. Spec was NOT saved."));
10635
+ console.log(import_chalk21.default.yellow(" Aborted. Spec was NOT saved."));
10198
10636
  process.exit(0);
10199
10637
  }
10200
- console.log(import_chalk18.default.green(" \u2714 Approved \u2014 continuing to code generation."));
10638
+ console.log(import_chalk21.default.green(" \u2714 Approved \u2014 continuing to code generation."));
10201
10639
  } else {
10202
- console.log(import_chalk18.default.gray("[3.5/6] Approval Gate: skipped (--auto)."));
10640
+ console.log(import_chalk21.default.gray("[3.5/6] Approval Gate: skipped (--auto)."));
10203
10641
  }
10204
10642
  let extractedDsl = null;
10205
10643
  if (opts.skipDsl) {
10206
- console.log(import_chalk18.default.gray("\n[DSL] Skipped (--skip-dsl)."));
10644
+ console.log(import_chalk21.default.gray("\n[DSL] Skipped (--skip-dsl)."));
10207
10645
  } else {
10208
- console.log(import_chalk18.default.blue("\n[DSL] Extracting structured DSL from spec..."));
10209
- console.log(import_chalk18.default.gray(` Provider: ${specProviderName}/${specModelName}`));
10646
+ console.log(import_chalk21.default.blue("\n[DSL] Extracting structured DSL from spec..."));
10647
+ console.log(import_chalk21.default.gray(` Provider: ${specProviderName}/${specModelName}`));
10210
10648
  runLogger.stageStart("dsl_extract");
10211
10649
  try {
10212
10650
  const isFrontend = isFrontendDeps(context.dependencies);
10213
- if (isFrontend) console.log(import_chalk18.default.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
10651
+ if (isFrontend) console.log(import_chalk21.default.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
10214
10652
  const dslExtractor = new DslExtractor(specProvider);
10215
10653
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
10216
10654
  if (extractedDsl) {
10217
10655
  runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
10218
- console.log(import_chalk18.default.green(" \u2714 DSL extracted and validated."));
10656
+ console.log(import_chalk21.default.green(" \u2714 DSL extracted and validated."));
10219
10657
  } else {
10220
10658
  runLogger.stageEnd("dsl_extract", { skipped: true });
10221
- console.log(import_chalk18.default.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
10659
+ console.log(import_chalk21.default.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
10222
10660
  }
10223
10661
  } catch (err) {
10224
10662
  runLogger.stageFail("dsl_extract", err.message);
10225
- console.log(import_chalk18.default.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
10663
+ console.log(import_chalk21.default.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
10664
+ }
10665
+ }
10666
+ if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
10667
+ const dslGaps = assessDslRichness(extractedDsl);
10668
+ if (dslGaps.length > 0) {
10669
+ printDslGaps(dslGaps);
10670
+ runLogger.stageStart("dsl_gap_feedback", { gapCount: dslGaps.length, gaps: dslGaps.map((g) => g.code) });
10671
+ const refineChoice = await (0, import_prompts3.select)({
10672
+ message: "How would you like to proceed?",
10673
+ choices: [
10674
+ { name: "\u{1F527} Refine spec (AI fills the gaps, then re-extract DSL)", value: "refine" },
10675
+ { name: "\u23ED Skip \u2014 proceed with the current DSL", value: "skip" }
10676
+ ]
10677
+ });
10678
+ if (refineChoice === "refine") {
10679
+ console.log(import_chalk21.default.blue(" Refining spec to fill DSL gaps..."));
10680
+ try {
10681
+ const refinedSpec = await specProvider.generate(
10682
+ buildDslGapRefinementPrompt(finalSpec, dslGaps),
10683
+ "You are a Senior Tech Lead doing a targeted spec revision. Output only the complete revised Markdown spec."
10684
+ );
10685
+ finalSpec = refinedSpec;
10686
+ console.log(import_chalk21.default.green(" \u2714 Spec refined."));
10687
+ console.log(import_chalk21.default.blue(" Re-extracting DSL from refined spec..."));
10688
+ const isFrontend2 = isFrontendDeps(context.dependencies);
10689
+ const reExtractor = new DslExtractor(specProvider);
10690
+ const reExtractedDsl = await reExtractor.extract(finalSpec, { auto: true, isFrontend: isFrontend2 });
10691
+ if (reExtractedDsl) {
10692
+ extractedDsl = reExtractedDsl;
10693
+ console.log(import_chalk21.default.green(` \u2714 DSL re-extracted: ${extractedDsl.endpoints.length} endpoint(s), ${extractedDsl.models.length} model(s).`));
10694
+ runLogger.stageEnd("dsl_gap_feedback", { action: "refined", endpoints: extractedDsl.endpoints.length, models: extractedDsl.models.length });
10695
+ } else {
10696
+ console.log(import_chalk21.default.yellow(" \u26A0 Re-extraction failed \u2014 keeping original DSL."));
10697
+ runLogger.stageEnd("dsl_gap_feedback", { action: "refined_but_reextract_failed" });
10698
+ }
10699
+ } catch (err) {
10700
+ console.log(import_chalk21.default.yellow(` \u26A0 Spec refinement failed: ${err.message} \u2014 keeping original DSL.`));
10701
+ runLogger.stageEnd("dsl_gap_feedback", { action: "refinement_error", error: err.message });
10702
+ }
10703
+ } else {
10704
+ runLogger.stageEnd("dsl_gap_feedback", { action: "skipped" });
10705
+ console.log(import_chalk21.default.gray(" Continuing with current DSL."));
10706
+ }
10226
10707
  }
10227
10708
  }
10228
10709
  const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
10229
10710
  const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
10230
10711
  let workingDir = currentDir;
10231
10712
  if (!skipWorktree) {
10232
- console.log(import_chalk18.default.blue("\n[4/6] Setting up git worktree..."));
10713
+ console.log(import_chalk21.default.blue("\n[4/6] Setting up git worktree..."));
10233
10714
  const worktreeManager = new GitWorktreeManager(currentDir);
10234
10715
  const worktreePath = await worktreeManager.createWorktree(idea);
10235
10716
  if (worktreePath) workingDir = worktreePath;
10236
10717
  } else {
10237
10718
  const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
10238
- console.log(import_chalk18.default.gray(`[4/6] Skipping worktree${reason}.`));
10719
+ console.log(import_chalk21.default.gray(`[4/6] Skipping worktree${reason}.`));
10239
10720
  }
10240
- const specsDir = path22.join(workingDir, "specs");
10241
- await fs23.ensureDir(specsDir);
10721
+ const specsDir = path23.join(workingDir, "specs");
10722
+ await fs24.ensureDir(specsDir);
10242
10723
  const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
10243
- await fs23.writeFile(specFile, finalSpec, "utf-8");
10244
- console.log(import_chalk18.default.green(`
10245
- [5/6] \u2714 Spec saved: ${specFile}`) + import_chalk18.default.gray(` (v${specVersion})`));
10724
+ await fs24.writeFile(specFile, finalSpec, "utf-8");
10725
+ console.log(import_chalk21.default.green(`
10726
+ [5/6] \u2714 Spec saved: ${specFile}`) + import_chalk21.default.gray(` (v${specVersion})`));
10246
10727
  let savedDslFile = null;
10247
10728
  if (extractedDsl) {
10248
10729
  const dslExtractor = new DslExtractor(specProvider);
10249
10730
  savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
10250
- console.log(import_chalk18.default.green(` \u2714 DSL saved : ${savedDslFile}`));
10731
+ console.log(import_chalk21.default.green(` \u2714 DSL saved : ${savedDslFile}`));
10251
10732
  }
10252
10733
  if (!opts.skipTasks) {
10253
10734
  const taskGen = new TaskGenerator(specProvider);
10254
10735
  let tasksToSave = initialTasks;
10255
10736
  if (tasksToSave.length === 0) {
10256
- console.log(import_chalk18.default.blue(`
10737
+ console.log(import_chalk21.default.blue(`
10257
10738
  Generating tasks (separate call)...`));
10258
10739
  try {
10259
10740
  tasksToSave = await taskGen.generateTasks(finalSpec, context);
10260
10741
  } catch (err) {
10261
- console.log(import_chalk18.default.yellow(` \u26A0 Task generation failed: ${err.message}`));
10742
+ console.log(import_chalk21.default.yellow(` \u26A0 Task generation failed: ${err.message}`));
10262
10743
  }
10263
10744
  }
10264
10745
  if (tasksToSave.length > 0) {
10265
10746
  const sorted = taskGen.sortByLayer(tasksToSave);
10266
10747
  const tasksFile = await taskGen.saveTasks(sorted, specFile);
10267
10748
  printTasks(sorted);
10268
- console.log(import_chalk18.default.green(` \u2714 Tasks saved: ${tasksFile}`));
10749
+ console.log(import_chalk21.default.green(` \u2714 Tasks saved: ${tasksFile}`));
10269
10750
  } else {
10270
- console.log(import_chalk18.default.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
10751
+ console.log(import_chalk21.default.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
10271
10752
  }
10272
10753
  }
10273
- console.log(import_chalk18.default.blue(`
10754
+ console.log(import_chalk21.default.blue(`
10274
10755
  [6/6] Code generation (mode: ${codegenMode})...`));
10275
10756
  const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
10276
10757
  let generatedTestFiles = [];
10277
10758
  if (opts.tdd && extractedDsl) {
10278
- console.log(import_chalk18.default.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
10759
+ console.log(import_chalk21.default.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
10279
10760
  const testGen = new TestGenerator(codegenProvider);
10280
10761
  generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
10281
10762
  }
@@ -10289,27 +10770,29 @@ program.command("create").description("Generate a feature spec and kick off code
10289
10770
  });
10290
10771
  runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
10291
10772
  if (opts.tdd) {
10292
- console.log(import_chalk18.default.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
10773
+ console.log(import_chalk21.default.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
10293
10774
  } else if (opts.skipTests) {
10294
- console.log(import_chalk18.default.gray("\n[7/9] Skipping test generation (--skip-tests)."));
10775
+ console.log(import_chalk21.default.gray("\n[7/9] Skipping test generation (--skip-tests)."));
10295
10776
  } else if (!extractedDsl) {
10296
- console.log(import_chalk18.default.gray("\n[7/9] Skipping test generation (no DSL available)."));
10777
+ console.log(import_chalk21.default.gray("\n[7/9] Skipping test generation (no DSL available)."));
10297
10778
  } else {
10298
- console.log(import_chalk18.default.blue(`
10779
+ console.log(import_chalk21.default.blue(`
10299
10780
  [7/9] Test skeleton generation...`));
10300
10781
  runLogger.stageStart("test_gen");
10301
10782
  const testGen = new TestGenerator(codegenProvider);
10302
10783
  generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
10303
10784
  runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
10304
10785
  }
10786
+ let compilePassed = false;
10305
10787
  if (opts.skipErrorFeedback) {
10306
- console.log(import_chalk18.default.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
10788
+ console.log(import_chalk21.default.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
10789
+ compilePassed = true;
10307
10790
  } else {
10308
10791
  if (opts.tdd) {
10309
- console.log(import_chalk18.default.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
10792
+ console.log(import_chalk21.default.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
10310
10793
  }
10311
10794
  runLogger.stageStart("error_feedback");
10312
- await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
10795
+ compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
10313
10796
  maxCycles: opts.tdd ? 3 : 2
10314
10797
  // TDD gets one extra cycle
10315
10798
  });
@@ -10317,10 +10800,10 @@ program.command("create").description("Generate a feature spec and kick off code
10317
10800
  }
10318
10801
  let reviewResult = "";
10319
10802
  if (!opts.skipReview) {
10320
- console.log(import_chalk18.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
10803
+ console.log(import_chalk21.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
10321
10804
  runLogger.stageStart("review");
10322
10805
  const reviewer = new CodeReviewer(specProvider, currentDir);
10323
- const savedSpec = await fs23.readFile(specFile, "utf-8");
10806
+ const savedSpec = await fs24.readFile(specFile, "utf-8");
10324
10807
  if (codegenMode === "api" && generatedFiles.length > 0) {
10325
10808
  reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
10326
10809
  } else {
@@ -10335,20 +10818,89 @@ program.command("create").description("Generate a feature spec and kick off code
10335
10818
  runLogger.stageEnd("review");
10336
10819
  await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
10337
10820
  }
10821
+ if (reviewResult && !opts.skipReview && !opts.auto && extractedDsl && savedDslFile) {
10822
+ const structuralFindings = extractStructuralFindings(reviewResult);
10823
+ if (structuralFindings.length > 0) {
10824
+ printStructuralFindings(structuralFindings);
10825
+ runLogger.stageStart("review_dsl_feedback", { findingCount: structuralFindings.length, categories: structuralFindings.map((f) => f.category) });
10826
+ const savedSpecContent = await fs24.readFile(specFile, "utf-8");
10827
+ const patchChoice = await (0, import_prompts3.select)({
10828
+ message: "These are design issues in the Spec/DSL. How would you like to handle them?",
10829
+ choices: [
10830
+ { name: "\u{1F527} Amend spec + update DSL (AI fixes the design issues, no regen yet)", value: "amend" },
10831
+ { name: "\u{1F4DD} Note in \xA79 only (already done \u2014 no DSL change)", value: "note" },
10832
+ { name: "\u23ED Skip", value: "skip" }
10833
+ ]
10834
+ });
10835
+ if (patchChoice === "amend") {
10836
+ console.log(import_chalk21.default.blue(" Amending spec to address structural findings..."));
10837
+ try {
10838
+ const amendedSpec = await specProvider.generate(
10839
+ buildStructuralAmendmentPrompt(savedSpecContent, structuralFindings),
10840
+ "You are a Senior Tech Lead doing a targeted spec correction. Output only the complete revised Markdown spec."
10841
+ );
10842
+ await runSnapshot.snapshotFile(specFile);
10843
+ if (savedDslFile) await runSnapshot.snapshotFile(savedDslFile);
10844
+ await fs24.writeFile(specFile, amendedSpec, "utf-8");
10845
+ console.log(import_chalk21.default.green(` \u2714 Spec updated: ${specFile}`));
10846
+ console.log(import_chalk21.default.blue(" Re-extracting DSL from amended spec..."));
10847
+ const isFrontend3 = isFrontendDeps(context.dependencies);
10848
+ const amendExtractor = new DslExtractor(specProvider);
10849
+ const amendedDsl = await amendExtractor.extract(amendedSpec, { auto: true, isFrontend: isFrontend3 });
10850
+ if (amendedDsl) {
10851
+ const dslWriter = new DslExtractor(specProvider);
10852
+ const newDslPath = await dslWriter.saveDsl(amendedDsl, specFile);
10853
+ extractedDsl = amendedDsl;
10854
+ console.log(import_chalk21.default.green(` \u2714 DSL updated: ${newDslPath}`));
10855
+ console.log(import_chalk21.default.cyan(
10856
+ `
10857
+ Next step: run ${import_chalk21.default.white("ai-spec update --codegen")} to regenerate files affected by the DSL change.`
10858
+ ));
10859
+ runLogger.stageEnd("review_dsl_feedback", {
10860
+ action: "amended",
10861
+ endpoints: amendedDsl.endpoints.length,
10862
+ models: amendedDsl.models.length
10863
+ });
10864
+ } else {
10865
+ console.log(import_chalk21.default.yellow(" \u26A0 DSL re-extraction failed \u2014 spec was updated but DSL file unchanged."));
10866
+ runLogger.stageEnd("review_dsl_feedback", { action: "amended_spec_only" });
10867
+ }
10868
+ } catch (err) {
10869
+ console.log(import_chalk21.default.yellow(` \u26A0 Spec amendment failed: ${err.message}`));
10870
+ runLogger.stageEnd("review_dsl_feedback", { action: "amendment_error", error: err.message });
10871
+ }
10872
+ } else {
10873
+ runLogger.stageEnd("review_dsl_feedback", { action: patchChoice });
10874
+ if (patchChoice === "note") {
10875
+ console.log(import_chalk21.default.gray(" Structural findings retained in \xA79. DSL unchanged."));
10876
+ }
10877
+ }
10878
+ }
10879
+ }
10880
+ runLogger.stageStart("self_eval");
10881
+ const selfEvalResult = runSelfEval({
10882
+ dsl: extractedDsl,
10883
+ generatedFiles,
10884
+ compilePassed,
10885
+ reviewText: reviewResult,
10886
+ promptHash,
10887
+ logger: runLogger
10888
+ });
10889
+ printSelfEval(selfEvalResult);
10338
10890
  runLogger.finish();
10339
- console.log(import_chalk18.default.bold.green("\n\u2714 All done!"));
10340
- console.log(import_chalk18.default.gray(` Spec : ${specFile}`));
10341
- if (savedDslFile) console.log(import_chalk18.default.gray(` DSL : ${savedDslFile}`));
10891
+ console.log(import_chalk21.default.bold.green("\n\u2714 All done!"));
10892
+ console.log(import_chalk21.default.gray(` Spec : ${specFile}`));
10893
+ if (savedDslFile) console.log(import_chalk21.default.gray(` DSL : ${savedDslFile}`));
10342
10894
  if (generatedTestFiles.length > 0) {
10343
- console.log(import_chalk18.default.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
10895
+ console.log(import_chalk21.default.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
10344
10896
  }
10345
- console.log(import_chalk18.default.gray(` Working dir : ${workingDir}`));
10897
+ console.log(import_chalk21.default.gray(` Working dir : ${workingDir}`));
10346
10898
  if (workingDir !== currentDir) {
10347
- console.log(import_chalk18.default.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10899
+ console.log(import_chalk21.default.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10348
10900
  }
10349
10901
  runLogger.printSummary();
10350
10902
  if (runSnapshot.fileCount > 0) {
10351
- console.log(import_chalk18.default.gray(` To undo changes: ai-spec restore ${runId}`));
10903
+ console.log(import_chalk21.default.gray(` To undo changes: ai-spec restore ${runId}`));
10352
10904
  }
10353
10905
  });
10354
10906
  program.command("review").description("Run AI code review on current git diff against a spec").argument("[specFile]", "Path to spec file (auto-detects latest in specs/ if omitted)").option(
@@ -10365,24 +10917,24 @@ program.command("review").description("Run AI code review on current git diff ag
10365
10917
  const reviewer = new CodeReviewer(provider, currentDir);
10366
10918
  let specContent = "";
10367
10919
  let resolvedSpecFile;
10368
- if (specFile && await fs23.pathExists(specFile)) {
10369
- specContent = await fs23.readFile(specFile, "utf-8");
10920
+ if (specFile && await fs24.pathExists(specFile)) {
10921
+ specContent = await fs24.readFile(specFile, "utf-8");
10370
10922
  resolvedSpecFile = specFile;
10371
- console.log(import_chalk18.default.gray(`Using spec: ${specFile}`));
10923
+ console.log(import_chalk21.default.gray(`Using spec: ${specFile}`));
10372
10924
  } else {
10373
- const specsDir = path22.join(currentDir, "specs");
10374
- if (await fs23.pathExists(specsDir)) {
10375
- const files = (await fs23.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
10925
+ const specsDir = path23.join(currentDir, "specs");
10926
+ if (await fs24.pathExists(specsDir)) {
10927
+ const files = (await fs24.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
10376
10928
  if (files.length > 0) {
10377
- const latest = path22.join(specsDir, files[0]);
10378
- specContent = await fs23.readFile(latest, "utf-8");
10929
+ const latest = path23.join(specsDir, files[0]);
10930
+ specContent = await fs24.readFile(latest, "utf-8");
10379
10931
  resolvedSpecFile = latest;
10380
- console.log(import_chalk18.default.gray(`Auto-detected spec: specs/${files[0]}`));
10932
+ console.log(import_chalk21.default.gray(`Auto-detected spec: specs/${files[0]}`));
10381
10933
  }
10382
10934
  }
10383
10935
  }
10384
10936
  if (!specContent) {
10385
- console.log(import_chalk18.default.yellow("No spec file found. Running review without spec context."));
10937
+ console.log(import_chalk21.default.yellow("No spec file found. Running review without spec context."));
10386
10938
  }
10387
10939
  await reviewer.reviewCode(specContent, resolvedSpecFile);
10388
10940
  await reviewer.printScoreTrend();
@@ -10409,15 +10961,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
10409
10961
  auto: opts.auto
10410
10962
  });
10411
10963
  if (result.written) {
10412
- console.log(import_chalk18.default.blue("\n Summary:"));
10413
- console.log(import_chalk18.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)})`));
10414
- console.log(import_chalk18.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
10964
+ console.log(import_chalk21.default.blue("\n Summary:"));
10965
+ console.log(import_chalk21.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)})`));
10966
+ console.log(import_chalk21.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
10415
10967
  if (result.backupPath) {
10416
- console.log(import_chalk18.default.gray(` Backup: ${path22.basename(result.backupPath)}`));
10968
+ console.log(import_chalk21.default.gray(` Backup: ${path23.basename(result.backupPath)}`));
10417
10969
  }
10418
10970
  }
10419
10971
  } catch (err) {
10420
- console.error(import_chalk18.default.red(` \u2718 Consolidation failed: ${err.message}`));
10972
+ console.error(import_chalk21.default.red(` \u2718 Consolidation failed: ${err.message}`));
10421
10973
  process.exit(1);
10422
10974
  }
10423
10975
  return;
@@ -10425,75 +10977,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
10425
10977
  if (opts.global) {
10426
10978
  const existing = await loadGlobalConstitution([currentDir]);
10427
10979
  if (existing && !opts.force) {
10428
- console.log(import_chalk18.default.yellow(`
10980
+ console.log(import_chalk21.default.yellow(`
10429
10981
  Global constitution already exists at: ${existing.source}`));
10430
- console.log(import_chalk18.default.gray(" Use --force to overwrite it."));
10982
+ console.log(import_chalk21.default.gray(" Use --force to overwrite it."));
10431
10983
  return;
10432
10984
  }
10433
- console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10434
- console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
10435
- console.log(import_chalk18.default.gray(" Scanning repos in workspace..."));
10985
+ console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10986
+ console.log(import_chalk21.default.gray(` Provider: ${providerName}/${modelName}`));
10987
+ console.log(import_chalk21.default.gray(" Scanning repos in workspace..."));
10436
10988
  const loader = new ContextLoader(currentDir);
10437
10989
  const ctx = await loader.loadProjectContext();
10438
10990
  const summary = [
10439
10991
  `Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
10440
10992
  `Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
10441
10993
  ].join("\n");
10442
- const prompt = buildGlobalConstitutionPrompt([{ name: path22.basename(currentDir), summary }]);
10994
+ const prompt = buildGlobalConstitutionPrompt([{ name: path23.basename(currentDir), summary }]);
10443
10995
  let globalConstitution;
10444
10996
  try {
10445
10997
  globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
10446
10998
  } catch (err) {
10447
- console.error(import_chalk18.default.red(" \u2718 Failed to generate global constitution:"), err);
10999
+ console.error(import_chalk21.default.red(" \u2718 Failed to generate global constitution:"), err);
10448
11000
  process.exit(1);
10449
11001
  }
10450
11002
  const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
10451
- console.log(import_chalk18.default.green(`
11003
+ console.log(import_chalk21.default.green(`
10452
11004
  \u2714 Global constitution saved: ${saved2}`));
10453
- console.log(import_chalk18.default.gray(" This will be automatically merged into all project constitutions in this workspace."));
10454
- console.log(import_chalk18.default.gray(" Project-level rules always override global rules.\n"));
10455
- console.log(import_chalk18.default.bold(" Preview:"));
10456
- console.log(import_chalk18.default.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
11005
+ console.log(import_chalk21.default.gray(" This will be automatically merged into all project constitutions in this workspace."));
11006
+ console.log(import_chalk21.default.gray(" Project-level rules always override global rules.\n"));
11007
+ console.log(import_chalk21.default.bold(" Preview:"));
11008
+ console.log(import_chalk21.default.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
10457
11009
  if (globalConstitution.split("\n").length > 12) {
10458
- console.log(import_chalk18.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
11010
+ console.log(import_chalk21.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
10459
11011
  }
10460
11012
  return;
10461
11013
  }
10462
- const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
10463
- if (!opts.force && await fs23.pathExists(constitutionPath)) {
10464
- console.log(import_chalk18.default.yellow(`
11014
+ const constitutionPath = path23.join(currentDir, CONSTITUTION_FILE);
11015
+ if (!opts.force && await fs24.pathExists(constitutionPath)) {
11016
+ console.log(import_chalk21.default.yellow(`
10465
11017
  ${CONSTITUTION_FILE} already exists.`));
10466
- console.log(import_chalk18.default.gray(" Use --force to overwrite it."));
10467
- console.log(import_chalk18.default.gray(` Or edit it directly: ${constitutionPath}`));
11018
+ console.log(import_chalk21.default.gray(" Use --force to overwrite it."));
11019
+ console.log(import_chalk21.default.gray(` Or edit it directly: ${constitutionPath}`));
10468
11020
  return;
10469
11021
  }
10470
- console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10471
- console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
10472
- console.log(import_chalk18.default.gray(" Analyzing codebase..."));
11022
+ console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11023
+ console.log(import_chalk21.default.gray(` Provider: ${providerName}/${modelName}`));
11024
+ console.log(import_chalk21.default.gray(" Analyzing codebase..."));
10473
11025
  const generator = new ConstitutionGenerator(provider);
10474
11026
  let constitution;
10475
11027
  try {
10476
11028
  constitution = await generator.generate(currentDir);
10477
11029
  } catch (err) {
10478
- console.error(import_chalk18.default.red(" \u2718 Failed to generate constitution:"), err);
11030
+ console.error(import_chalk21.default.red(" \u2718 Failed to generate constitution:"), err);
10479
11031
  process.exit(1);
10480
11032
  }
10481
11033
  const saved = await generator.saveConstitution(currentDir, constitution);
10482
- const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
11034
+ const globalResult = await loadGlobalConstitution([path23.dirname(currentDir)]);
10483
11035
  if (globalResult) {
10484
- console.log(import_chalk18.default.cyan(`
11036
+ console.log(import_chalk21.default.cyan(`
10485
11037
  \u2139 Global constitution detected: ${globalResult.source}`));
10486
- console.log(import_chalk18.default.gray(" It will be merged with this project constitution at runtime."));
10487
- console.log(import_chalk18.default.gray(" Project rules take priority over global rules."));
11038
+ console.log(import_chalk21.default.gray(" It will be merged with this project constitution at runtime."));
11039
+ console.log(import_chalk21.default.gray(" Project rules take priority over global rules."));
10488
11040
  }
10489
- console.log(import_chalk18.default.green(`
11041
+ console.log(import_chalk21.default.green(`
10490
11042
  \u2714 Constitution saved: ${saved}`));
10491
- console.log(import_chalk18.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
10492
- console.log(import_chalk18.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
10493
- console.log(import_chalk18.default.bold(" Preview:"));
10494
- console.log(import_chalk18.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
11043
+ console.log(import_chalk21.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
11044
+ console.log(import_chalk21.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
11045
+ console.log(import_chalk21.default.bold(" Preview:"));
11046
+ console.log(import_chalk21.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
10495
11047
  if (constitution.split("\n").length > 15) {
10496
- console.log(import_chalk18.default.gray(` ... (${constitution.split("\n").length} lines total)`));
11048
+ console.log(import_chalk21.default.gray(` ... (${constitution.split("\n").length} lines total)`));
10497
11049
  }
10498
11050
  });
10499
11051
  program.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(
@@ -10501,44 +11053,44 @@ program.command("config").description(`Set default configuration for this projec
10501
11053
  "Default code generation mode (claude-code|api|plan)"
10502
11054
  ).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("--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) => {
10503
11055
  const currentDir = process.cwd();
10504
- const configPath = path22.join(currentDir, CONFIG_FILE);
11056
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10505
11057
  if (opts.clearKeys) {
10506
11058
  await clearAllKeys();
10507
- console.log(import_chalk18.default.green(`\u2714 All saved API keys cleared.`));
11059
+ console.log(import_chalk21.default.green(`\u2714 All saved API keys cleared.`));
10508
11060
  return;
10509
11061
  }
10510
11062
  if (opts.clearKey) {
10511
11063
  await clearKey(opts.clearKey);
10512
- console.log(import_chalk18.default.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
11064
+ console.log(import_chalk21.default.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
10513
11065
  return;
10514
11066
  }
10515
11067
  if (opts.listKeys) {
10516
- const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
11068
+ const store = await fs24.readJson(KEY_STORE_FILE).catch(() => ({}));
10517
11069
  const providers = Object.keys(store);
10518
11070
  if (providers.length === 0) {
10519
- console.log(import_chalk18.default.gray("No saved API keys."));
11071
+ console.log(import_chalk21.default.gray("No saved API keys."));
10520
11072
  } else {
10521
- console.log(import_chalk18.default.bold("Saved API keys:"));
11073
+ console.log(import_chalk21.default.bold("Saved API keys:"));
10522
11074
  for (const p of providers) {
10523
11075
  const k2 = store[p];
10524
- console.log(import_chalk18.default.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
11076
+ console.log(import_chalk21.default.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
10525
11077
  }
10526
- console.log(import_chalk18.default.gray(`
11078
+ console.log(import_chalk21.default.gray(`
10527
11079
  File: ${KEY_STORE_FILE}`));
10528
11080
  }
10529
11081
  return;
10530
11082
  }
10531
11083
  if (opts.reset) {
10532
- await fs23.writeJson(configPath, {}, { spaces: 2 });
10533
- console.log(import_chalk18.default.green(`\u2714 Config reset: ${configPath}`));
11084
+ await fs24.writeJson(configPath, {}, { spaces: 2 });
11085
+ console.log(import_chalk21.default.green(`\u2714 Config reset: ${configPath}`));
10534
11086
  return;
10535
11087
  }
10536
11088
  const existing = await loadConfig(currentDir);
10537
11089
  if (opts.show) {
10538
11090
  if (Object.keys(existing).length === 0) {
10539
- console.log(import_chalk18.default.gray("No config file found. Using built-in defaults."));
11091
+ console.log(import_chalk21.default.gray("No config file found. Using built-in defaults."));
10540
11092
  } else {
10541
- console.log(import_chalk18.default.bold(`${configPath}:`));
11093
+ console.log(import_chalk21.default.bold(`${configPath}:`));
10542
11094
  console.log(JSON.stringify(existing, null, 2));
10543
11095
  }
10544
11096
  return;
@@ -10552,27 +11104,27 @@ File: ${KEY_STORE_FILE}`));
10552
11104
  if (opts.minSpecScore !== void 0) {
10553
11105
  const score = parseInt(opts.minSpecScore, 10);
10554
11106
  if (isNaN(score) || score < 0 || score > 10) {
10555
- console.error(import_chalk18.default.red(" --min-spec-score must be a number between 0 and 10"));
11107
+ console.error(import_chalk21.default.red(" --min-spec-score must be a number between 0 and 10"));
10556
11108
  process.exit(1);
10557
11109
  }
10558
11110
  updated.minSpecScore = score;
10559
11111
  }
10560
- await fs23.writeJson(configPath, updated, { spaces: 2 });
10561
- console.log(import_chalk18.default.green(`\u2714 Config saved to ${configPath}`));
11112
+ await fs24.writeJson(configPath, updated, { spaces: 2 });
11113
+ console.log(import_chalk21.default.green(`\u2714 Config saved to ${configPath}`));
10562
11114
  console.log(JSON.stringify(updated, null, 2));
10563
11115
  });
10564
11116
  program.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) => {
10565
11117
  const currentDir = process.cwd();
10566
- const configPath = path22.join(currentDir, CONFIG_FILE);
11118
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10567
11119
  if (opts.list) {
10568
- console.log(import_chalk18.default.bold("\nAvailable providers & models:\n"));
11120
+ console.log(import_chalk21.default.bold("\nAvailable providers & models:\n"));
10569
11121
  for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
10570
11122
  console.log(
10571
- ` ${import_chalk18.default.bold.cyan(key.padEnd(10))} ${import_chalk18.default.white(meta.displayName)}`
11123
+ ` ${import_chalk21.default.bold.cyan(key.padEnd(10))} ${import_chalk21.default.white(meta.displayName)}`
10572
11124
  );
10573
- console.log(import_chalk18.default.gray(` ${meta.description}`));
11125
+ console.log(import_chalk21.default.gray(` ${meta.description}`));
10574
11126
  console.log(
10575
- import_chalk18.default.gray(
11127
+ import_chalk21.default.gray(
10576
11128
  ` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
10577
11129
  )
10578
11130
  );
@@ -10581,10 +11133,10 @@ program.command("model").description("Interactively switch the active AI provide
10581
11133
  return;
10582
11134
  }
10583
11135
  const existing = await loadConfig(currentDir);
10584
- console.log(import_chalk18.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"));
11136
+ console.log(import_chalk21.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"));
10585
11137
  if (Object.keys(existing).length > 0) {
10586
11138
  console.log(
10587
- import_chalk18.default.gray(
11139
+ import_chalk21.default.gray(
10588
11140
  ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
10589
11141
  )
10590
11142
  );
@@ -10602,7 +11154,7 @@ program.command("model").description("Interactively switch the active AI provide
10602
11154
  const providerKey = await (0, import_prompts3.select)({
10603
11155
  message: `${label} \u2014 select provider:`,
10604
11156
  choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
10605
- name: `${meta2.displayName.padEnd(22)} ${import_chalk18.default.gray(meta2.description)}`,
11157
+ name: `${meta2.displayName.padEnd(22)} ${import_chalk21.default.gray(meta2.description)}`,
10606
11158
  value: key,
10607
11159
  short: meta2.displayName
10608
11160
  }))
@@ -10610,7 +11162,7 @@ program.command("model").description("Interactively switch the active AI provide
10610
11162
  const meta = PROVIDER_CATALOG[providerKey];
10611
11163
  const modelChoices = [
10612
11164
  ...meta.models.map((m) => ({ name: m, value: m })),
10613
- { name: import_chalk18.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
11165
+ { name: import_chalk21.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
10614
11166
  ];
10615
11167
  let chosenModel = await (0, import_prompts3.select)({
10616
11168
  message: `${label} \u2014 select model (${meta.displayName}):`,
@@ -10644,37 +11196,37 @@ program.command("model").description("Interactively switch the active AI provide
10644
11196
  if (!updated.codegen || updated.codegen === "claude-code") {
10645
11197
  updated.codegen = "api";
10646
11198
  console.log(
10647
- import_chalk18.default.yellow(
11199
+ import_chalk21.default.yellow(
10648
11200
  `
10649
11201
  \u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
10650
11202
  )
10651
11203
  );
10652
- console.log(import_chalk18.default.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
11204
+ console.log(import_chalk21.default.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
10653
11205
  }
10654
11206
  }
10655
11207
  }
10656
- console.log(import_chalk18.default.blue("\n Preview:"));
10657
- console.log(import_chalk18.default.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
11208
+ console.log(import_chalk21.default.blue("\n Preview:"));
11209
+ console.log(import_chalk21.default.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
10658
11210
  if (updated.codegenProvider) {
10659
11211
  console.log(
10660
- import_chalk18.default.gray(
11212
+ import_chalk21.default.gray(
10661
11213
  ` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
10662
11214
  )
10663
11215
  );
10664
11216
  }
10665
11217
  const ok = await (0, import_prompts3.confirm)({ message: "Save to .ai-spec.json?", default: true });
10666
11218
  if (!ok) {
10667
- console.log(import_chalk18.default.gray(" Cancelled."));
11219
+ console.log(import_chalk21.default.gray(" Cancelled."));
10668
11220
  return;
10669
11221
  }
10670
- await fs23.writeJson(configPath, updated, { spaces: 2 });
10671
- console.log(import_chalk18.default.green(`
11222
+ await fs24.writeJson(configPath, updated, { spaces: 2 });
11223
+ console.log(import_chalk21.default.green(`
10672
11224
  \u2714 Saved to ${configPath}`));
10673
11225
  const providerToCheck = updated.provider ?? "gemini";
10674
11226
  const envKey = ENV_KEY_MAP[providerToCheck];
10675
11227
  if (envKey && !process.env[envKey]) {
10676
11228
  console.log(
10677
- import_chalk18.default.yellow(
11229
+ import_chalk21.default.yellow(
10678
11230
  ` \u26A0 Remember to set ${envKey} in your environment or .env file.`
10679
11231
  )
10680
11232
  );
@@ -10693,29 +11245,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10693
11245
  cliOpts,
10694
11246
  contractContextSection
10695
11247
  } = opts;
10696
- console.log(import_chalk18.default.blue(`
11248
+ console.log(import_chalk21.default.blue(`
10697
11249
  [${repoName}] Loading project context...`));
10698
11250
  const loader = new ContextLoader(repoAbsPath);
10699
11251
  let context = await loader.loadProjectContext();
10700
11252
  const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
10701
- console.log(import_chalk18.default.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10702
- console.log(import_chalk18.default.gray(` Dependencies: ${context.dependencies.length} packages`));
11253
+ console.log(import_chalk21.default.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
11254
+ console.log(import_chalk21.default.gray(` Dependencies: ${context.dependencies.length} packages`));
10703
11255
  if (context.constitution && context.constitution.length > 6e3) {
10704
- console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
11256
+ console.log(import_chalk21.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
10705
11257
  }
10706
11258
  if (!context.constitution) {
10707
- console.log(import_chalk18.default.yellow(` Constitution: not found \u2014 auto-generating...`));
11259
+ console.log(import_chalk21.default.yellow(` Constitution: not found \u2014 auto-generating...`));
10708
11260
  try {
10709
11261
  const constitutionGen = new ConstitutionGenerator(specProvider);
10710
11262
  const constitutionContent = await constitutionGen.generate(repoAbsPath);
10711
11263
  await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
10712
11264
  context.constitution = constitutionContent;
10713
- console.log(import_chalk18.default.green(` Constitution: generated`));
11265
+ console.log(import_chalk21.default.green(` Constitution: generated`));
10714
11266
  } catch (err) {
10715
- console.log(import_chalk18.default.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
11267
+ console.log(import_chalk21.default.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
10716
11268
  }
10717
11269
  } else {
10718
- console.log(import_chalk18.default.green(` Constitution: found`));
11270
+ console.log(import_chalk21.default.green(` Constitution: found`));
10719
11271
  }
10720
11272
  let fullIdea = idea;
10721
11273
  if (contractContextSection) {
@@ -10723,58 +11275,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10723
11275
 
10724
11276
  ${contractContextSection}`;
10725
11277
  }
10726
- console.log(import_chalk18.default.blue(` [${repoName}] Generating spec...`));
11278
+ console.log(import_chalk21.default.blue(` [${repoName}] Generating spec...`));
10727
11279
  let finalSpec;
10728
11280
  try {
10729
11281
  const result = await generateSpecWithTasks(specProvider, fullIdea, context);
10730
11282
  finalSpec = result.spec;
10731
- console.log(import_chalk18.default.green(` Spec generated.`));
11283
+ console.log(import_chalk21.default.green(` Spec generated.`));
10732
11284
  } catch (err) {
10733
- console.error(import_chalk18.default.red(` Spec generation failed: ${err.message}`));
11285
+ console.error(import_chalk21.default.red(` Spec generation failed: ${err.message}`));
10734
11286
  return { dsl: null, specFile: null };
10735
11287
  }
10736
11288
  let extractedDsl = null;
10737
11289
  if (!cliOpts.skipDsl) {
10738
- console.log(import_chalk18.default.blue(` [${repoName}] Extracting DSL...`));
11290
+ console.log(import_chalk21.default.blue(` [${repoName}] Extracting DSL...`));
10739
11291
  try {
10740
11292
  const dslExtractor = new DslExtractor(specProvider);
10741
11293
  const repoIsFrontend = isFrontendDeps(context.dependencies);
10742
11294
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
10743
11295
  if (extractedDsl) {
10744
- console.log(import_chalk18.default.green(` DSL extracted.`));
11296
+ console.log(import_chalk21.default.green(` DSL extracted.`));
10745
11297
  }
10746
11298
  } catch (err) {
10747
- console.log(import_chalk18.default.yellow(` DSL extraction failed: ${err.message}`));
11299
+ console.log(import_chalk21.default.yellow(` DSL extraction failed: ${err.message}`));
10748
11300
  }
10749
11301
  }
10750
11302
  const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
10751
11303
  const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
10752
11304
  let workingDir = repoAbsPath;
10753
11305
  if (!skipWorktreeForRepo) {
10754
- console.log(import_chalk18.default.blue(` [${repoName}] Setting up git worktree...`));
11306
+ console.log(import_chalk21.default.blue(` [${repoName}] Setting up git worktree...`));
10755
11307
  try {
10756
11308
  const worktreeManager = new GitWorktreeManager(repoAbsPath);
10757
11309
  const worktreePath = await worktreeManager.createWorktree(idea);
10758
11310
  if (worktreePath) workingDir = worktreePath;
10759
11311
  } catch (err) {
10760
- console.log(import_chalk18.default.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
11312
+ console.log(import_chalk21.default.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
10761
11313
  }
10762
11314
  } else {
10763
- console.log(import_chalk18.default.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
11315
+ console.log(import_chalk21.default.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
10764
11316
  }
10765
- const specsDir = path22.join(workingDir, "specs");
10766
- await fs23.ensureDir(specsDir);
11317
+ const specsDir = path23.join(workingDir, "specs");
11318
+ await fs24.ensureDir(specsDir);
10767
11319
  const featureSlug = slugify(idea);
10768
11320
  const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
10769
- await fs23.writeFile(specFile, finalSpec, "utf-8");
10770
- console.log(import_chalk18.default.green(` Spec saved: ${path22.relative(repoAbsPath, specFile)}`));
11321
+ await fs24.writeFile(specFile, finalSpec, "utf-8");
11322
+ console.log(import_chalk21.default.green(` Spec saved: ${path23.relative(repoAbsPath, specFile)}`));
10771
11323
  let savedDslFile = null;
10772
11324
  if (extractedDsl) {
10773
11325
  const dslExtractorForSave = new DslExtractor(specProvider);
10774
11326
  savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
10775
- console.log(import_chalk18.default.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
11327
+ console.log(import_chalk21.default.green(` DSL saved: ${path23.relative(repoAbsPath, savedDslFile)}`));
10776
11328
  }
10777
- console.log(import_chalk18.default.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
11329
+ console.log(import_chalk21.default.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
10778
11330
  try {
10779
11331
  const codegen = new CodeGenerator(codegenProvider, codegenMode);
10780
11332
  await codegen.generateCode(specFile, workingDir, context, {
@@ -10782,29 +11334,29 @@ ${contractContextSection}`;
10782
11334
  dslFilePath: savedDslFile ?? void 0,
10783
11335
  repoType: detectedRepoType
10784
11336
  });
10785
- console.log(import_chalk18.default.green(` Code generation complete.`));
11337
+ console.log(import_chalk21.default.green(` Code generation complete.`));
10786
11338
  } catch (err) {
10787
- console.log(import_chalk18.default.yellow(` Code generation failed: ${err.message}`));
11339
+ console.log(import_chalk21.default.yellow(` Code generation failed: ${err.message}`));
10788
11340
  }
10789
11341
  if (!cliOpts.skipTests && extractedDsl) {
10790
- console.log(import_chalk18.default.blue(` [${repoName}] Generating test skeletons...`));
11342
+ console.log(import_chalk21.default.blue(` [${repoName}] Generating test skeletons...`));
10791
11343
  try {
10792
11344
  const testGen = new TestGenerator(codegenProvider);
10793
11345
  const testFiles = await testGen.generate(extractedDsl, workingDir);
10794
- console.log(import_chalk18.default.green(` ${testFiles.length} test file(s) generated.`));
11346
+ console.log(import_chalk21.default.green(` ${testFiles.length} test file(s) generated.`));
10795
11347
  } catch (err) {
10796
- console.log(import_chalk18.default.yellow(` Test generation failed: ${err.message}`));
11348
+ console.log(import_chalk21.default.yellow(` Test generation failed: ${err.message}`));
10797
11349
  }
10798
11350
  }
10799
11351
  if (!cliOpts.skipErrorFeedback) {
10800
11352
  try {
10801
11353
  await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
10802
11354
  } catch (err) {
10803
- console.log(import_chalk18.default.yellow(` Error feedback failed: ${err.message}`));
11355
+ console.log(import_chalk21.default.yellow(` Error feedback failed: ${err.message}`));
10804
11356
  }
10805
11357
  }
10806
11358
  if (!cliOpts.skipReview) {
10807
- console.log(import_chalk18.default.blue(` [${repoName}] Running code review...`));
11359
+ console.log(import_chalk21.default.blue(` [${repoName}] Running code review...`));
10808
11360
  try {
10809
11361
  const reviewer = new CodeReviewer(specProvider);
10810
11362
  const originalDir = process.cwd();
@@ -10816,9 +11368,9 @@ ${contractContextSection}`;
10816
11368
  process.chdir(originalDir);
10817
11369
  }
10818
11370
  await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
10819
- console.log(import_chalk18.default.green(` Code review complete.`));
11371
+ console.log(import_chalk21.default.green(` Code review complete.`));
10820
11372
  } catch (err) {
10821
- console.log(import_chalk18.default.yellow(` Code review failed: ${err.message}`));
11373
+ console.log(import_chalk21.default.yellow(` Code review failed: ${err.message}`));
10822
11374
  }
10823
11375
  }
10824
11376
  return { dsl: extractedDsl, specFile };
@@ -10841,7 +11393,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10841
11393
  codegenModel: codegenModelName
10842
11394
  });
10843
11395
  const workspaceLoader = new WorkspaceLoader(currentDir);
10844
- console.log(import_chalk18.default.blue("\n[W1] Loading per-repo contexts..."));
11396
+ console.log(import_chalk21.default.blue("\n[W1] Loading per-repo contexts..."));
10845
11397
  const contexts = /* @__PURE__ */ new Map();
10846
11398
  const frontendContexts = /* @__PURE__ */ new Map();
10847
11399
  for (const repo of workspace.repos) {
@@ -10853,27 +11405,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10853
11405
  if (repo.role === "frontend" || repo.role === "mobile") {
10854
11406
  const fctx = await loadFrontendContext(repoAbsPath);
10855
11407
  frontendContexts.set(repo.name, fctx);
10856
- console.log(import_chalk18.default.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
11408
+ console.log(import_chalk21.default.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
10857
11409
  } else {
10858
- console.log(import_chalk18.default.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
11410
+ console.log(import_chalk21.default.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
10859
11411
  }
10860
11412
  } catch (err) {
10861
- console.log(import_chalk18.default.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
11413
+ console.log(import_chalk21.default.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
10862
11414
  }
10863
11415
  }
10864
- console.log(import_chalk18.default.blue("\n[W2] Decomposing requirement across repos..."));
11416
+ console.log(import_chalk21.default.blue("\n[W2] Decomposing requirement across repos..."));
10865
11417
  const decomposer = new RequirementDecomposer(specProvider);
10866
11418
  let decomposition;
10867
11419
  try {
10868
11420
  decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
10869
- console.log(import_chalk18.default.green(` Summary: ${decomposition.summary}`));
10870
- console.log(import_chalk18.default.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
11421
+ console.log(import_chalk21.default.green(` Summary: ${decomposition.summary}`));
11422
+ console.log(import_chalk21.default.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
10871
11423
  if (decomposition.coordinationNotes) {
10872
- console.log(import_chalk18.default.gray(` Coordination: ${decomposition.coordinationNotes}`));
11424
+ console.log(import_chalk21.default.gray(` Coordination: ${decomposition.coordinationNotes}`));
10873
11425
  }
10874
11426
  } catch (err) {
10875
- console.error(import_chalk18.default.red(` Decomposition failed: ${err.message}`));
10876
- console.log(import_chalk18.default.yellow(" Falling back to running all repos independently."));
11427
+ console.error(import_chalk21.default.red(` Decomposition failed: ${err.message}`));
11428
+ console.log(import_chalk21.default.yellow(" Falling back to running all repos independently."));
10877
11429
  decomposition = {
10878
11430
  originalRequirement: idea,
10879
11431
  summary: idea,
@@ -10889,11 +11441,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10889
11441
  };
10890
11442
  }
10891
11443
  if (!opts.auto) {
10892
- console.log(import_chalk18.default.cyan("\n[W3] Decomposition Preview:"));
10893
- console.log(import_chalk18.default.cyan("\u2500".repeat(52)));
11444
+ console.log(import_chalk21.default.cyan("\n[W3] Decomposition Preview:"));
11445
+ console.log(import_chalk21.default.cyan("\u2500".repeat(52)));
10894
11446
  for (const r of decomposition.repos) {
10895
- console.log(import_chalk18.default.bold(` ${r.repoName} (${r.role})`));
10896
- console.log(import_chalk18.default.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
11447
+ console.log(import_chalk21.default.bold(` ${r.repoName} (${r.role})`));
11448
+ console.log(import_chalk21.default.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
10897
11449
  if (r.uxDecisions) {
10898
11450
  const ux = r.uxDecisions;
10899
11451
  const uxSummary = [
@@ -10902,13 +11454,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10902
11454
  ux.optimisticUpdate ? "optimistic-update" : "",
10903
11455
  ux.errorRollback ? "rollback" : ""
10904
11456
  ].filter(Boolean).join(", ");
10905
- if (uxSummary) console.log(import_chalk18.default.cyan(` UX: ${uxSummary}`));
11457
+ if (uxSummary) console.log(import_chalk21.default.cyan(` UX: ${uxSummary}`));
10906
11458
  }
10907
11459
  if (r.dependsOnRepos.length > 0) {
10908
- console.log(import_chalk18.default.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
11460
+ console.log(import_chalk21.default.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
10909
11461
  }
10910
11462
  }
10911
- console.log(import_chalk18.default.cyan("\u2500".repeat(52)));
11463
+ console.log(import_chalk21.default.cyan("\u2500".repeat(52)));
10912
11464
  const gate = await (0, import_prompts3.select)({
10913
11465
  message: "Proceed with multi-repo pipeline?",
10914
11466
  choices: [
@@ -10917,24 +11469,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10917
11469
  ]
10918
11470
  });
10919
11471
  if (gate === "abort") {
10920
- console.log(import_chalk18.default.yellow(" Aborted."));
11472
+ console.log(import_chalk21.default.yellow(" Aborted."));
10921
11473
  process.exit(0);
10922
11474
  }
10923
11475
  }
10924
11476
  const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
10925
11477
  const contractDsls = /* @__PURE__ */ new Map();
10926
- console.log(import_chalk18.default.blue(`
11478
+ console.log(import_chalk21.default.blue(`
10927
11479
  [W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
10928
11480
  const results = [];
10929
11481
  for (const repoReq of sortedRepoRequirements) {
10930
11482
  const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
10931
11483
  if (!repoConfig) {
10932
- console.log(import_chalk18.default.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
11484
+ console.log(import_chalk21.default.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
10933
11485
  results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
10934
11486
  continue;
10935
11487
  }
10936
11488
  const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
10937
- console.log(import_chalk18.default.bold.blue(`
11489
+ console.log(import_chalk21.default.bold.blue(`
10938
11490
  \u2500\u2500 ${repoReq.repoName} (${repoReq.role}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
10939
11491
  let contractContextSection;
10940
11492
  if (repoReq.dependsOnRepos.length > 0) {
@@ -10942,7 +11494,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10942
11494
  for (const depName of repoReq.dependsOnRepos) {
10943
11495
  const depDsl = contractDsls.get(depName);
10944
11496
  if (depDsl) {
10945
- console.log(import_chalk18.default.gray(` Using API contract from: ${depName}`));
11497
+ console.log(import_chalk21.default.gray(` Using API contract from: ${depName}`));
10946
11498
  const contract = buildFrontendApiContract(depDsl);
10947
11499
  contractParts.push(buildContractContextSection(contract));
10948
11500
  }
@@ -10962,7 +11514,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10962
11514
  frontendContext: frontendCtx
10963
11515
  });
10964
11516
  contractContextSection = void 0;
10965
- console.log(import_chalk18.default.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
11517
+ console.log(import_chalk21.default.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
10966
11518
  }
10967
11519
  try {
10968
11520
  const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
@@ -10979,22 +11531,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10979
11531
  });
10980
11532
  if (repoReq.isContractProvider && dsl) {
10981
11533
  contractDsls.set(repoReq.repoName, dsl);
10982
- console.log(import_chalk18.default.green(` Contract stored for downstream repos.`));
11534
+ console.log(import_chalk21.default.green(` Contract stored for downstream repos.`));
10983
11535
  }
10984
11536
  results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
10985
- console.log(import_chalk18.default.green(` \u2714 ${repoReq.repoName} complete`));
11537
+ console.log(import_chalk21.default.green(` \u2714 ${repoReq.repoName} complete`));
10986
11538
  } catch (err) {
10987
- console.error(import_chalk18.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
11539
+ console.error(import_chalk21.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
10988
11540
  results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
10989
11541
  }
10990
11542
  }
10991
- console.log(import_chalk18.default.bold.green("\n\u2714 Multi-repo pipeline complete!"));
10992
- console.log(import_chalk18.default.gray(` Workspace: ${workspace.name}`));
10993
- console.log(import_chalk18.default.gray(` Requirement: ${idea}`));
11543
+ console.log(import_chalk21.default.bold.green("\n\u2714 Multi-repo pipeline complete!"));
11544
+ console.log(import_chalk21.default.gray(` Workspace: ${workspace.name}`));
11545
+ console.log(import_chalk21.default.gray(` Requirement: ${idea}`));
10994
11546
  console.log();
10995
11547
  for (const r of results) {
10996
- const icon = r.status === "success" ? import_chalk18.default.green("\u2714") : r.status === "failed" ? import_chalk18.default.red("\u2718") : import_chalk18.default.gray("\u2212");
10997
- const specInfo = r.specFile ? import_chalk18.default.gray(` \u2192 ${r.specFile}`) : "";
11548
+ const icon = r.status === "success" ? import_chalk21.default.green("\u2714") : r.status === "failed" ? import_chalk21.default.red("\u2718") : import_chalk21.default.gray("\u2212");
11549
+ const specInfo = r.specFile ? import_chalk21.default.gray(` \u2192 ${r.specFile}`) : "";
10998
11550
  console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
10999
11551
  }
11000
11552
  return results;
@@ -11002,18 +11554,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11002
11554
  var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
11003
11555
  workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
11004
11556
  const currentDir = process.cwd();
11005
- const configPath = path22.join(currentDir, WORKSPACE_CONFIG_FILE);
11006
- if (await fs23.pathExists(configPath)) {
11557
+ const configPath = path23.join(currentDir, WORKSPACE_CONFIG_FILE);
11558
+ if (await fs24.pathExists(configPath)) {
11007
11559
  const overwrite = await (0, import_prompts3.confirm)({
11008
11560
  message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
11009
11561
  default: false
11010
11562
  });
11011
11563
  if (!overwrite) {
11012
- console.log(import_chalk18.default.gray(" Cancelled."));
11564
+ console.log(import_chalk21.default.gray(" Cancelled."));
11013
11565
  return;
11014
11566
  }
11015
11567
  }
11016
- console.log(import_chalk18.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"));
11568
+ console.log(import_chalk21.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"));
11017
11569
  const workspaceName = await (0, import_prompts3.input)({
11018
11570
  message: "Workspace name:",
11019
11571
  validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
@@ -11027,11 +11579,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11027
11579
  const workspaceLoader = new WorkspaceLoader(currentDir);
11028
11580
  const detected = await workspaceLoader.autoDetect();
11029
11581
  if (detected.length === 0) {
11030
- console.log(import_chalk18.default.yellow(" No recognizable repos found in sibling directories."));
11582
+ console.log(import_chalk21.default.yellow(" No recognizable repos found in sibling directories."));
11031
11583
  } else {
11032
- console.log(import_chalk18.default.cyan("\n Detected repos:"));
11584
+ console.log(import_chalk21.default.cyan("\n Detected repos:"));
11033
11585
  for (const r of detected) {
11034
- console.log(import_chalk18.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11586
+ console.log(import_chalk21.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11035
11587
  }
11036
11588
  const keepAll = await (0, import_prompts3.confirm)({
11037
11589
  message: `Include all ${detected.length} detected repo(s)?`,
@@ -11048,7 +11600,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11048
11600
  if (keep) repos.push(r);
11049
11601
  }
11050
11602
  }
11051
- console.log(import_chalk18.default.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
11603
+ console.log(import_chalk21.default.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
11052
11604
  }
11053
11605
  }
11054
11606
  const repoTypeChoices = [
@@ -11070,7 +11622,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11070
11622
  default: repos.length === 0
11071
11623
  });
11072
11624
  while (addMore) {
11073
- console.log(import_chalk18.default.cyan(`
11625
+ console.log(import_chalk21.default.cyan(`
11074
11626
  Adding repo #${repos.length + 1}`));
11075
11627
  const repoName = await (0, import_prompts3.input)({
11076
11628
  message: "Repo name (e.g. api, web, app):",
@@ -11084,16 +11636,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11084
11636
  message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
11085
11637
  default: `./${repoName}`
11086
11638
  });
11087
- const absPath = path22.resolve(currentDir, repoPath);
11639
+ const absPath = path23.resolve(currentDir, repoPath);
11088
11640
  let detectedType = "unknown";
11089
11641
  let detectedRole = "shared";
11090
- if (await fs23.pathExists(absPath)) {
11642
+ if (await fs24.pathExists(absPath)) {
11091
11643
  const { type, role } = await detectRepoType(absPath);
11092
11644
  detectedType = type;
11093
11645
  detectedRole = role;
11094
- console.log(import_chalk18.default.gray(` Auto-detected: type=${type}, role=${role}`));
11646
+ console.log(import_chalk21.default.gray(` Auto-detected: type=${type}, role=${role}`));
11095
11647
  } else {
11096
- console.log(import_chalk18.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
11648
+ console.log(import_chalk21.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
11097
11649
  }
11098
11650
  const repoType = await (0, import_prompts3.select)({
11099
11651
  message: `Repo type for "${repoName}":`,
@@ -11116,53 +11668,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11116
11668
  type: repoType,
11117
11669
  role: repoRole
11118
11670
  });
11119
- console.log(import_chalk18.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
11671
+ console.log(import_chalk21.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
11120
11672
  addMore = await (0, import_prompts3.confirm)({
11121
11673
  message: "Add another repo?",
11122
11674
  default: false
11123
11675
  });
11124
11676
  }
11125
11677
  const workspaceConfig = { name: workspaceName, repos };
11126
- console.log(import_chalk18.default.cyan("\n Workspace summary:"));
11127
- console.log(import_chalk18.default.gray(` Name: ${workspaceName}`));
11678
+ console.log(import_chalk21.default.cyan("\n Workspace summary:"));
11679
+ console.log(import_chalk21.default.gray(` Name: ${workspaceName}`));
11128
11680
  for (const r of repos) {
11129
- console.log(import_chalk18.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11681
+ console.log(import_chalk21.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11130
11682
  }
11131
11683
  const ok = await (0, import_prompts3.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
11132
11684
  if (!ok) {
11133
- console.log(import_chalk18.default.gray(" Cancelled."));
11685
+ console.log(import_chalk21.default.gray(" Cancelled."));
11134
11686
  return;
11135
11687
  }
11136
11688
  const loader = new WorkspaceLoader(currentDir);
11137
11689
  const saved = await loader.save(workspaceConfig);
11138
- console.log(import_chalk18.default.green(`
11690
+ console.log(import_chalk21.default.green(`
11139
11691
  \u2714 Workspace saved: ${saved}`));
11140
- console.log(import_chalk18.default.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
11692
+ console.log(import_chalk21.default.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
11141
11693
  });
11142
11694
  workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
11143
11695
  const currentDir = process.cwd();
11144
11696
  const loader = new WorkspaceLoader(currentDir);
11145
11697
  const config2 = await loader.load();
11146
11698
  if (!config2) {
11147
- console.log(import_chalk18.default.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
11148
- console.log(import_chalk18.default.gray(" Run `ai-spec workspace init` to create one."));
11699
+ console.log(import_chalk21.default.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
11700
+ console.log(import_chalk21.default.gray(" Run `ai-spec workspace init` to create one."));
11149
11701
  return;
11150
11702
  }
11151
- console.log(import_chalk18.default.bold(`
11703
+ console.log(import_chalk21.default.bold(`
11152
11704
  Workspace: ${config2.name}`));
11153
- console.log(import_chalk18.default.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
11154
- console.log(import_chalk18.default.gray(` Repos (${config2.repos.length}):
11705
+ console.log(import_chalk21.default.gray(` Config: ${path23.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
11706
+ console.log(import_chalk21.default.gray(` Repos (${config2.repos.length}):
11155
11707
  `));
11156
11708
  for (const repo of config2.repos) {
11157
11709
  const absPath = loader.resolveAbsPath(repo);
11158
- const exists = await fs23.pathExists(absPath);
11159
- const status = exists ? import_chalk18.default.green("found") : import_chalk18.default.red("not found");
11710
+ const exists = await fs24.pathExists(absPath);
11711
+ const status = exists ? import_chalk21.default.green("found") : import_chalk21.default.red("not found");
11160
11712
  console.log(
11161
- ` ${import_chalk18.default.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
11713
+ ` ${import_chalk21.default.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
11162
11714
  );
11163
- console.log(import_chalk18.default.gray(` path: ${absPath}`));
11715
+ console.log(import_chalk21.default.gray(` path: ${absPath}`));
11164
11716
  if (repo.constitution) {
11165
- console.log(import_chalk18.default.green(` constitution: found`));
11717
+ console.log(import_chalk21.default.green(` constitution: found`));
11166
11718
  }
11167
11719
  }
11168
11720
  });
@@ -11179,30 +11731,30 @@ program.command("update").description("Update an existing spec with a change req
11179
11731
  const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
11180
11732
  const apiKey = await resolveApiKey(providerName, opts.key);
11181
11733
  const provider = createProvider(providerName, apiKey, modelName);
11182
- console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11183
- console.log(import_chalk18.default.gray(` Provider: ${providerName}/${modelName}`));
11734
+ console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11735
+ console.log(import_chalk21.default.gray(` Provider: ${providerName}/${modelName}`));
11184
11736
  const updateRunId = generateRunId();
11185
11737
  const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
11186
11738
  setActiveSnapshot(updateSnapshot);
11187
11739
  const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
11188
11740
  setActiveLogger(updateLogger);
11189
- console.log(import_chalk18.default.gray(` Run ID: ${updateRunId}`));
11741
+ console.log(import_chalk21.default.gray(` Run ID: ${updateRunId}`));
11190
11742
  let specPath = opts.spec ?? null;
11191
11743
  if (!specPath) {
11192
- const specsDir = path22.join(currentDir, "specs");
11744
+ const specsDir = path23.join(currentDir, "specs");
11193
11745
  const latest = await SpecUpdater.findLatestSpec(specsDir);
11194
11746
  if (!latest) {
11195
- console.error(import_chalk18.default.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
11747
+ console.error(import_chalk21.default.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
11196
11748
  process.exit(1);
11197
11749
  }
11198
11750
  specPath = latest.filePath;
11199
- console.log(import_chalk18.default.gray(` Using spec: ${path22.relative(currentDir, specPath)} (v${latest.version})`));
11751
+ console.log(import_chalk21.default.gray(` Using spec: ${path23.relative(currentDir, specPath)} (v${latest.version})`));
11200
11752
  }
11201
- console.log(import_chalk18.default.gray(" Loading project context..."));
11753
+ console.log(import_chalk21.default.gray(" Loading project context..."));
11202
11754
  const loader = new ContextLoader(currentDir);
11203
11755
  const context = await loader.loadProjectContext();
11204
11756
  if (context.constitution && context.constitution.length > 6e3) {
11205
- console.log(import_chalk18.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
11757
+ console.log(import_chalk21.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
11206
11758
  }
11207
11759
  const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
11208
11760
  const { type: repoType } = await _detectRepoType(currentDir);
@@ -11214,19 +11766,19 @@ program.command("update").description("Update an existing spec with a change req
11214
11766
  repoType
11215
11767
  });
11216
11768
  } catch (err) {
11217
- console.error(import_chalk18.default.red(` Update failed: ${err.message}`));
11769
+ console.error(import_chalk21.default.red(` Update failed: ${err.message}`));
11218
11770
  process.exit(1);
11219
11771
  }
11220
- console.log(import_chalk18.default.green(`
11221
- \u2714 Spec updated \u2192 v${result.newVersion}: ${path22.relative(currentDir, result.newSpecPath)}`));
11772
+ console.log(import_chalk21.default.green(`
11773
+ \u2714 Spec updated \u2192 v${result.newVersion}: ${path23.relative(currentDir, result.newSpecPath)}`));
11222
11774
  if (result.newDslPath) {
11223
- console.log(import_chalk18.default.green(` \u2714 DSL updated: ${path22.relative(currentDir, result.newDslPath)}`));
11775
+ console.log(import_chalk21.default.green(` \u2714 DSL updated: ${path23.relative(currentDir, result.newDslPath)}`));
11224
11776
  }
11225
11777
  if (result.affectedFiles.length > 0) {
11226
- console.log(import_chalk18.default.cyan("\n Affected files:"));
11778
+ console.log(import_chalk21.default.cyan("\n Affected files:"));
11227
11779
  for (const f of result.affectedFiles) {
11228
- const icon = f.action === "create" ? import_chalk18.default.green("+") : import_chalk18.default.yellow("~");
11229
- console.log(` ${icon} ${f.file}: ${import_chalk18.default.gray(f.description)}`);
11780
+ const icon = f.action === "create" ? import_chalk21.default.green("+") : import_chalk21.default.yellow("~");
11781
+ console.log(` ${icon} ${f.file}: ${import_chalk21.default.gray(f.description)}`);
11230
11782
  }
11231
11783
  }
11232
11784
  if (opts.codegen && result.affectedFiles.length > 0) {
@@ -11234,9 +11786,9 @@ program.command("update").description("Update an existing spec with a change req
11234
11786
  const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
11235
11787
  const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
11236
11788
  const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
11237
- console.log(import_chalk18.default.blue("\n Regenerating affected files..."));
11789
+ console.log(import_chalk21.default.blue("\n Regenerating affected files..."));
11238
11790
  const codeGenerator = new CodeGenerator(codegenProvider, "api");
11239
- const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
11791
+ const specContent = await fs24.readFile(result.newSpecPath, "utf-8");
11240
11792
  const constitutionSection = context.constitution ? `
11241
11793
  === Project Constitution (MUST follow) ===
11242
11794
  ${context.constitution}
@@ -11247,10 +11799,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
11247
11799
  ` : "";
11248
11800
  updateLogger.stageStart("update_codegen");
11249
11801
  for (const affected of result.affectedFiles) {
11250
- const fullPath = path22.join(currentDir, affected.file);
11802
+ const fullPath = path23.join(currentDir, affected.file);
11251
11803
  let existing = "";
11252
11804
  try {
11253
- existing = await fs23.readFile(fullPath, "utf-8");
11805
+ existing = await fs24.readFile(fullPath, "utf-8");
11254
11806
  } catch {
11255
11807
  }
11256
11808
  const codePrompt = `Apply this change to the file.
@@ -11264,23 +11816,23 @@ ${specContent}
11264
11816
  ${constitutionSection}${dslSection}
11265
11817
  === ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
11266
11818
  ${existing || "Create from scratch."}`;
11267
- process.stdout.write(` ${existing ? import_chalk18.default.yellow("~") : import_chalk18.default.green("+")} ${affected.file}... `);
11819
+ process.stdout.write(` ${existing ? import_chalk21.default.yellow("~") : import_chalk21.default.green("+")} ${affected.file}... `);
11268
11820
  try {
11269
11821
  const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
11270
11822
  const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
11271
11823
  const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
11272
- await fs23.ensureDir(path22.dirname(fullPath));
11824
+ await fs24.ensureDir(path23.dirname(fullPath));
11273
11825
  await updateSnapshot.snapshotFile(fullPath);
11274
- await fs23.writeFile(fullPath, content, "utf-8");
11826
+ await fs24.writeFile(fullPath, content, "utf-8");
11275
11827
  updateLogger.fileWritten(affected.file);
11276
- console.log(import_chalk18.default.green("\u2714"));
11828
+ console.log(import_chalk21.default.green("\u2714"));
11277
11829
  } catch (err) {
11278
11830
  updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
11279
- console.log(import_chalk18.default.red(`\u2718 ${err.message}`));
11831
+ console.log(import_chalk21.default.red(`\u2718 ${err.message}`));
11280
11832
  }
11281
11833
  }
11282
11834
  updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
11283
- const updatedSpecContent = await fs23.readFile(result.newSpecPath, "utf-8").catch(() => "");
11835
+ const updatedSpecContent = await fs24.readFile(result.newSpecPath, "utf-8").catch(() => "");
11284
11836
  if (updatedSpecContent) {
11285
11837
  const updateReviewer = new CodeReviewer(provider, currentDir);
11286
11838
  const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
@@ -11292,13 +11844,13 @@ ${existing || "Create from scratch."}`;
11292
11844
  updateLogger.finish();
11293
11845
  updateLogger.printSummary();
11294
11846
  if (updateSnapshot.fileCount > 0) {
11295
- console.log(import_chalk18.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
11847
+ console.log(import_chalk21.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
11296
11848
  }
11297
11849
  if (!opts.codegen && result.affectedFiles.length > 0) {
11298
- console.log(import_chalk18.default.blue("\n Next steps:"));
11299
- console.log(import_chalk18.default.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
11300
- console.log(import_chalk18.default.gray(` \u2022 Or update files manually based on the affected files list above`));
11301
- console.log(import_chalk18.default.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
11850
+ console.log(import_chalk21.default.blue("\n Next steps:"));
11851
+ console.log(import_chalk21.default.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
11852
+ console.log(import_chalk21.default.gray(` \u2022 Or update files manually based on the affected files list above`));
11853
+ console.log(import_chalk21.default.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
11302
11854
  }
11303
11855
  });
11304
11856
  program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (YAML or JSON)").option("--openapi", "Export as OpenAPI 3.1.0 (default behaviour)").option("--format <fmt>", "Output format: yaml | json (default: yaml)", "yaml").option("--output <path>", "Output file path (default: openapi.yaml)").option("--server <url>", "API server URL in the OpenAPI document (default: http://localhost:3000)").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").action(async (opts) => {
@@ -11307,19 +11859,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
11307
11859
  if (!dslPath) {
11308
11860
  dslPath = await findLatestDslFile(currentDir);
11309
11861
  if (!dslPath) {
11310
- console.error(import_chalk18.default.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
11862
+ console.error(import_chalk21.default.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
11311
11863
  process.exit(1);
11312
11864
  }
11313
- console.log(import_chalk18.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
11865
+ console.log(import_chalk21.default.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
11314
11866
  }
11315
11867
  let dsl;
11316
11868
  try {
11317
- dsl = await fs23.readJson(dslPath);
11869
+ dsl = await fs24.readJson(dslPath);
11318
11870
  } catch (err) {
11319
- console.error(import_chalk18.default.red(` Failed to read DSL: ${err.message}`));
11871
+ console.error(import_chalk21.default.red(` Failed to read DSL: ${err.message}`));
11320
11872
  process.exit(1);
11321
11873
  }
11322
- console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11874
+ console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11323
11875
  const format = opts.format === "json" ? "json" : "yaml";
11324
11876
  const serverUrl = opts.server || "http://localhost:3000";
11325
11877
  try {
@@ -11328,31 +11880,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
11328
11880
  serverUrl,
11329
11881
  outputPath: opts.output
11330
11882
  });
11331
- const rel = path22.relative(currentDir, outputPath);
11332
- console.log(import_chalk18.default.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
11333
- console.log(import_chalk18.default.gray(` Feature : ${dsl.feature.title}`));
11334
- console.log(import_chalk18.default.gray(` Endpoints: ${dsl.endpoints.length}`));
11335
- console.log(import_chalk18.default.gray(` Models : ${dsl.models.length}`));
11336
- console.log(import_chalk18.default.gray(` Server : ${serverUrl}`));
11337
- console.log(import_chalk18.default.blue("\n Next steps:"));
11338
- console.log(import_chalk18.default.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
11339
- console.log(import_chalk18.default.gray(` \u2022 Use openapi-generator to generate client SDKs`));
11883
+ const rel = path23.relative(currentDir, outputPath);
11884
+ console.log(import_chalk21.default.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
11885
+ console.log(import_chalk21.default.gray(` Feature : ${dsl.feature.title}`));
11886
+ console.log(import_chalk21.default.gray(` Endpoints: ${dsl.endpoints.length}`));
11887
+ console.log(import_chalk21.default.gray(` Models : ${dsl.models.length}`));
11888
+ console.log(import_chalk21.default.gray(` Server : ${serverUrl}`));
11889
+ console.log(import_chalk21.default.blue("\n Next steps:"));
11890
+ console.log(import_chalk21.default.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
11891
+ console.log(import_chalk21.default.gray(` \u2022 Use openapi-generator to generate client SDKs`));
11340
11892
  } catch (err) {
11341
- console.error(import_chalk18.default.red(` Export failed: ${err.message}`));
11893
+ console.error(import_chalk21.default.red(` Export failed: ${err.message}`));
11342
11894
  process.exit(1);
11343
11895
  }
11344
11896
  });
11345
11897
  program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option("--workspace", "Generate mock assets for all backend repos in the workspace").option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)").option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)").option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)").action(async (opts) => {
11346
11898
  const currentDir = process.cwd();
11347
11899
  const port = parseInt(opts.port, 10) || 3001;
11348
- console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11900
+ console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11349
11901
  if (opts.restore) {
11350
- const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
11902
+ const frontendDir = opts.frontend ? path23.resolve(opts.frontend) : currentDir;
11351
11903
  const r = await restoreMockProxy(frontendDir);
11352
11904
  if (r.restored) {
11353
- console.log(import_chalk18.default.green(" \u2714 Proxy restored and mock server stopped."));
11905
+ console.log(import_chalk21.default.green(" \u2714 Proxy restored and mock server stopped."));
11354
11906
  } else {
11355
- console.log(import_chalk18.default.yellow(` ${r.note ?? "Nothing to restore."}`));
11907
+ console.log(import_chalk21.default.yellow(` ${r.note ?? "Nothing to restore."}`));
11356
11908
  }
11357
11909
  return;
11358
11910
  }
@@ -11360,32 +11912,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11360
11912
  const workspaceLoader = new WorkspaceLoader(currentDir);
11361
11913
  const workspaceConfig = await workspaceLoader.load();
11362
11914
  if (!workspaceConfig) {
11363
- console.error(import_chalk18.default.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
11915
+ console.error(import_chalk21.default.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
11364
11916
  process.exit(1);
11365
11917
  }
11366
11918
  const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
11367
11919
  if (backendRepos.length === 0) {
11368
- console.log(import_chalk18.default.yellow(" No backend repos found in workspace."));
11920
+ console.log(import_chalk21.default.yellow(" No backend repos found in workspace."));
11369
11921
  return;
11370
11922
  }
11371
11923
  for (const repo of backendRepos) {
11372
11924
  const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
11373
- console.log(import_chalk18.default.cyan(`
11925
+ console.log(import_chalk21.default.cyan(`
11374
11926
  Repo: ${repo.name} (${repoAbsPath})`));
11375
11927
  const dslFile = await findLatestDslFile(repoAbsPath);
11376
11928
  if (!dslFile) {
11377
- console.log(import_chalk18.default.yellow(` No DSL file found \u2014 skipping.`));
11929
+ console.log(import_chalk21.default.yellow(` No DSL file found \u2014 skipping.`));
11378
11930
  continue;
11379
11931
  }
11380
- const dsl2 = await fs23.readJson(dslFile);
11932
+ const dsl2 = await fs24.readJson(dslFile);
11381
11933
  const result2 = await generateMockAssets(dsl2, repoAbsPath, {
11382
11934
  port,
11383
11935
  msw: opts.msw,
11384
11936
  proxy: opts.proxy
11385
11937
  });
11386
11938
  for (const f of result2.files) {
11387
- console.log(import_chalk18.default.green(` \u2714 ${f.path}`));
11388
- console.log(import_chalk18.default.gray(` ${f.description}`));
11939
+ console.log(import_chalk21.default.green(` \u2714 ${f.path}`));
11940
+ console.log(import_chalk21.default.gray(` ${f.description}`));
11389
11941
  }
11390
11942
  }
11391
11943
  return;
@@ -11395,19 +11947,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11395
11947
  dslPath = await findLatestDslFile(currentDir);
11396
11948
  if (!dslPath) {
11397
11949
  console.error(
11398
- import_chalk18.default.red(
11950
+ import_chalk21.default.red(
11399
11951
  " No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
11400
11952
  )
11401
11953
  );
11402
11954
  process.exit(1);
11403
11955
  }
11404
- console.log(import_chalk18.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
11956
+ console.log(import_chalk21.default.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
11405
11957
  }
11406
11958
  let dsl;
11407
11959
  try {
11408
- dsl = await fs23.readJson(dslPath);
11960
+ dsl = await fs24.readJson(dslPath);
11409
11961
  } catch (err) {
11410
- console.error(import_chalk18.default.red(` Failed to read DSL file: ${err.message}`));
11962
+ console.error(import_chalk21.default.red(` Failed to read DSL file: ${err.message}`));
11411
11963
  process.exit(1);
11412
11964
  }
11413
11965
  const result = await generateMockAssets(dsl, currentDir, {
@@ -11415,58 +11967,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11415
11967
  msw: opts.msw,
11416
11968
  proxy: opts.proxy
11417
11969
  });
11418
- console.log(import_chalk18.default.green(`
11970
+ console.log(import_chalk21.default.green(`
11419
11971
  \u2714 Mock assets generated (${result.files.length} file(s)):`));
11420
11972
  for (const f of result.files) {
11421
- console.log(import_chalk18.default.green(` ${f.path}`));
11422
- console.log(import_chalk18.default.gray(` ${f.description}`));
11973
+ console.log(import_chalk21.default.green(` ${f.path}`));
11974
+ console.log(import_chalk21.default.gray(` ${f.description}`));
11423
11975
  }
11424
11976
  if (opts.serve) {
11425
- const serverJsPath = path22.join(currentDir, "mock", "server.js");
11426
- if (!await fs23.pathExists(serverJsPath)) {
11427
- console.error(import_chalk18.default.red(" mock/server.js not found \u2014 generation may have failed."));
11977
+ const serverJsPath = path23.join(currentDir, "mock", "server.js");
11978
+ if (!await fs24.pathExists(serverJsPath)) {
11979
+ console.error(import_chalk21.default.red(" mock/server.js not found \u2014 generation may have failed."));
11428
11980
  process.exit(1);
11429
11981
  }
11430
11982
  const pid = startMockServerBackground(serverJsPath, port);
11431
- console.log(import_chalk18.default.green(`
11983
+ console.log(import_chalk21.default.green(`
11432
11984
  \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
11433
11985
  if (opts.frontend) {
11434
- const frontendDir = path22.resolve(opts.frontend);
11986
+ const frontendDir = path23.resolve(opts.frontend);
11435
11987
  const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
11436
11988
  await saveMockServerPid(frontendDir, pid);
11437
11989
  if (proxyResult.applied) {
11438
- console.log(import_chalk18.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11439
- console.log(import_chalk18.default.bold.cyan(`
11990
+ console.log(import_chalk21.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11991
+ console.log(import_chalk21.default.bold.cyan(`
11440
11992
  Ready! Open a new terminal and run:`));
11441
- console.log(import_chalk18.default.white(` cd ${frontendDir}`));
11442
- console.log(import_chalk18.default.white(` ${proxyResult.devCommand}`));
11443
- console.log(import_chalk18.default.gray(`
11993
+ console.log(import_chalk21.default.white(` cd ${frontendDir}`));
11994
+ console.log(import_chalk21.default.white(` ${proxyResult.devCommand}`));
11995
+ console.log(import_chalk21.default.gray(`
11444
11996
  When done: ai-spec mock --restore --frontend ${frontendDir}`));
11445
11997
  } else {
11446
- console.log(import_chalk18.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11447
- if (proxyResult.note) console.log(import_chalk18.default.gray(` ${proxyResult.note}`));
11998
+ console.log(import_chalk21.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11999
+ if (proxyResult.note) console.log(import_chalk21.default.gray(` ${proxyResult.note}`));
11448
12000
  }
11449
12001
  } else {
11450
- console.log(import_chalk18.default.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
11451
- console.log(import_chalk18.default.gray(` Mock server: http://localhost:${port}`));
12002
+ console.log(import_chalk21.default.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
12003
+ console.log(import_chalk21.default.gray(` Mock server: http://localhost:${port}`));
11452
12004
  }
11453
12005
  return;
11454
12006
  }
11455
- console.log(import_chalk18.default.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
11456
- console.log(import_chalk18.default.white(` 1. Install express (if not already):`));
11457
- console.log(import_chalk18.default.gray(` npm install --save-dev express`));
11458
- console.log(import_chalk18.default.white(` 2. Start mock server:`));
11459
- console.log(import_chalk18.default.gray(` node mock/server.js`));
11460
- console.log(import_chalk18.default.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
11461
- console.log(import_chalk18.default.white(` 3. Configure your frontend to proxy API calls to:`));
11462
- console.log(import_chalk18.default.gray(` http://localhost:${port}`));
12007
+ console.log(import_chalk21.default.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12008
+ console.log(import_chalk21.default.white(` 1. Install express (if not already):`));
12009
+ console.log(import_chalk21.default.gray(` npm install --save-dev express`));
12010
+ console.log(import_chalk21.default.white(` 2. Start mock server:`));
12011
+ console.log(import_chalk21.default.gray(` node mock/server.js`));
12012
+ console.log(import_chalk21.default.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
12013
+ console.log(import_chalk21.default.white(` 3. Configure your frontend to proxy API calls to:`));
12014
+ console.log(import_chalk21.default.gray(` http://localhost:${port}`));
11463
12015
  if (opts.proxy) {
11464
- console.log(import_chalk18.default.gray(` (See the generated proxy config file for framework-specific instructions)`));
12016
+ console.log(import_chalk21.default.gray(` (See the generated proxy config file for framework-specific instructions)`));
11465
12017
  }
11466
12018
  if (opts.msw) {
11467
- console.log(import_chalk18.default.white(` 4. MSW: import and start the worker in your app entry:`));
11468
- console.log(import_chalk18.default.gray(` import { worker } from './mocks/browser';`));
11469
- console.log(import_chalk18.default.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
12019
+ console.log(import_chalk21.default.white(` 4. MSW: import and start the worker in your app entry:`));
12020
+ console.log(import_chalk21.default.gray(` import { worker } from './mocks/browser';`));
12021
+ console.log(import_chalk21.default.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
11470
12022
  }
11471
12023
  });
11472
12024
  program.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) => {
@@ -11482,26 +12034,118 @@ program.command("learn").description("Append a lesson or engineering decision di
11482
12034
  }
11483
12035
  const result = await appendDirectLesson(currentDir, lesson.trim());
11484
12036
  if (result.appended) {
11485
- console.log(import_chalk18.default.green(`
12037
+ console.log(import_chalk21.default.green(`
11486
12038
  \u2714 Lesson appended to constitution \xA79`));
11487
- console.log(import_chalk18.default.gray(` File: .ai-spec-constitution.md`));
12039
+ console.log(import_chalk21.default.gray(` File: .ai-spec-constitution.md`));
11488
12040
  } else {
11489
- console.log(import_chalk18.default.yellow(`
12041
+ console.log(import_chalk21.default.yellow(`
11490
12042
  \u26A0 Not appended: ${result.reason}`));
11491
12043
  }
11492
12044
  });
11493
12045
  program.command("restore").description("Restore files modified by a previous run").argument("<runId>", "Run ID shown at the end of a create / generate run").action(async (runId) => {
11494
12046
  const currentDir = process.cwd();
11495
12047
  const snapshot = new RunSnapshot(currentDir, runId);
11496
- console.log(import_chalk18.default.blue(`Restoring run: ${runId}...`));
12048
+ console.log(import_chalk21.default.blue(`Restoring run: ${runId}...`));
11497
12049
  const restored = await snapshot.restore();
11498
12050
  if (restored.length === 0) {
11499
- console.log(import_chalk18.default.yellow(" No backup found for this run ID."));
12051
+ console.log(import_chalk21.default.yellow(" No backup found for this run ID."));
11500
12052
  } else {
11501
- restored.forEach((f) => console.log(import_chalk18.default.green(` \u2714 restored: ${f}`)));
11502
- console.log(import_chalk18.default.bold.green(`
12053
+ restored.forEach((f) => console.log(import_chalk21.default.green(` \u2714 restored: ${f}`)));
12054
+ console.log(import_chalk21.default.bold.green(`
11503
12055
  \u2714 ${restored.length} file(s) restored.`));
11504
12056
  }
11505
12057
  });
12058
+ program.command("trend").description("Show harness score trend across past create runs").option("--last <n>", "Number of recent scored runs to show (default: 15)", "15").option("--prompt <hash>", "Filter to a specific prompt hash (prefix match)").option("--json", "Output raw JSON instead of formatted table").action(async (opts) => {
12059
+ const currentDir = process.cwd();
12060
+ const last = parseInt(opts.last, 10) || 15;
12061
+ const logs = await loadRunLogs(currentDir);
12062
+ if (logs.length === 0) {
12063
+ console.log(import_chalk21.default.yellow(
12064
+ "\n No run logs found. Run `ai-spec create` at least once to start tracking.\n"
12065
+ ));
12066
+ return;
12067
+ }
12068
+ const report = buildTrendReport(logs, {
12069
+ last,
12070
+ promptFilter: opts.prompt
12071
+ });
12072
+ if (opts.json) {
12073
+ console.log(JSON.stringify(report, null, 2));
12074
+ return;
12075
+ }
12076
+ printTrendReport(report, currentDir);
12077
+ });
12078
+ program.command("logs").description("List recent run logs with stage timing").argument("[runId]", "Show detailed stage breakdown for a specific run ID").option("--last <n>", "Number of runs to list (default: 10)", "10").action(async (runId, opts) => {
12079
+ const currentDir = process.cwd();
12080
+ const logDir = path23.join(currentDir, ".ai-spec-logs");
12081
+ if (!await fs24.pathExists(logDir)) {
12082
+ console.log(import_chalk21.default.yellow("\n No run logs found (.ai-spec-logs/ does not exist).\n"));
12083
+ return;
12084
+ }
12085
+ if (runId) {
12086
+ const logPath = path23.join(logDir, `${runId}.json`);
12087
+ if (!await fs24.pathExists(logPath)) {
12088
+ console.log(import_chalk21.default.red(`
12089
+ Run not found: ${runId}
12090
+ `));
12091
+ return;
12092
+ }
12093
+ const log = await fs24.readJson(logPath);
12094
+ console.log(import_chalk21.default.cyan(`
12095
+ \u2500\u2500\u2500 Run: ${log.runId} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
12096
+ console.log(import_chalk21.default.gray(` Started : ${log.startedAt}`));
12097
+ if (log.endedAt) console.log(import_chalk21.default.gray(` Ended : ${log.endedAt}`));
12098
+ if (log.totalDurationMs !== void 0)
12099
+ console.log(import_chalk21.default.gray(` Duration: ${(log.totalDurationMs / 1e3).toFixed(1)}s`));
12100
+ if (log.provider) console.log(import_chalk21.default.gray(` Provider: ${log.provider} / ${log.model ?? "?"}`));
12101
+ if (log.promptHash) console.log(import_chalk21.default.gray(` Prompt : ${log.promptHash}`));
12102
+ if (log.harnessScore !== void 0)
12103
+ console.log(import_chalk21.default.white(` Score : ${log.harnessScore}/10`));
12104
+ if (log.filesWritten?.length)
12105
+ console.log(import_chalk21.default.gray(` Files : ${log.filesWritten.length} written`));
12106
+ if (log.errors?.length)
12107
+ console.log(import_chalk21.default.yellow(` Errors : ${log.errors.length}`));
12108
+ if (log.entries?.length) {
12109
+ console.log(import_chalk21.default.bold("\n Stages:\n"));
12110
+ const doneEvents = log.entries.filter((e) => e.event.endsWith(":done") || e.event.endsWith(":failed"));
12111
+ for (const entry of doneEvents) {
12112
+ const isOk = entry.event.endsWith(":done");
12113
+ const stage = entry.event.replace(/:done$|:failed$/, "");
12114
+ const dur = entry.data?.durationMs ? import_chalk21.default.gray(` ${(Number(entry.data.durationMs) / 1e3).toFixed(1)}s`) : "";
12115
+ const mark = isOk ? import_chalk21.default.green("\u2714") : import_chalk21.default.red("\u2718");
12116
+ console.log(` ${mark} ${stage.padEnd(20)}${dur}`);
12117
+ }
12118
+ }
12119
+ console.log(import_chalk21.default.cyan("\u2500".repeat(52)));
12120
+ return;
12121
+ }
12122
+ const logs = await loadRunLogs(currentDir);
12123
+ const last = parseInt(opts.last, 10) || 10;
12124
+ const shown = logs.slice(0, last);
12125
+ if (shown.length === 0) {
12126
+ console.log(import_chalk21.default.yellow("\n No run logs found.\n"));
12127
+ return;
12128
+ }
12129
+ console.log(import_chalk21.default.cyan("\n\u2500\u2500\u2500 Run Logs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
12130
+ console.log(import_chalk21.default.gray(
12131
+ "\n " + "Run ID ".padEnd(26) + "Date " + "Score ".padStart(6) + " Files Dur\n"
12132
+ ));
12133
+ for (const log of shown) {
12134
+ const date = log.startedAt.slice(0, 10);
12135
+ const score = log.harnessScore !== void 0 ? (log.harnessScore >= 8 ? import_chalk21.default.green : log.harnessScore >= 6 ? import_chalk21.default.yellow : import_chalk21.default.red)(
12136
+ log.harnessScore.toFixed(1).padStart(5)
12137
+ ) : import_chalk21.default.gray(" \u2014");
12138
+ const files = String(log.filesWritten?.length ?? 0).padStart(5);
12139
+ const dur = log.totalDurationMs !== void 0 ? import_chalk21.default.gray((log.totalDurationMs / 1e3).toFixed(0) + "s") : import_chalk21.default.gray("\u2014");
12140
+ const errMark = (log.errors?.length ?? 0) > 0 ? import_chalk21.default.yellow(` \u26A0${log.errors.length}`) : "";
12141
+ console.log(` ${import_chalk21.default.white(log.runId.padEnd(25))} ${import_chalk21.default.gray(date)} ${score} ${import_chalk21.default.gray(files)} ${dur}${errMark}`);
12142
+ }
12143
+ console.log(import_chalk21.default.gray(`
12144
+ Showing ${shown.length} of ${logs.length} run(s) \xB7 logs: .ai-spec-logs/`));
12145
+ console.log(import_chalk21.default.cyan("\u2500".repeat(63)));
12146
+ console.log(import_chalk21.default.gray(` Tip: ai-spec logs <runId> to see stage breakdown`));
12147
+ console.log(import_chalk21.default.gray(` ai-spec trend to see score trend by prompt version
12148
+ `));
12149
+ });
11506
12150
  program.parse();
11507
12151
  //# sourceMappingURL=index.js.map