ai-spec-dev 0.31.0 → 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_chalk19 = __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
  };
@@ -9959,6 +9959,17 @@ function extractReviewScore(reviewText) {
9959
9959
  const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
9960
9960
  return match ? parseFloat(match[1]) : null;
9961
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
+ }
9962
9973
  function runSelfEval(opts) {
9963
9974
  const { dsl, generatedFiles, compilePassed, reviewText, promptHash, logger } = opts;
9964
9975
  const endpointsTotal = dsl?.endpoints?.length ?? 0;
@@ -9966,16 +9977,39 @@ function runSelfEval(opts) {
9966
9977
  const endpointLayerCovered = generatedFiles.some(
9967
9978
  (f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
9968
9979
  );
9980
+ const endpointLayerFiles = generatedFiles.filter(
9981
+ (f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
9982
+ ).length;
9969
9983
  const modelLayerCovered = generatedFiles.some(
9970
9984
  (f) => MODEL_LAYER_PATTERNS.some((p) => p.test(f))
9971
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;
9972
9998
  let dslCoverageScore = 10;
9973
9999
  if (generatedFiles.length === 0) {
9974
10000
  dslCoverageScore = 0;
9975
10001
  } else {
9976
10002
  if (endpointsTotal > 0 && !endpointLayerCovered) dslCoverageScore -= 4;
9977
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
+ }
9978
10011
  }
10012
+ dslCoverageScore = Math.max(0, Math.min(10, dslCoverageScore));
9979
10013
  const compileScore = compilePassed ? 10 : 5;
9980
10014
  const reviewScore = reviewText ? extractReviewScore(reviewText) : null;
9981
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;
@@ -9988,8 +10022,11 @@ function runSelfEval(opts) {
9988
10022
  detail: {
9989
10023
  endpointsTotal,
9990
10024
  endpointLayerCovered,
10025
+ endpointLayerFiles,
9991
10026
  modelsTotal,
9992
10027
  modelLayerCovered,
10028
+ modelNameCoverage: Math.round(modelNameCoverage * 100) / 100,
10029
+ modelNameMatched,
9993
10030
  filesWritten: generatedFiles.length
9994
10031
  }
9995
10032
  };
@@ -9999,32 +10036,324 @@ function runSelfEval(opts) {
9999
10036
  dslCoverageScore,
10000
10037
  compileScore,
10001
10038
  reviewScore: reviewScore ?? void 0,
10002
- promptHash
10039
+ promptHash,
10040
+ modelNameCoverage: result.detail.modelNameCoverage,
10041
+ modelNameMatched: result.detail.modelNameMatched,
10042
+ endpointLayerFiles: result.detail.endpointLayerFiles
10003
10043
  });
10004
10044
  return result;
10005
10045
  }
10006
10046
  function printSelfEval(result) {
10007
- const scoreColor = result.harnessScore >= 8 ? import_chalk18.default.green : result.harnessScore >= 6 ? import_chalk18.default.yellow : import_chalk18.default.red;
10047
+ const scoreColor2 = result.harnessScore >= 8 ? import_chalk18.default.green : result.harnessScore >= 6 ? import_chalk18.default.yellow : import_chalk18.default.red;
10008
10048
  const filled = Math.round(result.harnessScore);
10009
10049
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
10010
10050
  const compileTag = result.compileScore === 10 ? import_chalk18.default.green("pass") : import_chalk18.default.yellow("partial");
10011
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
+ }
10012
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"));
10013
- console.log(` Score : ${scoreColor(`[${bar}] ${result.harnessScore}/10`)}`);
10059
+ console.log(` Score : ${scoreColor2(`[${bar}] ${result.harnessScore}/10`)}`);
10014
10060
  console.log(
10015
- ` DSL : ${scoreColor(result.dslCoverageScore + "/10")} Compile: ${compileTag} ${reviewTag}`
10061
+ ` DSL : ${scoreColor2(String(result.dslCoverageScore) + "/10")} Compile: ${compileTag} ${reviewTag}`
10016
10062
  );
10063
+ if (modelCoverageTag) {
10064
+ console.log(
10065
+ ` Detail : ${modelCoverageTag} ` + import_chalk18.default.gray(`Endpoints: ${result.detail.endpointsTotal} Files: ${result.detail.filesWritten}`)
10066
+ );
10067
+ }
10017
10068
  console.log(import_chalk18.default.gray(` Prompt : ${result.promptHash}`));
10018
- console.log(import_chalk18.default.gray("\u2500".repeat(49)));
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."));
10019
10348
  }
10020
10349
 
10021
10350
  // cli/index.ts
10022
10351
  dotenv.config();
10023
10352
  var CONFIG_FILE = ".ai-spec.json";
10024
10353
  async function loadConfig(dir) {
10025
- const p = path22.join(dir, CONFIG_FILE);
10026
- if (await fs23.pathExists(p)) {
10027
- return fs23.readJson(p);
10354
+ const p = path23.join(dir, CONFIG_FILE);
10355
+ if (await fs24.pathExists(p)) {
10356
+ return fs24.readJson(p);
10028
10357
  }
10029
10358
  return {};
10030
10359
  }
@@ -10049,20 +10378,20 @@ async function resolveApiKey(providerName, cliKey) {
10049
10378
  validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
10050
10379
  });
10051
10380
  await saveKey(providerName, newKey.trim());
10052
- console.log(import_chalk19.default.gray(` Key saved to ${KEY_STORE_FILE}`));
10381
+ console.log(import_chalk21.default.gray(` Key saved to ${KEY_STORE_FILE}`));
10053
10382
  return newKey.trim();
10054
10383
  }
10055
10384
  function printBanner(opts) {
10056
- console.log(import_chalk19.default.blue("\n" + "\u2500".repeat(52)));
10057
- console.log(import_chalk19.default.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
10058
- console.log(import_chalk19.default.blue("\u2500".repeat(52)));
10059
- console.log(import_chalk19.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}`));
10060
10389
  console.log(
10061
- import_chalk19.default.gray(
10390
+ import_chalk21.default.gray(
10062
10391
  ` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
10063
10392
  )
10064
10393
  );
10065
- console.log(import_chalk19.default.blue("\u2500".repeat(52) + "\n"));
10394
+ console.log(import_chalk21.default.blue("\u2500".repeat(52) + "\n"));
10066
10395
  }
10067
10396
  var program = new import_commander.Command();
10068
10397
  program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
@@ -10089,42 +10418,42 @@ program.command("create").description("Generate a feature spec and kick off code
10089
10418
  const workspaceLoader = new WorkspaceLoader(currentDir);
10090
10419
  const workspaceConfig = await workspaceLoader.load();
10091
10420
  if (workspaceConfig) {
10092
- console.log(import_chalk19.default.cyan(`
10421
+ console.log(import_chalk21.default.cyan(`
10093
10422
  [Workspace] Detected workspace: ${workspaceConfig.name}`));
10094
- console.log(import_chalk19.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(", ")}`));
10095
10424
  const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
10096
10425
  if (opts.serve) {
10097
- console.log(import_chalk19.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"));
10098
10427
  const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
10099
10428
  const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
10100
10429
  if (!backendResult) {
10101
- console.log(import_chalk19.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."));
10102
10431
  } else {
10103
10432
  const mockPort = 3001;
10104
10433
  const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
10105
- const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
10106
- console.log(import_chalk19.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))`));
10107
10436
  const pid = startMockServerBackground(serverJsPath, mockPort);
10108
- console.log(import_chalk19.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}`));
10109
10438
  if (frontendResult) {
10110
10439
  const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
10111
10440
  await saveMockServerPid(frontendResult.repoAbsPath, pid);
10112
10441
  if (proxyResult.applied) {
10113
- console.log(import_chalk19.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
10114
- console.log(import_chalk19.default.bold.cyan(`
10442
+ console.log(import_chalk21.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
10443
+ console.log(import_chalk21.default.bold.cyan(`
10115
10444
  Ready! Run your frontend dev server:`));
10116
- console.log(import_chalk19.default.white(` cd ${frontendResult.repoAbsPath}`));
10117
- console.log(import_chalk19.default.white(` ${proxyResult.devCommand}`));
10118
- console.log(import_chalk19.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(`
10119
10448
  When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
10120
10449
  } else {
10121
- console.log(import_chalk19.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
10122
- if (proxyResult.note) console.log(import_chalk19.default.gray(` ${proxyResult.note}`));
10123
- console.log(import_chalk19.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}`));
10124
10453
  }
10125
10454
  } else {
10126
- console.log(import_chalk19.default.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
10127
- console.log(import_chalk19.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}`));
10128
10457
  }
10129
10458
  }
10130
10459
  }
@@ -10145,7 +10474,7 @@ program.command("create").description("Generate a feature spec and kick off code
10145
10474
  codegenModel: codegenModelName
10146
10475
  });
10147
10476
  const runId = generateRunId();
10148
- console.log(import_chalk19.default.gray(` Run ID: ${runId}`));
10477
+ console.log(import_chalk21.default.gray(` Run ID: ${runId}`));
10149
10478
  const runSnapshot = new RunSnapshot(currentDir, runId);
10150
10479
  setActiveSnapshot(runSnapshot);
10151
10480
  const runLogger = new RunLogger(currentDir, runId, {
@@ -10155,25 +10484,25 @@ program.command("create").description("Generate a feature spec and kick off code
10155
10484
  setActiveLogger(runLogger);
10156
10485
  const promptHash = computePromptHash();
10157
10486
  runLogger.setPromptHash(promptHash);
10158
- console.log(import_chalk19.default.blue("[1/6] Loading project context..."));
10487
+ console.log(import_chalk21.default.blue("[1/6] Loading project context..."));
10159
10488
  runLogger.stageStart("context_load");
10160
10489
  const loader = new ContextLoader(currentDir);
10161
10490
  const context = await loader.loadProjectContext();
10162
10491
  const { type: detectedRepoType } = await detectRepoType(currentDir);
10163
10492
  runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
10164
- console.log(import_chalk19.default.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10165
- console.log(import_chalk19.default.gray(` Dependencies: ${context.dependencies.length} packages`));
10166
- console.log(import_chalk19.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`));
10167
10496
  if (context.schema) {
10168
- console.log(import_chalk19.default.gray(` Prisma schema: found`));
10497
+ console.log(import_chalk21.default.gray(` Prisma schema: found`));
10169
10498
  }
10170
10499
  if (context.constitution) {
10171
- console.log(import_chalk19.default.green(` Constitution : found (.ai-spec-constitution.md)`));
10500
+ console.log(import_chalk21.default.green(` Constitution : found (.ai-spec-constitution.md)`));
10172
10501
  if (context.constitution.length > 6e3) {
10173
- console.log(import_chalk19.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`));
10174
10503
  }
10175
10504
  } else {
10176
- console.log(import_chalk19.default.yellow(" Constitution : not found \u2014 auto-generating..."));
10505
+ console.log(import_chalk21.default.yellow(" Constitution : not found \u2014 auto-generating..."));
10177
10506
  try {
10178
10507
  const constitutionGen = new ConstitutionGenerator(
10179
10508
  createProvider(specProviderName, specApiKey, specModelName)
@@ -10181,12 +10510,12 @@ program.command("create").description("Generate a feature spec and kick off code
10181
10510
  const constitutionContent = await constitutionGen.generate(currentDir);
10182
10511
  await constitutionGen.saveConstitution(currentDir, constitutionContent);
10183
10512
  context.constitution = constitutionContent;
10184
- console.log(import_chalk19.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)`));
10185
10514
  } catch (err) {
10186
- console.log(import_chalk19.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.`));
10187
10516
  }
10188
10517
  }
10189
- console.log(import_chalk19.default.blue(`
10518
+ console.log(import_chalk21.default.blue(`
10190
10519
  [2/6] Generating spec with ${specProviderName}/${specModelName}...`));
10191
10520
  const specProvider = createProvider(specProviderName, specApiKey, specModelName);
10192
10521
  let initialSpec;
@@ -10196,30 +10525,30 @@ program.command("create").description("Generate a feature spec and kick off code
10196
10525
  if (opts.skipTasks) {
10197
10526
  const generator = new SpecGenerator(specProvider);
10198
10527
  initialSpec = await generator.generateSpec(idea, context);
10199
- console.log(import_chalk19.default.green(" \u2714 Spec generated."));
10528
+ console.log(import_chalk21.default.green(" \u2714 Spec generated."));
10200
10529
  } else {
10201
10530
  const result = await generateSpecWithTasks(specProvider, idea, context);
10202
10531
  initialSpec = result.spec;
10203
10532
  initialTasks = result.tasks;
10204
- console.log(import_chalk19.default.green(` \u2714 Spec generated.`));
10533
+ console.log(import_chalk21.default.green(` \u2714 Spec generated.`));
10205
10534
  if (initialTasks.length > 0) {
10206
- console.log(import_chalk19.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
10535
+ console.log(import_chalk21.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
10207
10536
  } else {
10208
- console.log(import_chalk19.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."));
10209
10538
  }
10210
10539
  }
10211
10540
  runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
10212
10541
  } catch (err) {
10213
10542
  runLogger.stageFail("spec_gen", err.message);
10214
- console.error(import_chalk19.default.red(" \u2718 Spec generation failed:"), err);
10543
+ console.error(import_chalk21.default.red(" \u2718 Spec generation failed:"), err);
10215
10544
  process.exit(1);
10216
10545
  }
10217
10546
  let finalSpec;
10218
10547
  if (opts.fast) {
10219
- console.log(import_chalk19.default.gray("\n[3/6] Skipping refinement (--fast)."));
10548
+ console.log(import_chalk21.default.gray("\n[3/6] Skipping refinement (--fast)."));
10220
10549
  finalSpec = initialSpec;
10221
10550
  } else {
10222
- console.log(import_chalk19.default.blue("\n[3/6] Interactive spec refinement..."));
10551
+ console.log(import_chalk21.default.blue("\n[3/6] Interactive spec refinement..."));
10223
10552
  runLogger.stageStart("spec_refine");
10224
10553
  const refiner = new SpecRefiner(specProvider);
10225
10554
  finalSpec = await refiner.refineLoop(initialSpec);
@@ -10230,7 +10559,7 @@ program.command("create").description("Generate a feature spec and kick off code
10230
10559
  const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
10231
10560
  if (shouldRunAssessment) {
10232
10561
  if (!opts.auto) {
10233
- console.log(import_chalk19.default.blue("\n[3.4/6] Spec quality assessment..."));
10562
+ console.log(import_chalk21.default.blue("\n[3.4/6] Spec quality assessment..."));
10234
10563
  }
10235
10564
  runLogger.stageStart("spec_assess");
10236
10565
  const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
@@ -10239,45 +10568,45 @@ program.command("create").description("Generate a feature spec and kick off code
10239
10568
  if (!opts.auto) printSpecAssessment(assessment);
10240
10569
  if (minScore > 0 && assessment.overallScore < minScore) {
10241
10570
  if (opts.force) {
10242
- console.log(import_chalk19.default.yellow(`
10571
+ console.log(import_chalk21.default.yellow(`
10243
10572
  \u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
10244
10573
  } else {
10245
10574
  runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
10246
- console.log(import_chalk19.default.red(`
10575
+ console.log(import_chalk21.default.red(`
10247
10576
  \u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
10248
10577
  if (!opts.auto) {
10249
- console.log(import_chalk19.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.`));
10250
10579
  } else {
10251
- console.log(import_chalk19.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.`));
10252
10581
  }
10253
- console.log(import_chalk19.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}`));
10254
10583
  process.exit(1);
10255
10584
  }
10256
10585
  }
10257
10586
  } else {
10258
10587
  runLogger.stageEnd("spec_assess", { skipped: true });
10259
10588
  if (!opts.auto) {
10260
- console.log(import_chalk19.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)"));
10261
10590
  }
10262
10591
  }
10263
10592
  }
10264
10593
  if (!opts.auto) {
10265
- console.log(import_chalk19.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"));
10266
10595
  const specLines = finalSpec.split("\n").length;
10267
10596
  const specWords = finalSpec.split(/\s+/).length;
10268
10597
  const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
10269
- console.log(import_chalk19.default.gray(` Spec length : ${specLines} lines / ${specWords} words`));
10270
- if (taskCountHint) console.log(import_chalk19.default.gray(taskCountHint));
10271
- 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");
10272
10601
  const slug = featureSlug;
10273
10602
  const prevVersion = await findLatestVersion(previewSpecsDir, slug);
10274
10603
  if (prevVersion) {
10275
- console.log(import_chalk19.default.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
10604
+ console.log(import_chalk21.default.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
10276
10605
  const diff = computeDiff(prevVersion.content, finalSpec);
10277
- console.log(import_chalk19.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"));
10278
10607
  printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
10279
10608
  printDiff(diff);
10280
- console.log(import_chalk19.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"));
10281
10610
  }
10282
10611
  const gate = await (0, import_prompts3.select)({
10283
10612
  message: "Ready to proceed to code generation?",
@@ -10288,9 +10617,9 @@ program.command("create").description("Generate a feature spec and kick off code
10288
10617
  ]
10289
10618
  });
10290
10619
  if (gate === "view") {
10291
- console.log(import_chalk19.default.cyan("\n" + "\u2500".repeat(52)));
10620
+ console.log(import_chalk21.default.cyan("\n" + "\u2500".repeat(52)));
10292
10621
  console.log(finalSpec);
10293
- console.log(import_chalk19.default.cyan("\u2500".repeat(52) + "\n"));
10622
+ console.log(import_chalk21.default.cyan("\u2500".repeat(52) + "\n"));
10294
10623
  const confirm22 = await (0, import_prompts3.select)({
10295
10624
  message: "Proceed to code generation?",
10296
10625
  choices: [
@@ -10299,92 +10628,135 @@ program.command("create").description("Generate a feature spec and kick off code
10299
10628
  ]
10300
10629
  });
10301
10630
  if (confirm22 === "abort") {
10302
- console.log(import_chalk19.default.yellow(" Aborted. Spec was NOT saved."));
10631
+ console.log(import_chalk21.default.yellow(" Aborted. Spec was NOT saved."));
10303
10632
  process.exit(0);
10304
10633
  }
10305
10634
  } else if (gate === "abort") {
10306
- console.log(import_chalk19.default.yellow(" Aborted. Spec was NOT saved."));
10635
+ console.log(import_chalk21.default.yellow(" Aborted. Spec was NOT saved."));
10307
10636
  process.exit(0);
10308
10637
  }
10309
- console.log(import_chalk19.default.green(" \u2714 Approved \u2014 continuing to code generation."));
10638
+ console.log(import_chalk21.default.green(" \u2714 Approved \u2014 continuing to code generation."));
10310
10639
  } else {
10311
- console.log(import_chalk19.default.gray("[3.5/6] Approval Gate: skipped (--auto)."));
10640
+ console.log(import_chalk21.default.gray("[3.5/6] Approval Gate: skipped (--auto)."));
10312
10641
  }
10313
10642
  let extractedDsl = null;
10314
10643
  if (opts.skipDsl) {
10315
- console.log(import_chalk19.default.gray("\n[DSL] Skipped (--skip-dsl)."));
10644
+ console.log(import_chalk21.default.gray("\n[DSL] Skipped (--skip-dsl)."));
10316
10645
  } else {
10317
- console.log(import_chalk19.default.blue("\n[DSL] Extracting structured DSL from spec..."));
10318
- console.log(import_chalk19.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}`));
10319
10648
  runLogger.stageStart("dsl_extract");
10320
10649
  try {
10321
10650
  const isFrontend = isFrontendDeps(context.dependencies);
10322
- if (isFrontend) console.log(import_chalk19.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"));
10323
10652
  const dslExtractor = new DslExtractor(specProvider);
10324
10653
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
10325
10654
  if (extractedDsl) {
10326
10655
  runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
10327
- console.log(import_chalk19.default.green(" \u2714 DSL extracted and validated."));
10656
+ console.log(import_chalk21.default.green(" \u2714 DSL extracted and validated."));
10328
10657
  } else {
10329
10658
  runLogger.stageEnd("dsl_extract", { skipped: true });
10330
- console.log(import_chalk19.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."));
10331
10660
  }
10332
10661
  } catch (err) {
10333
10662
  runLogger.stageFail("dsl_extract", err.message);
10334
- console.log(import_chalk19.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
+ }
10335
10707
  }
10336
10708
  }
10337
10709
  const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
10338
10710
  const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
10339
10711
  let workingDir = currentDir;
10340
10712
  if (!skipWorktree) {
10341
- console.log(import_chalk19.default.blue("\n[4/6] Setting up git worktree..."));
10713
+ console.log(import_chalk21.default.blue("\n[4/6] Setting up git worktree..."));
10342
10714
  const worktreeManager = new GitWorktreeManager(currentDir);
10343
10715
  const worktreePath = await worktreeManager.createWorktree(idea);
10344
10716
  if (worktreePath) workingDir = worktreePath;
10345
10717
  } else {
10346
10718
  const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
10347
- console.log(import_chalk19.default.gray(`[4/6] Skipping worktree${reason}.`));
10719
+ console.log(import_chalk21.default.gray(`[4/6] Skipping worktree${reason}.`));
10348
10720
  }
10349
- const specsDir = path22.join(workingDir, "specs");
10350
- await fs23.ensureDir(specsDir);
10721
+ const specsDir = path23.join(workingDir, "specs");
10722
+ await fs24.ensureDir(specsDir);
10351
10723
  const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
10352
- await fs23.writeFile(specFile, finalSpec, "utf-8");
10353
- console.log(import_chalk19.default.green(`
10354
- [5/6] \u2714 Spec saved: ${specFile}`) + import_chalk19.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})`));
10355
10727
  let savedDslFile = null;
10356
10728
  if (extractedDsl) {
10357
10729
  const dslExtractor = new DslExtractor(specProvider);
10358
10730
  savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
10359
- console.log(import_chalk19.default.green(` \u2714 DSL saved : ${savedDslFile}`));
10731
+ console.log(import_chalk21.default.green(` \u2714 DSL saved : ${savedDslFile}`));
10360
10732
  }
10361
10733
  if (!opts.skipTasks) {
10362
10734
  const taskGen = new TaskGenerator(specProvider);
10363
10735
  let tasksToSave = initialTasks;
10364
10736
  if (tasksToSave.length === 0) {
10365
- console.log(import_chalk19.default.blue(`
10737
+ console.log(import_chalk21.default.blue(`
10366
10738
  Generating tasks (separate call)...`));
10367
10739
  try {
10368
10740
  tasksToSave = await taskGen.generateTasks(finalSpec, context);
10369
10741
  } catch (err) {
10370
- console.log(import_chalk19.default.yellow(` \u26A0 Task generation failed: ${err.message}`));
10742
+ console.log(import_chalk21.default.yellow(` \u26A0 Task generation failed: ${err.message}`));
10371
10743
  }
10372
10744
  }
10373
10745
  if (tasksToSave.length > 0) {
10374
10746
  const sorted = taskGen.sortByLayer(tasksToSave);
10375
10747
  const tasksFile = await taskGen.saveTasks(sorted, specFile);
10376
10748
  printTasks(sorted);
10377
- console.log(import_chalk19.default.green(` \u2714 Tasks saved: ${tasksFile}`));
10749
+ console.log(import_chalk21.default.green(` \u2714 Tasks saved: ${tasksFile}`));
10378
10750
  } else {
10379
- console.log(import_chalk19.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."));
10380
10752
  }
10381
10753
  }
10382
- console.log(import_chalk19.default.blue(`
10754
+ console.log(import_chalk21.default.blue(`
10383
10755
  [6/6] Code generation (mode: ${codegenMode})...`));
10384
10756
  const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
10385
10757
  let generatedTestFiles = [];
10386
10758
  if (opts.tdd && extractedDsl) {
10387
- console.log(import_chalk19.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)..."));
10388
10760
  const testGen = new TestGenerator(codegenProvider);
10389
10761
  generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
10390
10762
  }
@@ -10398,13 +10770,13 @@ program.command("create").description("Generate a feature spec and kick off code
10398
10770
  });
10399
10771
  runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
10400
10772
  if (opts.tdd) {
10401
- console.log(import_chalk19.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."));
10402
10774
  } else if (opts.skipTests) {
10403
- console.log(import_chalk19.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)."));
10404
10776
  } else if (!extractedDsl) {
10405
- console.log(import_chalk19.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)."));
10406
10778
  } else {
10407
- console.log(import_chalk19.default.blue(`
10779
+ console.log(import_chalk21.default.blue(`
10408
10780
  [7/9] Test skeleton generation...`));
10409
10781
  runLogger.stageStart("test_gen");
10410
10782
  const testGen = new TestGenerator(codegenProvider);
@@ -10413,11 +10785,11 @@ program.command("create").description("Generate a feature spec and kick off code
10413
10785
  }
10414
10786
  let compilePassed = false;
10415
10787
  if (opts.skipErrorFeedback) {
10416
- console.log(import_chalk19.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)."));
10417
10789
  compilePassed = true;
10418
10790
  } else {
10419
10791
  if (opts.tdd) {
10420
- console.log(import_chalk19.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..."));
10421
10793
  }
10422
10794
  runLogger.stageStart("error_feedback");
10423
10795
  compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
@@ -10428,10 +10800,10 @@ program.command("create").description("Generate a feature spec and kick off code
10428
10800
  }
10429
10801
  let reviewResult = "";
10430
10802
  if (!opts.skipReview) {
10431
- console.log(import_chalk19.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)..."));
10432
10804
  runLogger.stageStart("review");
10433
10805
  const reviewer = new CodeReviewer(specProvider, currentDir);
10434
- const savedSpec = await fs23.readFile(specFile, "utf-8");
10806
+ const savedSpec = await fs24.readFile(specFile, "utf-8");
10435
10807
  if (codegenMode === "api" && generatedFiles.length > 0) {
10436
10808
  reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
10437
10809
  } else {
@@ -10446,6 +10818,65 @@ program.command("create").description("Generate a feature spec and kick off code
10446
10818
  runLogger.stageEnd("review");
10447
10819
  await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
10448
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
+ }
10449
10880
  runLogger.stageStart("self_eval");
10450
10881
  const selfEvalResult = runSelfEval({
10451
10882
  dsl: extractedDsl,
@@ -10457,19 +10888,19 @@ program.command("create").description("Generate a feature spec and kick off code
10457
10888
  });
10458
10889
  printSelfEval(selfEvalResult);
10459
10890
  runLogger.finish();
10460
- console.log(import_chalk19.default.bold.green("\n\u2714 All done!"));
10461
- console.log(import_chalk19.default.gray(` Spec : ${specFile}`));
10462
- if (savedDslFile) console.log(import_chalk19.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}`));
10463
10894
  if (generatedTestFiles.length > 0) {
10464
- console.log(import_chalk19.default.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
10895
+ console.log(import_chalk21.default.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
10465
10896
  }
10466
- console.log(import_chalk19.default.gray(` Working dir : ${workingDir}`));
10897
+ console.log(import_chalk21.default.gray(` Working dir : ${workingDir}`));
10467
10898
  if (workingDir !== currentDir) {
10468
- console.log(import_chalk19.default.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10899
+ console.log(import_chalk21.default.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10469
10900
  }
10470
10901
  runLogger.printSummary();
10471
10902
  if (runSnapshot.fileCount > 0) {
10472
- console.log(import_chalk19.default.gray(` To undo changes: ai-spec restore ${runId}`));
10903
+ console.log(import_chalk21.default.gray(` To undo changes: ai-spec restore ${runId}`));
10473
10904
  }
10474
10905
  });
10475
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(
@@ -10486,24 +10917,24 @@ program.command("review").description("Run AI code review on current git diff ag
10486
10917
  const reviewer = new CodeReviewer(provider, currentDir);
10487
10918
  let specContent = "";
10488
10919
  let resolvedSpecFile;
10489
- if (specFile && await fs23.pathExists(specFile)) {
10490
- specContent = await fs23.readFile(specFile, "utf-8");
10920
+ if (specFile && await fs24.pathExists(specFile)) {
10921
+ specContent = await fs24.readFile(specFile, "utf-8");
10491
10922
  resolvedSpecFile = specFile;
10492
- console.log(import_chalk19.default.gray(`Using spec: ${specFile}`));
10923
+ console.log(import_chalk21.default.gray(`Using spec: ${specFile}`));
10493
10924
  } else {
10494
- const specsDir = path22.join(currentDir, "specs");
10495
- if (await fs23.pathExists(specsDir)) {
10496
- 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();
10497
10928
  if (files.length > 0) {
10498
- const latest = path22.join(specsDir, files[0]);
10499
- specContent = await fs23.readFile(latest, "utf-8");
10929
+ const latest = path23.join(specsDir, files[0]);
10930
+ specContent = await fs24.readFile(latest, "utf-8");
10500
10931
  resolvedSpecFile = latest;
10501
- console.log(import_chalk19.default.gray(`Auto-detected spec: specs/${files[0]}`));
10932
+ console.log(import_chalk21.default.gray(`Auto-detected spec: specs/${files[0]}`));
10502
10933
  }
10503
10934
  }
10504
10935
  }
10505
10936
  if (!specContent) {
10506
- console.log(import_chalk19.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."));
10507
10938
  }
10508
10939
  await reviewer.reviewCode(specContent, resolvedSpecFile);
10509
10940
  await reviewer.printScoreTrend();
@@ -10530,15 +10961,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
10530
10961
  auto: opts.auto
10531
10962
  });
10532
10963
  if (result.written) {
10533
- console.log(import_chalk19.default.blue("\n Summary:"));
10534
- console.log(import_chalk19.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)})`));
10535
- console.log(import_chalk19.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`));
10536
10967
  if (result.backupPath) {
10537
- console.log(import_chalk19.default.gray(` Backup: ${path22.basename(result.backupPath)}`));
10968
+ console.log(import_chalk21.default.gray(` Backup: ${path23.basename(result.backupPath)}`));
10538
10969
  }
10539
10970
  }
10540
10971
  } catch (err) {
10541
- console.error(import_chalk19.default.red(` \u2718 Consolidation failed: ${err.message}`));
10972
+ console.error(import_chalk21.default.red(` \u2718 Consolidation failed: ${err.message}`));
10542
10973
  process.exit(1);
10543
10974
  }
10544
10975
  return;
@@ -10546,75 +10977,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
10546
10977
  if (opts.global) {
10547
10978
  const existing = await loadGlobalConstitution([currentDir]);
10548
10979
  if (existing && !opts.force) {
10549
- console.log(import_chalk19.default.yellow(`
10980
+ console.log(import_chalk21.default.yellow(`
10550
10981
  Global constitution already exists at: ${existing.source}`));
10551
- console.log(import_chalk19.default.gray(" Use --force to overwrite it."));
10982
+ console.log(import_chalk21.default.gray(" Use --force to overwrite it."));
10552
10983
  return;
10553
10984
  }
10554
- console.log(import_chalk19.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10555
- console.log(import_chalk19.default.gray(` Provider: ${providerName}/${modelName}`));
10556
- console.log(import_chalk19.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..."));
10557
10988
  const loader = new ContextLoader(currentDir);
10558
10989
  const ctx = await loader.loadProjectContext();
10559
10990
  const summary = [
10560
10991
  `Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
10561
10992
  `Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
10562
10993
  ].join("\n");
10563
- const prompt = buildGlobalConstitutionPrompt([{ name: path22.basename(currentDir), summary }]);
10994
+ const prompt = buildGlobalConstitutionPrompt([{ name: path23.basename(currentDir), summary }]);
10564
10995
  let globalConstitution;
10565
10996
  try {
10566
10997
  globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
10567
10998
  } catch (err) {
10568
- console.error(import_chalk19.default.red(" \u2718 Failed to generate global constitution:"), err);
10999
+ console.error(import_chalk21.default.red(" \u2718 Failed to generate global constitution:"), err);
10569
11000
  process.exit(1);
10570
11001
  }
10571
11002
  const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
10572
- console.log(import_chalk19.default.green(`
11003
+ console.log(import_chalk21.default.green(`
10573
11004
  \u2714 Global constitution saved: ${saved2}`));
10574
- console.log(import_chalk19.default.gray(" This will be automatically merged into all project constitutions in this workspace."));
10575
- console.log(import_chalk19.default.gray(" Project-level rules always override global rules.\n"));
10576
- console.log(import_chalk19.default.bold(" Preview:"));
10577
- console.log(import_chalk19.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")));
10578
11009
  if (globalConstitution.split("\n").length > 12) {
10579
- console.log(import_chalk19.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
11010
+ console.log(import_chalk21.default.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
10580
11011
  }
10581
11012
  return;
10582
11013
  }
10583
- const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
10584
- if (!opts.force && await fs23.pathExists(constitutionPath)) {
10585
- console.log(import_chalk19.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(`
10586
11017
  ${CONSTITUTION_FILE} already exists.`));
10587
- console.log(import_chalk19.default.gray(" Use --force to overwrite it."));
10588
- console.log(import_chalk19.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}`));
10589
11020
  return;
10590
11021
  }
10591
- console.log(import_chalk19.default.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10592
- console.log(import_chalk19.default.gray(` Provider: ${providerName}/${modelName}`));
10593
- console.log(import_chalk19.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..."));
10594
11025
  const generator = new ConstitutionGenerator(provider);
10595
11026
  let constitution;
10596
11027
  try {
10597
11028
  constitution = await generator.generate(currentDir);
10598
11029
  } catch (err) {
10599
- console.error(import_chalk19.default.red(" \u2718 Failed to generate constitution:"), err);
11030
+ console.error(import_chalk21.default.red(" \u2718 Failed to generate constitution:"), err);
10600
11031
  process.exit(1);
10601
11032
  }
10602
11033
  const saved = await generator.saveConstitution(currentDir, constitution);
10603
- const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
11034
+ const globalResult = await loadGlobalConstitution([path23.dirname(currentDir)]);
10604
11035
  if (globalResult) {
10605
- console.log(import_chalk19.default.cyan(`
11036
+ console.log(import_chalk21.default.cyan(`
10606
11037
  \u2139 Global constitution detected: ${globalResult.source}`));
10607
- console.log(import_chalk19.default.gray(" It will be merged with this project constitution at runtime."));
10608
- console.log(import_chalk19.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."));
10609
11040
  }
10610
- console.log(import_chalk19.default.green(`
11041
+ console.log(import_chalk21.default.green(`
10611
11042
  \u2714 Constitution saved: ${saved}`));
10612
- console.log(import_chalk19.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
10613
- console.log(import_chalk19.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
10614
- console.log(import_chalk19.default.bold(" Preview:"));
10615
- console.log(import_chalk19.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")));
10616
11047
  if (constitution.split("\n").length > 15) {
10617
- console.log(import_chalk19.default.gray(` ... (${constitution.split("\n").length} lines total)`));
11048
+ console.log(import_chalk21.default.gray(` ... (${constitution.split("\n").length} lines total)`));
10618
11049
  }
10619
11050
  });
10620
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(
@@ -10622,44 +11053,44 @@ program.command("config").description(`Set default configuration for this projec
10622
11053
  "Default code generation mode (claude-code|api|plan)"
10623
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) => {
10624
11055
  const currentDir = process.cwd();
10625
- const configPath = path22.join(currentDir, CONFIG_FILE);
11056
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10626
11057
  if (opts.clearKeys) {
10627
11058
  await clearAllKeys();
10628
- console.log(import_chalk19.default.green(`\u2714 All saved API keys cleared.`));
11059
+ console.log(import_chalk21.default.green(`\u2714 All saved API keys cleared.`));
10629
11060
  return;
10630
11061
  }
10631
11062
  if (opts.clearKey) {
10632
11063
  await clearKey(opts.clearKey);
10633
- console.log(import_chalk19.default.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
11064
+ console.log(import_chalk21.default.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
10634
11065
  return;
10635
11066
  }
10636
11067
  if (opts.listKeys) {
10637
- const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
11068
+ const store = await fs24.readJson(KEY_STORE_FILE).catch(() => ({}));
10638
11069
  const providers = Object.keys(store);
10639
11070
  if (providers.length === 0) {
10640
- console.log(import_chalk19.default.gray("No saved API keys."));
11071
+ console.log(import_chalk21.default.gray("No saved API keys."));
10641
11072
  } else {
10642
- console.log(import_chalk19.default.bold("Saved API keys:"));
11073
+ console.log(import_chalk21.default.bold("Saved API keys:"));
10643
11074
  for (const p of providers) {
10644
11075
  const k2 = store[p];
10645
- console.log(import_chalk19.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)}`));
10646
11077
  }
10647
- console.log(import_chalk19.default.gray(`
11078
+ console.log(import_chalk21.default.gray(`
10648
11079
  File: ${KEY_STORE_FILE}`));
10649
11080
  }
10650
11081
  return;
10651
11082
  }
10652
11083
  if (opts.reset) {
10653
- await fs23.writeJson(configPath, {}, { spaces: 2 });
10654
- console.log(import_chalk19.default.green(`\u2714 Config reset: ${configPath}`));
11084
+ await fs24.writeJson(configPath, {}, { spaces: 2 });
11085
+ console.log(import_chalk21.default.green(`\u2714 Config reset: ${configPath}`));
10655
11086
  return;
10656
11087
  }
10657
11088
  const existing = await loadConfig(currentDir);
10658
11089
  if (opts.show) {
10659
11090
  if (Object.keys(existing).length === 0) {
10660
- console.log(import_chalk19.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."));
10661
11092
  } else {
10662
- console.log(import_chalk19.default.bold(`${configPath}:`));
11093
+ console.log(import_chalk21.default.bold(`${configPath}:`));
10663
11094
  console.log(JSON.stringify(existing, null, 2));
10664
11095
  }
10665
11096
  return;
@@ -10673,27 +11104,27 @@ File: ${KEY_STORE_FILE}`));
10673
11104
  if (opts.minSpecScore !== void 0) {
10674
11105
  const score = parseInt(opts.minSpecScore, 10);
10675
11106
  if (isNaN(score) || score < 0 || score > 10) {
10676
- console.error(import_chalk19.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"));
10677
11108
  process.exit(1);
10678
11109
  }
10679
11110
  updated.minSpecScore = score;
10680
11111
  }
10681
- await fs23.writeJson(configPath, updated, { spaces: 2 });
10682
- console.log(import_chalk19.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}`));
10683
11114
  console.log(JSON.stringify(updated, null, 2));
10684
11115
  });
10685
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) => {
10686
11117
  const currentDir = process.cwd();
10687
- const configPath = path22.join(currentDir, CONFIG_FILE);
11118
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10688
11119
  if (opts.list) {
10689
- console.log(import_chalk19.default.bold("\nAvailable providers & models:\n"));
11120
+ console.log(import_chalk21.default.bold("\nAvailable providers & models:\n"));
10690
11121
  for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
10691
11122
  console.log(
10692
- ` ${import_chalk19.default.bold.cyan(key.padEnd(10))} ${import_chalk19.default.white(meta.displayName)}`
11123
+ ` ${import_chalk21.default.bold.cyan(key.padEnd(10))} ${import_chalk21.default.white(meta.displayName)}`
10693
11124
  );
10694
- console.log(import_chalk19.default.gray(` ${meta.description}`));
11125
+ console.log(import_chalk21.default.gray(` ${meta.description}`));
10695
11126
  console.log(
10696
- import_chalk19.default.gray(
11127
+ import_chalk21.default.gray(
10697
11128
  ` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
10698
11129
  )
10699
11130
  );
@@ -10702,10 +11133,10 @@ program.command("model").description("Interactively switch the active AI provide
10702
11133
  return;
10703
11134
  }
10704
11135
  const existing = await loadConfig(currentDir);
10705
- console.log(import_chalk19.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"));
10706
11137
  if (Object.keys(existing).length > 0) {
10707
11138
  console.log(
10708
- import_chalk19.default.gray(
11139
+ import_chalk21.default.gray(
10709
11140
  ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
10710
11141
  )
10711
11142
  );
@@ -10723,7 +11154,7 @@ program.command("model").description("Interactively switch the active AI provide
10723
11154
  const providerKey = await (0, import_prompts3.select)({
10724
11155
  message: `${label} \u2014 select provider:`,
10725
11156
  choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
10726
- name: `${meta2.displayName.padEnd(22)} ${import_chalk19.default.gray(meta2.description)}`,
11157
+ name: `${meta2.displayName.padEnd(22)} ${import_chalk21.default.gray(meta2.description)}`,
10727
11158
  value: key,
10728
11159
  short: meta2.displayName
10729
11160
  }))
@@ -10731,7 +11162,7 @@ program.command("model").description("Interactively switch the active AI provide
10731
11162
  const meta = PROVIDER_CATALOG[providerKey];
10732
11163
  const modelChoices = [
10733
11164
  ...meta.models.map((m) => ({ name: m, value: m })),
10734
- { name: import_chalk19.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
11165
+ { name: import_chalk21.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
10735
11166
  ];
10736
11167
  let chosenModel = await (0, import_prompts3.select)({
10737
11168
  message: `${label} \u2014 select model (${meta.displayName}):`,
@@ -10765,37 +11196,37 @@ program.command("model").description("Interactively switch the active AI provide
10765
11196
  if (!updated.codegen || updated.codegen === "claude-code") {
10766
11197
  updated.codegen = "api";
10767
11198
  console.log(
10768
- import_chalk19.default.yellow(
11199
+ import_chalk21.default.yellow(
10769
11200
  `
10770
11201
  \u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
10771
11202
  )
10772
11203
  );
10773
- console.log(import_chalk19.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`));
10774
11205
  }
10775
11206
  }
10776
11207
  }
10777
- console.log(import_chalk19.default.blue("\n Preview:"));
10778
- console.log(import_chalk19.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}`));
10779
11210
  if (updated.codegenProvider) {
10780
11211
  console.log(
10781
- import_chalk19.default.gray(
11212
+ import_chalk21.default.gray(
10782
11213
  ` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
10783
11214
  )
10784
11215
  );
10785
11216
  }
10786
11217
  const ok = await (0, import_prompts3.confirm)({ message: "Save to .ai-spec.json?", default: true });
10787
11218
  if (!ok) {
10788
- console.log(import_chalk19.default.gray(" Cancelled."));
11219
+ console.log(import_chalk21.default.gray(" Cancelled."));
10789
11220
  return;
10790
11221
  }
10791
- await fs23.writeJson(configPath, updated, { spaces: 2 });
10792
- console.log(import_chalk19.default.green(`
11222
+ await fs24.writeJson(configPath, updated, { spaces: 2 });
11223
+ console.log(import_chalk21.default.green(`
10793
11224
  \u2714 Saved to ${configPath}`));
10794
11225
  const providerToCheck = updated.provider ?? "gemini";
10795
11226
  const envKey = ENV_KEY_MAP[providerToCheck];
10796
11227
  if (envKey && !process.env[envKey]) {
10797
11228
  console.log(
10798
- import_chalk19.default.yellow(
11229
+ import_chalk21.default.yellow(
10799
11230
  ` \u26A0 Remember to set ${envKey} in your environment or .env file.`
10800
11231
  )
10801
11232
  );
@@ -10814,29 +11245,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10814
11245
  cliOpts,
10815
11246
  contractContextSection
10816
11247
  } = opts;
10817
- console.log(import_chalk19.default.blue(`
11248
+ console.log(import_chalk21.default.blue(`
10818
11249
  [${repoName}] Loading project context...`));
10819
11250
  const loader = new ContextLoader(repoAbsPath);
10820
11251
  let context = await loader.loadProjectContext();
10821
11252
  const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
10822
- console.log(import_chalk19.default.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10823
- console.log(import_chalk19.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`));
10824
11255
  if (context.constitution && context.constitution.length > 6e3) {
10825
- console.log(import_chalk19.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`));
10826
11257
  }
10827
11258
  if (!context.constitution) {
10828
- console.log(import_chalk19.default.yellow(` Constitution: not found \u2014 auto-generating...`));
11259
+ console.log(import_chalk21.default.yellow(` Constitution: not found \u2014 auto-generating...`));
10829
11260
  try {
10830
11261
  const constitutionGen = new ConstitutionGenerator(specProvider);
10831
11262
  const constitutionContent = await constitutionGen.generate(repoAbsPath);
10832
11263
  await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
10833
11264
  context.constitution = constitutionContent;
10834
- console.log(import_chalk19.default.green(` Constitution: generated`));
11265
+ console.log(import_chalk21.default.green(` Constitution: generated`));
10835
11266
  } catch (err) {
10836
- console.log(import_chalk19.default.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
11267
+ console.log(import_chalk21.default.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
10837
11268
  }
10838
11269
  } else {
10839
- console.log(import_chalk19.default.green(` Constitution: found`));
11270
+ console.log(import_chalk21.default.green(` Constitution: found`));
10840
11271
  }
10841
11272
  let fullIdea = idea;
10842
11273
  if (contractContextSection) {
@@ -10844,58 +11275,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10844
11275
 
10845
11276
  ${contractContextSection}`;
10846
11277
  }
10847
- console.log(import_chalk19.default.blue(` [${repoName}] Generating spec...`));
11278
+ console.log(import_chalk21.default.blue(` [${repoName}] Generating spec...`));
10848
11279
  let finalSpec;
10849
11280
  try {
10850
11281
  const result = await generateSpecWithTasks(specProvider, fullIdea, context);
10851
11282
  finalSpec = result.spec;
10852
- console.log(import_chalk19.default.green(` Spec generated.`));
11283
+ console.log(import_chalk21.default.green(` Spec generated.`));
10853
11284
  } catch (err) {
10854
- console.error(import_chalk19.default.red(` Spec generation failed: ${err.message}`));
11285
+ console.error(import_chalk21.default.red(` Spec generation failed: ${err.message}`));
10855
11286
  return { dsl: null, specFile: null };
10856
11287
  }
10857
11288
  let extractedDsl = null;
10858
11289
  if (!cliOpts.skipDsl) {
10859
- console.log(import_chalk19.default.blue(` [${repoName}] Extracting DSL...`));
11290
+ console.log(import_chalk21.default.blue(` [${repoName}] Extracting DSL...`));
10860
11291
  try {
10861
11292
  const dslExtractor = new DslExtractor(specProvider);
10862
11293
  const repoIsFrontend = isFrontendDeps(context.dependencies);
10863
11294
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
10864
11295
  if (extractedDsl) {
10865
- console.log(import_chalk19.default.green(` DSL extracted.`));
11296
+ console.log(import_chalk21.default.green(` DSL extracted.`));
10866
11297
  }
10867
11298
  } catch (err) {
10868
- console.log(import_chalk19.default.yellow(` DSL extraction failed: ${err.message}`));
11299
+ console.log(import_chalk21.default.yellow(` DSL extraction failed: ${err.message}`));
10869
11300
  }
10870
11301
  }
10871
11302
  const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
10872
11303
  const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
10873
11304
  let workingDir = repoAbsPath;
10874
11305
  if (!skipWorktreeForRepo) {
10875
- console.log(import_chalk19.default.blue(` [${repoName}] Setting up git worktree...`));
11306
+ console.log(import_chalk21.default.blue(` [${repoName}] Setting up git worktree...`));
10876
11307
  try {
10877
11308
  const worktreeManager = new GitWorktreeManager(repoAbsPath);
10878
11309
  const worktreePath = await worktreeManager.createWorktree(idea);
10879
11310
  if (worktreePath) workingDir = worktreePath;
10880
11311
  } catch (err) {
10881
- console.log(import_chalk19.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.`));
10882
11313
  }
10883
11314
  } else {
10884
- console.log(import_chalk19.default.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
11315
+ console.log(import_chalk21.default.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
10885
11316
  }
10886
- const specsDir = path22.join(workingDir, "specs");
10887
- await fs23.ensureDir(specsDir);
11317
+ const specsDir = path23.join(workingDir, "specs");
11318
+ await fs24.ensureDir(specsDir);
10888
11319
  const featureSlug = slugify(idea);
10889
11320
  const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
10890
- await fs23.writeFile(specFile, finalSpec, "utf-8");
10891
- console.log(import_chalk19.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)}`));
10892
11323
  let savedDslFile = null;
10893
11324
  if (extractedDsl) {
10894
11325
  const dslExtractorForSave = new DslExtractor(specProvider);
10895
11326
  savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
10896
- console.log(import_chalk19.default.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
11327
+ console.log(import_chalk21.default.green(` DSL saved: ${path23.relative(repoAbsPath, savedDslFile)}`));
10897
11328
  }
10898
- console.log(import_chalk19.default.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
11329
+ console.log(import_chalk21.default.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
10899
11330
  try {
10900
11331
  const codegen = new CodeGenerator(codegenProvider, codegenMode);
10901
11332
  await codegen.generateCode(specFile, workingDir, context, {
@@ -10903,29 +11334,29 @@ ${contractContextSection}`;
10903
11334
  dslFilePath: savedDslFile ?? void 0,
10904
11335
  repoType: detectedRepoType
10905
11336
  });
10906
- console.log(import_chalk19.default.green(` Code generation complete.`));
11337
+ console.log(import_chalk21.default.green(` Code generation complete.`));
10907
11338
  } catch (err) {
10908
- console.log(import_chalk19.default.yellow(` Code generation failed: ${err.message}`));
11339
+ console.log(import_chalk21.default.yellow(` Code generation failed: ${err.message}`));
10909
11340
  }
10910
11341
  if (!cliOpts.skipTests && extractedDsl) {
10911
- console.log(import_chalk19.default.blue(` [${repoName}] Generating test skeletons...`));
11342
+ console.log(import_chalk21.default.blue(` [${repoName}] Generating test skeletons...`));
10912
11343
  try {
10913
11344
  const testGen = new TestGenerator(codegenProvider);
10914
11345
  const testFiles = await testGen.generate(extractedDsl, workingDir);
10915
- console.log(import_chalk19.default.green(` ${testFiles.length} test file(s) generated.`));
11346
+ console.log(import_chalk21.default.green(` ${testFiles.length} test file(s) generated.`));
10916
11347
  } catch (err) {
10917
- console.log(import_chalk19.default.yellow(` Test generation failed: ${err.message}`));
11348
+ console.log(import_chalk21.default.yellow(` Test generation failed: ${err.message}`));
10918
11349
  }
10919
11350
  }
10920
11351
  if (!cliOpts.skipErrorFeedback) {
10921
11352
  try {
10922
11353
  await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
10923
11354
  } catch (err) {
10924
- console.log(import_chalk19.default.yellow(` Error feedback failed: ${err.message}`));
11355
+ console.log(import_chalk21.default.yellow(` Error feedback failed: ${err.message}`));
10925
11356
  }
10926
11357
  }
10927
11358
  if (!cliOpts.skipReview) {
10928
- console.log(import_chalk19.default.blue(` [${repoName}] Running code review...`));
11359
+ console.log(import_chalk21.default.blue(` [${repoName}] Running code review...`));
10929
11360
  try {
10930
11361
  const reviewer = new CodeReviewer(specProvider);
10931
11362
  const originalDir = process.cwd();
@@ -10937,9 +11368,9 @@ ${contractContextSection}`;
10937
11368
  process.chdir(originalDir);
10938
11369
  }
10939
11370
  await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
10940
- console.log(import_chalk19.default.green(` Code review complete.`));
11371
+ console.log(import_chalk21.default.green(` Code review complete.`));
10941
11372
  } catch (err) {
10942
- console.log(import_chalk19.default.yellow(` Code review failed: ${err.message}`));
11373
+ console.log(import_chalk21.default.yellow(` Code review failed: ${err.message}`));
10943
11374
  }
10944
11375
  }
10945
11376
  return { dsl: extractedDsl, specFile };
@@ -10962,7 +11393,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10962
11393
  codegenModel: codegenModelName
10963
11394
  });
10964
11395
  const workspaceLoader = new WorkspaceLoader(currentDir);
10965
- console.log(import_chalk19.default.blue("\n[W1] Loading per-repo contexts..."));
11396
+ console.log(import_chalk21.default.blue("\n[W1] Loading per-repo contexts..."));
10966
11397
  const contexts = /* @__PURE__ */ new Map();
10967
11398
  const frontendContexts = /* @__PURE__ */ new Map();
10968
11399
  for (const repo of workspace.repos) {
@@ -10974,27 +11405,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10974
11405
  if (repo.role === "frontend" || repo.role === "mobile") {
10975
11406
  const fctx = await loadFrontendContext(repoAbsPath);
10976
11407
  frontendContexts.set(repo.name, fctx);
10977
- console.log(import_chalk19.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}`));
10978
11409
  } else {
10979
- console.log(import_chalk19.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)`));
10980
11411
  }
10981
11412
  } catch (err) {
10982
- console.log(import_chalk19.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}`));
10983
11414
  }
10984
11415
  }
10985
- console.log(import_chalk19.default.blue("\n[W2] Decomposing requirement across repos..."));
11416
+ console.log(import_chalk21.default.blue("\n[W2] Decomposing requirement across repos..."));
10986
11417
  const decomposer = new RequirementDecomposer(specProvider);
10987
11418
  let decomposition;
10988
11419
  try {
10989
11420
  decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
10990
- console.log(import_chalk19.default.green(` Summary: ${decomposition.summary}`));
10991
- console.log(import_chalk19.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(", ")}`));
10992
11423
  if (decomposition.coordinationNotes) {
10993
- console.log(import_chalk19.default.gray(` Coordination: ${decomposition.coordinationNotes}`));
11424
+ console.log(import_chalk21.default.gray(` Coordination: ${decomposition.coordinationNotes}`));
10994
11425
  }
10995
11426
  } catch (err) {
10996
- console.error(import_chalk19.default.red(` Decomposition failed: ${err.message}`));
10997
- console.log(import_chalk19.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."));
10998
11429
  decomposition = {
10999
11430
  originalRequirement: idea,
11000
11431
  summary: idea,
@@ -11010,11 +11441,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11010
11441
  };
11011
11442
  }
11012
11443
  if (!opts.auto) {
11013
- console.log(import_chalk19.default.cyan("\n[W3] Decomposition Preview:"));
11014
- console.log(import_chalk19.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)));
11015
11446
  for (const r of decomposition.repos) {
11016
- console.log(import_chalk19.default.bold(` ${r.repoName} (${r.role})`));
11017
- console.log(import_chalk19.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 ? "..." : ""}`));
11018
11449
  if (r.uxDecisions) {
11019
11450
  const ux = r.uxDecisions;
11020
11451
  const uxSummary = [
@@ -11023,13 +11454,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11023
11454
  ux.optimisticUpdate ? "optimistic-update" : "",
11024
11455
  ux.errorRollback ? "rollback" : ""
11025
11456
  ].filter(Boolean).join(", ");
11026
- if (uxSummary) console.log(import_chalk19.default.cyan(` UX: ${uxSummary}`));
11457
+ if (uxSummary) console.log(import_chalk21.default.cyan(` UX: ${uxSummary}`));
11027
11458
  }
11028
11459
  if (r.dependsOnRepos.length > 0) {
11029
- console.log(import_chalk19.default.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
11460
+ console.log(import_chalk21.default.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
11030
11461
  }
11031
11462
  }
11032
- console.log(import_chalk19.default.cyan("\u2500".repeat(52)));
11463
+ console.log(import_chalk21.default.cyan("\u2500".repeat(52)));
11033
11464
  const gate = await (0, import_prompts3.select)({
11034
11465
  message: "Proceed with multi-repo pipeline?",
11035
11466
  choices: [
@@ -11038,24 +11469,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11038
11469
  ]
11039
11470
  });
11040
11471
  if (gate === "abort") {
11041
- console.log(import_chalk19.default.yellow(" Aborted."));
11472
+ console.log(import_chalk21.default.yellow(" Aborted."));
11042
11473
  process.exit(0);
11043
11474
  }
11044
11475
  }
11045
11476
  const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
11046
11477
  const contractDsls = /* @__PURE__ */ new Map();
11047
- console.log(import_chalk19.default.blue(`
11478
+ console.log(import_chalk21.default.blue(`
11048
11479
  [W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
11049
11480
  const results = [];
11050
11481
  for (const repoReq of sortedRepoRequirements) {
11051
11482
  const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
11052
11483
  if (!repoConfig) {
11053
- console.log(import_chalk19.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.`));
11054
11485
  results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
11055
11486
  continue;
11056
11487
  }
11057
11488
  const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
11058
- console.log(import_chalk19.default.bold.blue(`
11489
+ console.log(import_chalk21.default.bold.blue(`
11059
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`));
11060
11491
  let contractContextSection;
11061
11492
  if (repoReq.dependsOnRepos.length > 0) {
@@ -11063,7 +11494,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11063
11494
  for (const depName of repoReq.dependsOnRepos) {
11064
11495
  const depDsl = contractDsls.get(depName);
11065
11496
  if (depDsl) {
11066
- console.log(import_chalk19.default.gray(` Using API contract from: ${depName}`));
11497
+ console.log(import_chalk21.default.gray(` Using API contract from: ${depName}`));
11067
11498
  const contract = buildFrontendApiContract(depDsl);
11068
11499
  contractParts.push(buildContractContextSection(contract));
11069
11500
  }
@@ -11083,7 +11514,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11083
11514
  frontendContext: frontendCtx
11084
11515
  });
11085
11516
  contractContextSection = void 0;
11086
- console.log(import_chalk19.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}`));
11087
11518
  }
11088
11519
  try {
11089
11520
  const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
@@ -11100,22 +11531,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11100
11531
  });
11101
11532
  if (repoReq.isContractProvider && dsl) {
11102
11533
  contractDsls.set(repoReq.repoName, dsl);
11103
- console.log(import_chalk19.default.green(` Contract stored for downstream repos.`));
11534
+ console.log(import_chalk21.default.green(` Contract stored for downstream repos.`));
11104
11535
  }
11105
11536
  results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
11106
- console.log(import_chalk19.default.green(` \u2714 ${repoReq.repoName} complete`));
11537
+ console.log(import_chalk21.default.green(` \u2714 ${repoReq.repoName} complete`));
11107
11538
  } catch (err) {
11108
- console.error(import_chalk19.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
11539
+ console.error(import_chalk21.default.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
11109
11540
  results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
11110
11541
  }
11111
11542
  }
11112
- console.log(import_chalk19.default.bold.green("\n\u2714 Multi-repo pipeline complete!"));
11113
- console.log(import_chalk19.default.gray(` Workspace: ${workspace.name}`));
11114
- console.log(import_chalk19.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}`));
11115
11546
  console.log();
11116
11547
  for (const r of results) {
11117
- const icon = r.status === "success" ? import_chalk19.default.green("\u2714") : r.status === "failed" ? import_chalk19.default.red("\u2718") : import_chalk19.default.gray("\u2212");
11118
- const specInfo = r.specFile ? import_chalk19.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}`) : "";
11119
11550
  console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
11120
11551
  }
11121
11552
  return results;
@@ -11123,18 +11554,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11123
11554
  var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
11124
11555
  workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
11125
11556
  const currentDir = process.cwd();
11126
- const configPath = path22.join(currentDir, WORKSPACE_CONFIG_FILE);
11127
- if (await fs23.pathExists(configPath)) {
11557
+ const configPath = path23.join(currentDir, WORKSPACE_CONFIG_FILE);
11558
+ if (await fs24.pathExists(configPath)) {
11128
11559
  const overwrite = await (0, import_prompts3.confirm)({
11129
11560
  message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
11130
11561
  default: false
11131
11562
  });
11132
11563
  if (!overwrite) {
11133
- console.log(import_chalk19.default.gray(" Cancelled."));
11564
+ console.log(import_chalk21.default.gray(" Cancelled."));
11134
11565
  return;
11135
11566
  }
11136
11567
  }
11137
- console.log(import_chalk19.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"));
11138
11569
  const workspaceName = await (0, import_prompts3.input)({
11139
11570
  message: "Workspace name:",
11140
11571
  validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
@@ -11148,11 +11579,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11148
11579
  const workspaceLoader = new WorkspaceLoader(currentDir);
11149
11580
  const detected = await workspaceLoader.autoDetect();
11150
11581
  if (detected.length === 0) {
11151
- console.log(import_chalk19.default.yellow(" No recognizable repos found in sibling directories."));
11582
+ console.log(import_chalk21.default.yellow(" No recognizable repos found in sibling directories."));
11152
11583
  } else {
11153
- console.log(import_chalk19.default.cyan("\n Detected repos:"));
11584
+ console.log(import_chalk21.default.cyan("\n Detected repos:"));
11154
11585
  for (const r of detected) {
11155
- console.log(import_chalk19.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}`));
11156
11587
  }
11157
11588
  const keepAll = await (0, import_prompts3.confirm)({
11158
11589
  message: `Include all ${detected.length} detected repo(s)?`,
@@ -11169,7 +11600,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11169
11600
  if (keep) repos.push(r);
11170
11601
  }
11171
11602
  }
11172
- console.log(import_chalk19.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.`));
11173
11604
  }
11174
11605
  }
11175
11606
  const repoTypeChoices = [
@@ -11191,7 +11622,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11191
11622
  default: repos.length === 0
11192
11623
  });
11193
11624
  while (addMore) {
11194
- console.log(import_chalk19.default.cyan(`
11625
+ console.log(import_chalk21.default.cyan(`
11195
11626
  Adding repo #${repos.length + 1}`));
11196
11627
  const repoName = await (0, import_prompts3.input)({
11197
11628
  message: "Repo name (e.g. api, web, app):",
@@ -11205,16 +11636,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11205
11636
  message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
11206
11637
  default: `./${repoName}`
11207
11638
  });
11208
- const absPath = path22.resolve(currentDir, repoPath);
11639
+ const absPath = path23.resolve(currentDir, repoPath);
11209
11640
  let detectedType = "unknown";
11210
11641
  let detectedRole = "shared";
11211
- if (await fs23.pathExists(absPath)) {
11642
+ if (await fs24.pathExists(absPath)) {
11212
11643
  const { type, role } = await detectRepoType(absPath);
11213
11644
  detectedType = type;
11214
11645
  detectedRole = role;
11215
- console.log(import_chalk19.default.gray(` Auto-detected: type=${type}, role=${role}`));
11646
+ console.log(import_chalk21.default.gray(` Auto-detected: type=${type}, role=${role}`));
11216
11647
  } else {
11217
- console.log(import_chalk19.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.`));
11218
11649
  }
11219
11650
  const repoType = await (0, import_prompts3.select)({
11220
11651
  message: `Repo type for "${repoName}":`,
@@ -11237,53 +11668,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11237
11668
  type: repoType,
11238
11669
  role: repoRole
11239
11670
  });
11240
- console.log(import_chalk19.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
11671
+ console.log(import_chalk21.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
11241
11672
  addMore = await (0, import_prompts3.confirm)({
11242
11673
  message: "Add another repo?",
11243
11674
  default: false
11244
11675
  });
11245
11676
  }
11246
11677
  const workspaceConfig = { name: workspaceName, repos };
11247
- console.log(import_chalk19.default.cyan("\n Workspace summary:"));
11248
- console.log(import_chalk19.default.gray(` Name: ${workspaceName}`));
11678
+ console.log(import_chalk21.default.cyan("\n Workspace summary:"));
11679
+ console.log(import_chalk21.default.gray(` Name: ${workspaceName}`));
11249
11680
  for (const r of repos) {
11250
- console.log(import_chalk19.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}`));
11251
11682
  }
11252
11683
  const ok = await (0, import_prompts3.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
11253
11684
  if (!ok) {
11254
- console.log(import_chalk19.default.gray(" Cancelled."));
11685
+ console.log(import_chalk21.default.gray(" Cancelled."));
11255
11686
  return;
11256
11687
  }
11257
11688
  const loader = new WorkspaceLoader(currentDir);
11258
11689
  const saved = await loader.save(workspaceConfig);
11259
- console.log(import_chalk19.default.green(`
11690
+ console.log(import_chalk21.default.green(`
11260
11691
  \u2714 Workspace saved: ${saved}`));
11261
- console.log(import_chalk19.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.`));
11262
11693
  });
11263
11694
  workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
11264
11695
  const currentDir = process.cwd();
11265
11696
  const loader = new WorkspaceLoader(currentDir);
11266
11697
  const config2 = await loader.load();
11267
11698
  if (!config2) {
11268
- console.log(import_chalk19.default.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
11269
- console.log(import_chalk19.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."));
11270
11701
  return;
11271
11702
  }
11272
- console.log(import_chalk19.default.bold(`
11703
+ console.log(import_chalk21.default.bold(`
11273
11704
  Workspace: ${config2.name}`));
11274
- console.log(import_chalk19.default.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
11275
- console.log(import_chalk19.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}):
11276
11707
  `));
11277
11708
  for (const repo of config2.repos) {
11278
11709
  const absPath = loader.resolveAbsPath(repo);
11279
- const exists = await fs23.pathExists(absPath);
11280
- const status = exists ? import_chalk19.default.green("found") : import_chalk19.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");
11281
11712
  console.log(
11282
- ` ${import_chalk19.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}`
11283
11714
  );
11284
- console.log(import_chalk19.default.gray(` path: ${absPath}`));
11715
+ console.log(import_chalk21.default.gray(` path: ${absPath}`));
11285
11716
  if (repo.constitution) {
11286
- console.log(import_chalk19.default.green(` constitution: found`));
11717
+ console.log(import_chalk21.default.green(` constitution: found`));
11287
11718
  }
11288
11719
  }
11289
11720
  });
@@ -11300,30 +11731,30 @@ program.command("update").description("Update an existing spec with a change req
11300
11731
  const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
11301
11732
  const apiKey = await resolveApiKey(providerName, opts.key);
11302
11733
  const provider = createProvider(providerName, apiKey, modelName);
11303
- console.log(import_chalk19.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"));
11304
- console.log(import_chalk19.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}`));
11305
11736
  const updateRunId = generateRunId();
11306
11737
  const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
11307
11738
  setActiveSnapshot(updateSnapshot);
11308
11739
  const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
11309
11740
  setActiveLogger(updateLogger);
11310
- console.log(import_chalk19.default.gray(` Run ID: ${updateRunId}`));
11741
+ console.log(import_chalk21.default.gray(` Run ID: ${updateRunId}`));
11311
11742
  let specPath = opts.spec ?? null;
11312
11743
  if (!specPath) {
11313
- const specsDir = path22.join(currentDir, "specs");
11744
+ const specsDir = path23.join(currentDir, "specs");
11314
11745
  const latest = await SpecUpdater.findLatestSpec(specsDir);
11315
11746
  if (!latest) {
11316
- console.error(import_chalk19.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>."));
11317
11748
  process.exit(1);
11318
11749
  }
11319
11750
  specPath = latest.filePath;
11320
- console.log(import_chalk19.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})`));
11321
11752
  }
11322
- console.log(import_chalk19.default.gray(" Loading project context..."));
11753
+ console.log(import_chalk21.default.gray(" Loading project context..."));
11323
11754
  const loader = new ContextLoader(currentDir);
11324
11755
  const context = await loader.loadProjectContext();
11325
11756
  if (context.constitution && context.constitution.length > 6e3) {
11326
- console.log(import_chalk19.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`));
11327
11758
  }
11328
11759
  const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
11329
11760
  const { type: repoType } = await _detectRepoType(currentDir);
@@ -11335,19 +11766,19 @@ program.command("update").description("Update an existing spec with a change req
11335
11766
  repoType
11336
11767
  });
11337
11768
  } catch (err) {
11338
- console.error(import_chalk19.default.red(` Update failed: ${err.message}`));
11769
+ console.error(import_chalk21.default.red(` Update failed: ${err.message}`));
11339
11770
  process.exit(1);
11340
11771
  }
11341
- console.log(import_chalk19.default.green(`
11342
- \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)}`));
11343
11774
  if (result.newDslPath) {
11344
- console.log(import_chalk19.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)}`));
11345
11776
  }
11346
11777
  if (result.affectedFiles.length > 0) {
11347
- console.log(import_chalk19.default.cyan("\n Affected files:"));
11778
+ console.log(import_chalk21.default.cyan("\n Affected files:"));
11348
11779
  for (const f of result.affectedFiles) {
11349
- const icon = f.action === "create" ? import_chalk19.default.green("+") : import_chalk19.default.yellow("~");
11350
- console.log(` ${icon} ${f.file}: ${import_chalk19.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)}`);
11351
11782
  }
11352
11783
  }
11353
11784
  if (opts.codegen && result.affectedFiles.length > 0) {
@@ -11355,9 +11786,9 @@ program.command("update").description("Update an existing spec with a change req
11355
11786
  const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
11356
11787
  const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
11357
11788
  const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
11358
- console.log(import_chalk19.default.blue("\n Regenerating affected files..."));
11789
+ console.log(import_chalk21.default.blue("\n Regenerating affected files..."));
11359
11790
  const codeGenerator = new CodeGenerator(codegenProvider, "api");
11360
- const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
11791
+ const specContent = await fs24.readFile(result.newSpecPath, "utf-8");
11361
11792
  const constitutionSection = context.constitution ? `
11362
11793
  === Project Constitution (MUST follow) ===
11363
11794
  ${context.constitution}
@@ -11368,10 +11799,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
11368
11799
  ` : "";
11369
11800
  updateLogger.stageStart("update_codegen");
11370
11801
  for (const affected of result.affectedFiles) {
11371
- const fullPath = path22.join(currentDir, affected.file);
11802
+ const fullPath = path23.join(currentDir, affected.file);
11372
11803
  let existing = "";
11373
11804
  try {
11374
- existing = await fs23.readFile(fullPath, "utf-8");
11805
+ existing = await fs24.readFile(fullPath, "utf-8");
11375
11806
  } catch {
11376
11807
  }
11377
11808
  const codePrompt = `Apply this change to the file.
@@ -11385,23 +11816,23 @@ ${specContent}
11385
11816
  ${constitutionSection}${dslSection}
11386
11817
  === ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
11387
11818
  ${existing || "Create from scratch."}`;
11388
- process.stdout.write(` ${existing ? import_chalk19.default.yellow("~") : import_chalk19.default.green("+")} ${affected.file}... `);
11819
+ process.stdout.write(` ${existing ? import_chalk21.default.yellow("~") : import_chalk21.default.green("+")} ${affected.file}... `);
11389
11820
  try {
11390
11821
  const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
11391
11822
  const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
11392
11823
  const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
11393
- await fs23.ensureDir(path22.dirname(fullPath));
11824
+ await fs24.ensureDir(path23.dirname(fullPath));
11394
11825
  await updateSnapshot.snapshotFile(fullPath);
11395
- await fs23.writeFile(fullPath, content, "utf-8");
11826
+ await fs24.writeFile(fullPath, content, "utf-8");
11396
11827
  updateLogger.fileWritten(affected.file);
11397
- console.log(import_chalk19.default.green("\u2714"));
11828
+ console.log(import_chalk21.default.green("\u2714"));
11398
11829
  } catch (err) {
11399
11830
  updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
11400
- console.log(import_chalk19.default.red(`\u2718 ${err.message}`));
11831
+ console.log(import_chalk21.default.red(`\u2718 ${err.message}`));
11401
11832
  }
11402
11833
  }
11403
11834
  updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
11404
- const updatedSpecContent = await fs23.readFile(result.newSpecPath, "utf-8").catch(() => "");
11835
+ const updatedSpecContent = await fs24.readFile(result.newSpecPath, "utf-8").catch(() => "");
11405
11836
  if (updatedSpecContent) {
11406
11837
  const updateReviewer = new CodeReviewer(provider, currentDir);
11407
11838
  const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
@@ -11413,13 +11844,13 @@ ${existing || "Create from scratch."}`;
11413
11844
  updateLogger.finish();
11414
11845
  updateLogger.printSummary();
11415
11846
  if (updateSnapshot.fileCount > 0) {
11416
- console.log(import_chalk19.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
11847
+ console.log(import_chalk21.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
11417
11848
  }
11418
11849
  if (!opts.codegen && result.affectedFiles.length > 0) {
11419
- console.log(import_chalk19.default.blue("\n Next steps:"));
11420
- console.log(import_chalk19.default.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
11421
- console.log(import_chalk19.default.gray(` \u2022 Or update files manually based on the affected files list above`));
11422
- console.log(import_chalk19.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`));
11423
11854
  }
11424
11855
  });
11425
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) => {
@@ -11428,19 +11859,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
11428
11859
  if (!dslPath) {
11429
11860
  dslPath = await findLatestDslFile(currentDir);
11430
11861
  if (!dslPath) {
11431
- console.error(import_chalk19.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>."));
11432
11863
  process.exit(1);
11433
11864
  }
11434
- console.log(import_chalk19.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
11865
+ console.log(import_chalk21.default.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
11435
11866
  }
11436
11867
  let dsl;
11437
11868
  try {
11438
- dsl = await fs23.readJson(dslPath);
11869
+ dsl = await fs24.readJson(dslPath);
11439
11870
  } catch (err) {
11440
- console.error(import_chalk19.default.red(` Failed to read DSL: ${err.message}`));
11871
+ console.error(import_chalk21.default.red(` Failed to read DSL: ${err.message}`));
11441
11872
  process.exit(1);
11442
11873
  }
11443
- console.log(import_chalk19.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"));
11444
11875
  const format = opts.format === "json" ? "json" : "yaml";
11445
11876
  const serverUrl = opts.server || "http://localhost:3000";
11446
11877
  try {
@@ -11449,31 +11880,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
11449
11880
  serverUrl,
11450
11881
  outputPath: opts.output
11451
11882
  });
11452
- const rel = path22.relative(currentDir, outputPath);
11453
- console.log(import_chalk19.default.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
11454
- console.log(import_chalk19.default.gray(` Feature : ${dsl.feature.title}`));
11455
- console.log(import_chalk19.default.gray(` Endpoints: ${dsl.endpoints.length}`));
11456
- console.log(import_chalk19.default.gray(` Models : ${dsl.models.length}`));
11457
- console.log(import_chalk19.default.gray(` Server : ${serverUrl}`));
11458
- console.log(import_chalk19.default.blue("\n Next steps:"));
11459
- console.log(import_chalk19.default.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
11460
- console.log(import_chalk19.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`));
11461
11892
  } catch (err) {
11462
- console.error(import_chalk19.default.red(` Export failed: ${err.message}`));
11893
+ console.error(import_chalk21.default.red(` Export failed: ${err.message}`));
11463
11894
  process.exit(1);
11464
11895
  }
11465
11896
  });
11466
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) => {
11467
11898
  const currentDir = process.cwd();
11468
11899
  const port = parseInt(opts.port, 10) || 3001;
11469
- console.log(import_chalk19.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"));
11470
11901
  if (opts.restore) {
11471
- const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
11902
+ const frontendDir = opts.frontend ? path23.resolve(opts.frontend) : currentDir;
11472
11903
  const r = await restoreMockProxy(frontendDir);
11473
11904
  if (r.restored) {
11474
- console.log(import_chalk19.default.green(" \u2714 Proxy restored and mock server stopped."));
11905
+ console.log(import_chalk21.default.green(" \u2714 Proxy restored and mock server stopped."));
11475
11906
  } else {
11476
- console.log(import_chalk19.default.yellow(` ${r.note ?? "Nothing to restore."}`));
11907
+ console.log(import_chalk21.default.yellow(` ${r.note ?? "Nothing to restore."}`));
11477
11908
  }
11478
11909
  return;
11479
11910
  }
@@ -11481,32 +11912,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11481
11912
  const workspaceLoader = new WorkspaceLoader(currentDir);
11482
11913
  const workspaceConfig = await workspaceLoader.load();
11483
11914
  if (!workspaceConfig) {
11484
- console.error(import_chalk19.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.`));
11485
11916
  process.exit(1);
11486
11917
  }
11487
11918
  const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
11488
11919
  if (backendRepos.length === 0) {
11489
- console.log(import_chalk19.default.yellow(" No backend repos found in workspace."));
11920
+ console.log(import_chalk21.default.yellow(" No backend repos found in workspace."));
11490
11921
  return;
11491
11922
  }
11492
11923
  for (const repo of backendRepos) {
11493
11924
  const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
11494
- console.log(import_chalk19.default.cyan(`
11925
+ console.log(import_chalk21.default.cyan(`
11495
11926
  Repo: ${repo.name} (${repoAbsPath})`));
11496
11927
  const dslFile = await findLatestDslFile(repoAbsPath);
11497
11928
  if (!dslFile) {
11498
- console.log(import_chalk19.default.yellow(` No DSL file found \u2014 skipping.`));
11929
+ console.log(import_chalk21.default.yellow(` No DSL file found \u2014 skipping.`));
11499
11930
  continue;
11500
11931
  }
11501
- const dsl2 = await fs23.readJson(dslFile);
11932
+ const dsl2 = await fs24.readJson(dslFile);
11502
11933
  const result2 = await generateMockAssets(dsl2, repoAbsPath, {
11503
11934
  port,
11504
11935
  msw: opts.msw,
11505
11936
  proxy: opts.proxy
11506
11937
  });
11507
11938
  for (const f of result2.files) {
11508
- console.log(import_chalk19.default.green(` \u2714 ${f.path}`));
11509
- console.log(import_chalk19.default.gray(` ${f.description}`));
11939
+ console.log(import_chalk21.default.green(` \u2714 ${f.path}`));
11940
+ console.log(import_chalk21.default.gray(` ${f.description}`));
11510
11941
  }
11511
11942
  }
11512
11943
  return;
@@ -11516,19 +11947,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11516
11947
  dslPath = await findLatestDslFile(currentDir);
11517
11948
  if (!dslPath) {
11518
11949
  console.error(
11519
- import_chalk19.default.red(
11950
+ import_chalk21.default.red(
11520
11951
  " No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
11521
11952
  )
11522
11953
  );
11523
11954
  process.exit(1);
11524
11955
  }
11525
- console.log(import_chalk19.default.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
11956
+ console.log(import_chalk21.default.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
11526
11957
  }
11527
11958
  let dsl;
11528
11959
  try {
11529
- dsl = await fs23.readJson(dslPath);
11960
+ dsl = await fs24.readJson(dslPath);
11530
11961
  } catch (err) {
11531
- console.error(import_chalk19.default.red(` Failed to read DSL file: ${err.message}`));
11962
+ console.error(import_chalk21.default.red(` Failed to read DSL file: ${err.message}`));
11532
11963
  process.exit(1);
11533
11964
  }
11534
11965
  const result = await generateMockAssets(dsl, currentDir, {
@@ -11536,58 +11967,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11536
11967
  msw: opts.msw,
11537
11968
  proxy: opts.proxy
11538
11969
  });
11539
- console.log(import_chalk19.default.green(`
11970
+ console.log(import_chalk21.default.green(`
11540
11971
  \u2714 Mock assets generated (${result.files.length} file(s)):`));
11541
11972
  for (const f of result.files) {
11542
- console.log(import_chalk19.default.green(` ${f.path}`));
11543
- console.log(import_chalk19.default.gray(` ${f.description}`));
11973
+ console.log(import_chalk21.default.green(` ${f.path}`));
11974
+ console.log(import_chalk21.default.gray(` ${f.description}`));
11544
11975
  }
11545
11976
  if (opts.serve) {
11546
- const serverJsPath = path22.join(currentDir, "mock", "server.js");
11547
- if (!await fs23.pathExists(serverJsPath)) {
11548
- console.error(import_chalk19.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."));
11549
11980
  process.exit(1);
11550
11981
  }
11551
11982
  const pid = startMockServerBackground(serverJsPath, port);
11552
- console.log(import_chalk19.default.green(`
11983
+ console.log(import_chalk21.default.green(`
11553
11984
  \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
11554
11985
  if (opts.frontend) {
11555
- const frontendDir = path22.resolve(opts.frontend);
11986
+ const frontendDir = path23.resolve(opts.frontend);
11556
11987
  const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
11557
11988
  await saveMockServerPid(frontendDir, pid);
11558
11989
  if (proxyResult.applied) {
11559
- console.log(import_chalk19.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11560
- console.log(import_chalk19.default.bold.cyan(`
11990
+ console.log(import_chalk21.default.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11991
+ console.log(import_chalk21.default.bold.cyan(`
11561
11992
  Ready! Open a new terminal and run:`));
11562
- console.log(import_chalk19.default.white(` cd ${frontendDir}`));
11563
- console.log(import_chalk19.default.white(` ${proxyResult.devCommand}`));
11564
- console.log(import_chalk19.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(`
11565
11996
  When done: ai-spec mock --restore --frontend ${frontendDir}`));
11566
11997
  } else {
11567
- console.log(import_chalk19.default.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11568
- if (proxyResult.note) console.log(import_chalk19.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}`));
11569
12000
  }
11570
12001
  } else {
11571
- console.log(import_chalk19.default.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
11572
- console.log(import_chalk19.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}`));
11573
12004
  }
11574
12005
  return;
11575
12006
  }
11576
- console.log(import_chalk19.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"));
11577
- console.log(import_chalk19.default.white(` 1. Install express (if not already):`));
11578
- console.log(import_chalk19.default.gray(` npm install --save-dev express`));
11579
- console.log(import_chalk19.default.white(` 2. Start mock server:`));
11580
- console.log(import_chalk19.default.gray(` node mock/server.js`));
11581
- console.log(import_chalk19.default.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
11582
- console.log(import_chalk19.default.white(` 3. Configure your frontend to proxy API calls to:`));
11583
- console.log(import_chalk19.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}`));
11584
12015
  if (opts.proxy) {
11585
- console.log(import_chalk19.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)`));
11586
12017
  }
11587
12018
  if (opts.msw) {
11588
- console.log(import_chalk19.default.white(` 4. MSW: import and start the worker in your app entry:`));
11589
- console.log(import_chalk19.default.gray(` import { worker } from './mocks/browser';`));
11590
- console.log(import_chalk19.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();`));
11591
12022
  }
11592
12023
  });
11593
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) => {
@@ -11603,26 +12034,118 @@ program.command("learn").description("Append a lesson or engineering decision di
11603
12034
  }
11604
12035
  const result = await appendDirectLesson(currentDir, lesson.trim());
11605
12036
  if (result.appended) {
11606
- console.log(import_chalk19.default.green(`
12037
+ console.log(import_chalk21.default.green(`
11607
12038
  \u2714 Lesson appended to constitution \xA79`));
11608
- console.log(import_chalk19.default.gray(` File: .ai-spec-constitution.md`));
12039
+ console.log(import_chalk21.default.gray(` File: .ai-spec-constitution.md`));
11609
12040
  } else {
11610
- console.log(import_chalk19.default.yellow(`
12041
+ console.log(import_chalk21.default.yellow(`
11611
12042
  \u26A0 Not appended: ${result.reason}`));
11612
12043
  }
11613
12044
  });
11614
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) => {
11615
12046
  const currentDir = process.cwd();
11616
12047
  const snapshot = new RunSnapshot(currentDir, runId);
11617
- console.log(import_chalk19.default.blue(`Restoring run: ${runId}...`));
12048
+ console.log(import_chalk21.default.blue(`Restoring run: ${runId}...`));
11618
12049
  const restored = await snapshot.restore();
11619
12050
  if (restored.length === 0) {
11620
- console.log(import_chalk19.default.yellow(" No backup found for this run ID."));
12051
+ console.log(import_chalk21.default.yellow(" No backup found for this run ID."));
11621
12052
  } else {
11622
- restored.forEach((f) => console.log(import_chalk19.default.green(` \u2714 restored: ${f}`)));
11623
- console.log(import_chalk19.default.bold.green(`
12053
+ restored.forEach((f) => console.log(import_chalk21.default.green(` \u2714 restored: ${f}`)));
12054
+ console.log(import_chalk21.default.bold.green(`
11624
12055
  \u2714 ${restored.length} file(s) restored.`));
11625
12056
  }
11626
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
+ });
11627
12150
  program.parse();
11628
12151
  //# sourceMappingURL=index.js.map