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