ai-spec-dev 0.30.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 chalk18 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
  };
@@ -6106,6 +6106,16 @@ var RunLogger = class {
6106
6106
  this.log.errors.push(`[${event}] ${error}`);
6107
6107
  this.flush();
6108
6108
  }
6109
+ /** Record the prompt hash for this run (call once at run start). */
6110
+ setPromptHash(hash) {
6111
+ this.log.promptHash = hash;
6112
+ this.flush();
6113
+ }
6114
+ /** Record the harness self-eval score (call once at run end). */
6115
+ setHarnessScore(score) {
6116
+ this.log.harnessScore = score;
6117
+ this.flush();
6118
+ }
6109
6119
  fileWritten(filePath) {
6110
6120
  if (!this.log.filesWritten.includes(filePath)) {
6111
6121
  this.log.filesWritten.push(filePath);
@@ -9890,13 +9900,439 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
9890
9900
  return outputPath;
9891
9901
  }
9892
9902
 
9903
+ // core/prompt-hasher.ts
9904
+ init_codegen_prompt();
9905
+ init_codegen_prompt();
9906
+ import { createHash } from "crypto";
9907
+ function computePromptHash() {
9908
+ const segments = [
9909
+ codeGenSystemPrompt,
9910
+ dslSystemPrompt,
9911
+ specPrompt,
9912
+ reviewArchitectureSystemPrompt,
9913
+ reviewImplementationSystemPrompt,
9914
+ reviewImpactComplexitySystemPrompt
9915
+ ];
9916
+ return createHash("sha256").update(segments.join("\0")).digest("hex").slice(0, 8);
9917
+ }
9918
+
9919
+ // core/self-evaluator.ts
9920
+ import chalk18 from "chalk";
9921
+ var ENDPOINT_LAYER_PATTERNS = [
9922
+ /src\/api/,
9923
+ /src\/routes?/,
9924
+ /src\/controller/,
9925
+ /src\/handler/,
9926
+ /src\/endpoints?/
9927
+ ];
9928
+ var MODEL_LAYER_PATTERNS = [
9929
+ /src\/model/,
9930
+ /src\/schema/,
9931
+ /src\/entit/,
9932
+ /src\/db/,
9933
+ /prisma/,
9934
+ /src\/data/,
9935
+ /src\/domain/
9936
+ ];
9937
+ function extractReviewScore(reviewText) {
9938
+ const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
9939
+ return match ? parseFloat(match[1]) : null;
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
+ }
9952
+ function runSelfEval(opts) {
9953
+ const { dsl, generatedFiles, compilePassed, reviewText, promptHash, logger } = opts;
9954
+ const endpointsTotal = dsl?.endpoints?.length ?? 0;
9955
+ const modelsTotal = dsl?.models?.length ?? 0;
9956
+ const endpointLayerCovered = generatedFiles.some(
9957
+ (f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
9958
+ );
9959
+ const endpointLayerFiles = generatedFiles.filter(
9960
+ (f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
9961
+ ).length;
9962
+ const modelLayerCovered = generatedFiles.some(
9963
+ (f) => MODEL_LAYER_PATTERNS.some((p) => p.test(f))
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;
9977
+ let dslCoverageScore = 10;
9978
+ if (generatedFiles.length === 0) {
9979
+ dslCoverageScore = 0;
9980
+ } else {
9981
+ if (endpointsTotal > 0 && !endpointLayerCovered) dslCoverageScore -= 4;
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
+ }
9990
+ }
9991
+ dslCoverageScore = Math.max(0, Math.min(10, dslCoverageScore));
9992
+ const compileScore = compilePassed ? 10 : 5;
9993
+ const reviewScore = reviewText ? extractReviewScore(reviewText) : null;
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;
9995
+ const result = {
9996
+ dslCoverageScore,
9997
+ compileScore,
9998
+ reviewScore,
9999
+ harnessScore,
10000
+ promptHash,
10001
+ detail: {
10002
+ endpointsTotal,
10003
+ endpointLayerCovered,
10004
+ endpointLayerFiles,
10005
+ modelsTotal,
10006
+ modelLayerCovered,
10007
+ modelNameCoverage: Math.round(modelNameCoverage * 100) / 100,
10008
+ modelNameMatched,
10009
+ filesWritten: generatedFiles.length
10010
+ }
10011
+ };
10012
+ logger.setHarnessScore(harnessScore);
10013
+ logger.stageEnd("self_eval", {
10014
+ harnessScore,
10015
+ dslCoverageScore,
10016
+ compileScore,
10017
+ reviewScore: reviewScore ?? void 0,
10018
+ promptHash,
10019
+ modelNameCoverage: result.detail.modelNameCoverage,
10020
+ modelNameMatched: result.detail.modelNameMatched,
10021
+ endpointLayerFiles: result.detail.endpointLayerFiles
10022
+ });
10023
+ return result;
10024
+ }
10025
+ function printSelfEval(result) {
10026
+ const scoreColor2 = result.harnessScore >= 8 ? chalk18.green : result.harnessScore >= 6 ? chalk18.yellow : chalk18.red;
10027
+ const filled = Math.round(result.harnessScore);
10028
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
10029
+ const compileTag = result.compileScore === 10 ? chalk18.green("pass") : chalk18.yellow("partial");
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
+ }
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"));
10038
+ console.log(` Score : ${scoreColor2(`[${bar}] ${result.harnessScore}/10`)}`);
10039
+ console.log(
10040
+ ` DSL : ${scoreColor2(String(result.dslCoverageScore) + "/10")} Compile: ${compileTag} ${reviewTag}`
10041
+ );
10042
+ if (modelCoverageTag) {
10043
+ console.log(
10044
+ ` Detail : ${modelCoverageTag} ` + chalk18.gray(`Endpoints: ${result.detail.endpointsTotal} Files: ${result.detail.filesWritten}`)
10045
+ );
10046
+ }
10047
+ console.log(chalk18.gray(` Prompt : ${result.promptHash}`));
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."));
10327
+ }
10328
+
9893
10329
  // cli/index.ts
9894
10330
  dotenv.config();
9895
10331
  var CONFIG_FILE = ".ai-spec.json";
9896
10332
  async function loadConfig(dir) {
9897
- const p = path22.join(dir, CONFIG_FILE);
9898
- if (await fs23.pathExists(p)) {
9899
- return fs23.readJson(p);
10333
+ const p = path23.join(dir, CONFIG_FILE);
10334
+ if (await fs24.pathExists(p)) {
10335
+ return fs24.readJson(p);
9900
10336
  }
9901
10337
  return {};
9902
10338
  }
@@ -9921,20 +10357,20 @@ async function resolveApiKey(providerName, cliKey) {
9921
10357
  validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
9922
10358
  });
9923
10359
  await saveKey(providerName, newKey.trim());
9924
- console.log(chalk18.gray(` Key saved to ${KEY_STORE_FILE}`));
10360
+ console.log(chalk21.gray(` Key saved to ${KEY_STORE_FILE}`));
9925
10361
  return newKey.trim();
9926
10362
  }
9927
10363
  function printBanner(opts) {
9928
- console.log(chalk18.blue("\n" + "\u2500".repeat(52)));
9929
- console.log(chalk18.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
9930
- console.log(chalk18.blue("\u2500".repeat(52)));
9931
- console.log(chalk18.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}`));
9932
10368
  console.log(
9933
- chalk18.gray(
10369
+ chalk21.gray(
9934
10370
  ` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
9935
10371
  )
9936
10372
  );
9937
- console.log(chalk18.blue("\u2500".repeat(52) + "\n"));
10373
+ console.log(chalk21.blue("\u2500".repeat(52) + "\n"));
9938
10374
  }
9939
10375
  var program = new Command();
9940
10376
  program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
@@ -9961,42 +10397,42 @@ program.command("create").description("Generate a feature spec and kick off code
9961
10397
  const workspaceLoader = new WorkspaceLoader(currentDir);
9962
10398
  const workspaceConfig = await workspaceLoader.load();
9963
10399
  if (workspaceConfig) {
9964
- console.log(chalk18.cyan(`
10400
+ console.log(chalk21.cyan(`
9965
10401
  [Workspace] Detected workspace: ${workspaceConfig.name}`));
9966
- console.log(chalk18.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
10402
+ console.log(chalk21.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
9967
10403
  const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
9968
10404
  if (opts.serve) {
9969
- console.log(chalk18.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"));
9970
10406
  const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
9971
10407
  const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
9972
10408
  if (!backendResult) {
9973
- console.log(chalk18.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."));
9974
10410
  } else {
9975
10411
  const mockPort = 3001;
9976
10412
  const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
9977
- const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
9978
- console.log(chalk18.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))`));
9979
10415
  const pid = startMockServerBackground(serverJsPath, mockPort);
9980
- console.log(chalk18.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}`));
9981
10417
  if (frontendResult) {
9982
10418
  const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
9983
10419
  await saveMockServerPid(frontendResult.repoAbsPath, pid);
9984
10420
  if (proxyResult.applied) {
9985
- console.log(chalk18.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
9986
- console.log(chalk18.bold.cyan(`
10421
+ console.log(chalk21.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
10422
+ console.log(chalk21.bold.cyan(`
9987
10423
  Ready! Run your frontend dev server:`));
9988
- console.log(chalk18.white(` cd ${frontendResult.repoAbsPath}`));
9989
- console.log(chalk18.white(` ${proxyResult.devCommand}`));
9990
- console.log(chalk18.gray(`
10424
+ console.log(chalk21.white(` cd ${frontendResult.repoAbsPath}`));
10425
+ console.log(chalk21.white(` ${proxyResult.devCommand}`));
10426
+ console.log(chalk21.gray(`
9991
10427
  When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
9992
10428
  } else {
9993
- console.log(chalk18.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
9994
- if (proxyResult.note) console.log(chalk18.gray(` ${proxyResult.note}`));
9995
- console.log(chalk18.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}`));
9996
10432
  }
9997
10433
  } else {
9998
- console.log(chalk18.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
9999
- console.log(chalk18.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}`));
10000
10436
  }
10001
10437
  }
10002
10438
  }
@@ -10017,7 +10453,7 @@ program.command("create").description("Generate a feature spec and kick off code
10017
10453
  codegenModel: codegenModelName
10018
10454
  });
10019
10455
  const runId = generateRunId();
10020
- console.log(chalk18.gray(` Run ID: ${runId}`));
10456
+ console.log(chalk21.gray(` Run ID: ${runId}`));
10021
10457
  const runSnapshot = new RunSnapshot(currentDir, runId);
10022
10458
  setActiveSnapshot(runSnapshot);
10023
10459
  const runLogger = new RunLogger(currentDir, runId, {
@@ -10025,25 +10461,27 @@ program.command("create").description("Generate a feature spec and kick off code
10025
10461
  model: specModelName
10026
10462
  });
10027
10463
  setActiveLogger(runLogger);
10028
- console.log(chalk18.blue("[1/6] Loading project context..."));
10464
+ const promptHash = computePromptHash();
10465
+ runLogger.setPromptHash(promptHash);
10466
+ console.log(chalk21.blue("[1/6] Loading project context..."));
10029
10467
  runLogger.stageStart("context_load");
10030
10468
  const loader = new ContextLoader(currentDir);
10031
10469
  const context = await loader.loadProjectContext();
10032
10470
  const { type: detectedRepoType } = await detectRepoType(currentDir);
10033
10471
  runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
10034
- console.log(chalk18.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10035
- console.log(chalk18.gray(` Dependencies: ${context.dependencies.length} packages`));
10036
- console.log(chalk18.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`));
10037
10475
  if (context.schema) {
10038
- console.log(chalk18.gray(` Prisma schema: found`));
10476
+ console.log(chalk21.gray(` Prisma schema: found`));
10039
10477
  }
10040
10478
  if (context.constitution) {
10041
- console.log(chalk18.green(` Constitution : found (.ai-spec-constitution.md)`));
10479
+ console.log(chalk21.green(` Constitution : found (.ai-spec-constitution.md)`));
10042
10480
  if (context.constitution.length > 6e3) {
10043
- console.log(chalk18.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`));
10044
10482
  }
10045
10483
  } else {
10046
- console.log(chalk18.yellow(" Constitution : not found \u2014 auto-generating..."));
10484
+ console.log(chalk21.yellow(" Constitution : not found \u2014 auto-generating..."));
10047
10485
  try {
10048
10486
  const constitutionGen = new ConstitutionGenerator(
10049
10487
  createProvider(specProviderName, specApiKey, specModelName)
@@ -10051,12 +10489,12 @@ program.command("create").description("Generate a feature spec and kick off code
10051
10489
  const constitutionContent = await constitutionGen.generate(currentDir);
10052
10490
  await constitutionGen.saveConstitution(currentDir, constitutionContent);
10053
10491
  context.constitution = constitutionContent;
10054
- console.log(chalk18.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
10492
+ console.log(chalk21.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
10055
10493
  } catch (err) {
10056
- console.log(chalk18.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.`));
10057
10495
  }
10058
10496
  }
10059
- console.log(chalk18.blue(`
10497
+ console.log(chalk21.blue(`
10060
10498
  [2/6] Generating spec with ${specProviderName}/${specModelName}...`));
10061
10499
  const specProvider = createProvider(specProviderName, specApiKey, specModelName);
10062
10500
  let initialSpec;
@@ -10066,30 +10504,30 @@ program.command("create").description("Generate a feature spec and kick off code
10066
10504
  if (opts.skipTasks) {
10067
10505
  const generator = new SpecGenerator(specProvider);
10068
10506
  initialSpec = await generator.generateSpec(idea, context);
10069
- console.log(chalk18.green(" \u2714 Spec generated."));
10507
+ console.log(chalk21.green(" \u2714 Spec generated."));
10070
10508
  } else {
10071
10509
  const result = await generateSpecWithTasks(specProvider, idea, context);
10072
10510
  initialSpec = result.spec;
10073
10511
  initialTasks = result.tasks;
10074
- console.log(chalk18.green(` \u2714 Spec generated.`));
10512
+ console.log(chalk21.green(` \u2714 Spec generated.`));
10075
10513
  if (initialTasks.length > 0) {
10076
- console.log(chalk18.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
10514
+ console.log(chalk21.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
10077
10515
  } else {
10078
- console.log(chalk18.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."));
10079
10517
  }
10080
10518
  }
10081
10519
  runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
10082
10520
  } catch (err) {
10083
10521
  runLogger.stageFail("spec_gen", err.message);
10084
- console.error(chalk18.red(" \u2718 Spec generation failed:"), err);
10522
+ console.error(chalk21.red(" \u2718 Spec generation failed:"), err);
10085
10523
  process.exit(1);
10086
10524
  }
10087
10525
  let finalSpec;
10088
10526
  if (opts.fast) {
10089
- console.log(chalk18.gray("\n[3/6] Skipping refinement (--fast)."));
10527
+ console.log(chalk21.gray("\n[3/6] Skipping refinement (--fast)."));
10090
10528
  finalSpec = initialSpec;
10091
10529
  } else {
10092
- console.log(chalk18.blue("\n[3/6] Interactive spec refinement..."));
10530
+ console.log(chalk21.blue("\n[3/6] Interactive spec refinement..."));
10093
10531
  runLogger.stageStart("spec_refine");
10094
10532
  const refiner = new SpecRefiner(specProvider);
10095
10533
  finalSpec = await refiner.refineLoop(initialSpec);
@@ -10100,7 +10538,7 @@ program.command("create").description("Generate a feature spec and kick off code
10100
10538
  const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
10101
10539
  if (shouldRunAssessment) {
10102
10540
  if (!opts.auto) {
10103
- console.log(chalk18.blue("\n[3.4/6] Spec quality assessment..."));
10541
+ console.log(chalk21.blue("\n[3.4/6] Spec quality assessment..."));
10104
10542
  }
10105
10543
  runLogger.stageStart("spec_assess");
10106
10544
  const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
@@ -10109,45 +10547,45 @@ program.command("create").description("Generate a feature spec and kick off code
10109
10547
  if (!opts.auto) printSpecAssessment(assessment);
10110
10548
  if (minScore > 0 && assessment.overallScore < minScore) {
10111
10549
  if (opts.force) {
10112
- console.log(chalk18.yellow(`
10550
+ console.log(chalk21.yellow(`
10113
10551
  \u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
10114
10552
  } else {
10115
10553
  runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
10116
- console.log(chalk18.red(`
10554
+ console.log(chalk21.red(`
10117
10555
  \u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
10118
10556
  if (!opts.auto) {
10119
- console.log(chalk18.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.`));
10120
10558
  } else {
10121
- console.log(chalk18.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.`));
10122
10560
  }
10123
- console.log(chalk18.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}`));
10124
10562
  process.exit(1);
10125
10563
  }
10126
10564
  }
10127
10565
  } else {
10128
10566
  runLogger.stageEnd("spec_assess", { skipped: true });
10129
10567
  if (!opts.auto) {
10130
- console.log(chalk18.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
10568
+ console.log(chalk21.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
10131
10569
  }
10132
10570
  }
10133
10571
  }
10134
10572
  if (!opts.auto) {
10135
- console.log(chalk18.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"));
10136
10574
  const specLines = finalSpec.split("\n").length;
10137
10575
  const specWords = finalSpec.split(/\s+/).length;
10138
10576
  const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
10139
- console.log(chalk18.gray(` Spec length : ${specLines} lines / ${specWords} words`));
10140
- if (taskCountHint) console.log(chalk18.gray(taskCountHint));
10141
- 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");
10142
10580
  const slug = featureSlug;
10143
10581
  const prevVersion = await findLatestVersion(previewSpecsDir, slug);
10144
10582
  if (prevVersion) {
10145
- console.log(chalk18.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
10583
+ console.log(chalk21.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
10146
10584
  const diff = computeDiff(prevVersion.content, finalSpec);
10147
- console.log(chalk18.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"));
10148
10586
  printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
10149
10587
  printDiff(diff);
10150
- console.log(chalk18.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"));
10151
10589
  }
10152
10590
  const gate = await select3({
10153
10591
  message: "Ready to proceed to code generation?",
@@ -10158,9 +10596,9 @@ program.command("create").description("Generate a feature spec and kick off code
10158
10596
  ]
10159
10597
  });
10160
10598
  if (gate === "view") {
10161
- console.log(chalk18.cyan("\n" + "\u2500".repeat(52)));
10599
+ console.log(chalk21.cyan("\n" + "\u2500".repeat(52)));
10162
10600
  console.log(finalSpec);
10163
- console.log(chalk18.cyan("\u2500".repeat(52) + "\n"));
10601
+ console.log(chalk21.cyan("\u2500".repeat(52) + "\n"));
10164
10602
  const confirm22 = await select3({
10165
10603
  message: "Proceed to code generation?",
10166
10604
  choices: [
@@ -10169,92 +10607,135 @@ program.command("create").description("Generate a feature spec and kick off code
10169
10607
  ]
10170
10608
  });
10171
10609
  if (confirm22 === "abort") {
10172
- console.log(chalk18.yellow(" Aborted. Spec was NOT saved."));
10610
+ console.log(chalk21.yellow(" Aborted. Spec was NOT saved."));
10173
10611
  process.exit(0);
10174
10612
  }
10175
10613
  } else if (gate === "abort") {
10176
- console.log(chalk18.yellow(" Aborted. Spec was NOT saved."));
10614
+ console.log(chalk21.yellow(" Aborted. Spec was NOT saved."));
10177
10615
  process.exit(0);
10178
10616
  }
10179
- console.log(chalk18.green(" \u2714 Approved \u2014 continuing to code generation."));
10617
+ console.log(chalk21.green(" \u2714 Approved \u2014 continuing to code generation."));
10180
10618
  } else {
10181
- console.log(chalk18.gray("[3.5/6] Approval Gate: skipped (--auto)."));
10619
+ console.log(chalk21.gray("[3.5/6] Approval Gate: skipped (--auto)."));
10182
10620
  }
10183
10621
  let extractedDsl = null;
10184
10622
  if (opts.skipDsl) {
10185
- console.log(chalk18.gray("\n[DSL] Skipped (--skip-dsl)."));
10623
+ console.log(chalk21.gray("\n[DSL] Skipped (--skip-dsl)."));
10186
10624
  } else {
10187
- console.log(chalk18.blue("\n[DSL] Extracting structured DSL from spec..."));
10188
- console.log(chalk18.gray(` Provider: ${specProviderName}/${specModelName}`));
10625
+ console.log(chalk21.blue("\n[DSL] Extracting structured DSL from spec..."));
10626
+ console.log(chalk21.gray(` Provider: ${specProviderName}/${specModelName}`));
10189
10627
  runLogger.stageStart("dsl_extract");
10190
10628
  try {
10191
10629
  const isFrontend = isFrontendDeps(context.dependencies);
10192
- if (isFrontend) console.log(chalk18.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
10630
+ if (isFrontend) console.log(chalk21.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
10193
10631
  const dslExtractor = new DslExtractor(specProvider);
10194
10632
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
10195
10633
  if (extractedDsl) {
10196
10634
  runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
10197
- console.log(chalk18.green(" \u2714 DSL extracted and validated."));
10635
+ console.log(chalk21.green(" \u2714 DSL extracted and validated."));
10198
10636
  } else {
10199
10637
  runLogger.stageEnd("dsl_extract", { skipped: true });
10200
- console.log(chalk18.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."));
10201
10639
  }
10202
10640
  } catch (err) {
10203
10641
  runLogger.stageFail("dsl_extract", err.message);
10204
- console.log(chalk18.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
+ }
10205
10686
  }
10206
10687
  }
10207
10688
  const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
10208
10689
  const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
10209
10690
  let workingDir = currentDir;
10210
10691
  if (!skipWorktree) {
10211
- console.log(chalk18.blue("\n[4/6] Setting up git worktree..."));
10692
+ console.log(chalk21.blue("\n[4/6] Setting up git worktree..."));
10212
10693
  const worktreeManager = new GitWorktreeManager(currentDir);
10213
10694
  const worktreePath = await worktreeManager.createWorktree(idea);
10214
10695
  if (worktreePath) workingDir = worktreePath;
10215
10696
  } else {
10216
10697
  const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
10217
- console.log(chalk18.gray(`[4/6] Skipping worktree${reason}.`));
10698
+ console.log(chalk21.gray(`[4/6] Skipping worktree${reason}.`));
10218
10699
  }
10219
- const specsDir = path22.join(workingDir, "specs");
10220
- await fs23.ensureDir(specsDir);
10700
+ const specsDir = path23.join(workingDir, "specs");
10701
+ await fs24.ensureDir(specsDir);
10221
10702
  const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
10222
- await fs23.writeFile(specFile, finalSpec, "utf-8");
10223
- console.log(chalk18.green(`
10224
- [5/6] \u2714 Spec saved: ${specFile}`) + chalk18.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})`));
10225
10706
  let savedDslFile = null;
10226
10707
  if (extractedDsl) {
10227
10708
  const dslExtractor = new DslExtractor(specProvider);
10228
10709
  savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
10229
- console.log(chalk18.green(` \u2714 DSL saved : ${savedDslFile}`));
10710
+ console.log(chalk21.green(` \u2714 DSL saved : ${savedDslFile}`));
10230
10711
  }
10231
10712
  if (!opts.skipTasks) {
10232
10713
  const taskGen = new TaskGenerator(specProvider);
10233
10714
  let tasksToSave = initialTasks;
10234
10715
  if (tasksToSave.length === 0) {
10235
- console.log(chalk18.blue(`
10716
+ console.log(chalk21.blue(`
10236
10717
  Generating tasks (separate call)...`));
10237
10718
  try {
10238
10719
  tasksToSave = await taskGen.generateTasks(finalSpec, context);
10239
10720
  } catch (err) {
10240
- console.log(chalk18.yellow(` \u26A0 Task generation failed: ${err.message}`));
10721
+ console.log(chalk21.yellow(` \u26A0 Task generation failed: ${err.message}`));
10241
10722
  }
10242
10723
  }
10243
10724
  if (tasksToSave.length > 0) {
10244
10725
  const sorted = taskGen.sortByLayer(tasksToSave);
10245
10726
  const tasksFile = await taskGen.saveTasks(sorted, specFile);
10246
10727
  printTasks(sorted);
10247
- console.log(chalk18.green(` \u2714 Tasks saved: ${tasksFile}`));
10728
+ console.log(chalk21.green(` \u2714 Tasks saved: ${tasksFile}`));
10248
10729
  } else {
10249
- console.log(chalk18.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."));
10250
10731
  }
10251
10732
  }
10252
- console.log(chalk18.blue(`
10733
+ console.log(chalk21.blue(`
10253
10734
  [6/6] Code generation (mode: ${codegenMode})...`));
10254
10735
  const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
10255
10736
  let generatedTestFiles = [];
10256
10737
  if (opts.tdd && extractedDsl) {
10257
- console.log(chalk18.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)..."));
10258
10739
  const testGen = new TestGenerator(codegenProvider);
10259
10740
  generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
10260
10741
  }
@@ -10268,27 +10749,29 @@ program.command("create").description("Generate a feature spec and kick off code
10268
10749
  });
10269
10750
  runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
10270
10751
  if (opts.tdd) {
10271
- console.log(chalk18.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."));
10272
10753
  } else if (opts.skipTests) {
10273
- console.log(chalk18.gray("\n[7/9] Skipping test generation (--skip-tests)."));
10754
+ console.log(chalk21.gray("\n[7/9] Skipping test generation (--skip-tests)."));
10274
10755
  } else if (!extractedDsl) {
10275
- console.log(chalk18.gray("\n[7/9] Skipping test generation (no DSL available)."));
10756
+ console.log(chalk21.gray("\n[7/9] Skipping test generation (no DSL available)."));
10276
10757
  } else {
10277
- console.log(chalk18.blue(`
10758
+ console.log(chalk21.blue(`
10278
10759
  [7/9] Test skeleton generation...`));
10279
10760
  runLogger.stageStart("test_gen");
10280
10761
  const testGen = new TestGenerator(codegenProvider);
10281
10762
  generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
10282
10763
  runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
10283
10764
  }
10765
+ let compilePassed = false;
10284
10766
  if (opts.skipErrorFeedback) {
10285
- console.log(chalk18.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
10767
+ console.log(chalk21.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
10768
+ compilePassed = true;
10286
10769
  } else {
10287
10770
  if (opts.tdd) {
10288
- console.log(chalk18.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..."));
10289
10772
  }
10290
10773
  runLogger.stageStart("error_feedback");
10291
- await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
10774
+ compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
10292
10775
  maxCycles: opts.tdd ? 3 : 2
10293
10776
  // TDD gets one extra cycle
10294
10777
  });
@@ -10296,10 +10779,10 @@ program.command("create").description("Generate a feature spec and kick off code
10296
10779
  }
10297
10780
  let reviewResult = "";
10298
10781
  if (!opts.skipReview) {
10299
- console.log(chalk18.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)..."));
10300
10783
  runLogger.stageStart("review");
10301
10784
  const reviewer = new CodeReviewer(specProvider, currentDir);
10302
- const savedSpec = await fs23.readFile(specFile, "utf-8");
10785
+ const savedSpec = await fs24.readFile(specFile, "utf-8");
10303
10786
  if (codegenMode === "api" && generatedFiles.length > 0) {
10304
10787
  reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
10305
10788
  } else {
@@ -10314,20 +10797,89 @@ program.command("create").description("Generate a feature spec and kick off code
10314
10797
  runLogger.stageEnd("review");
10315
10798
  await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
10316
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
+ }
10859
+ runLogger.stageStart("self_eval");
10860
+ const selfEvalResult = runSelfEval({
10861
+ dsl: extractedDsl,
10862
+ generatedFiles,
10863
+ compilePassed,
10864
+ reviewText: reviewResult,
10865
+ promptHash,
10866
+ logger: runLogger
10867
+ });
10868
+ printSelfEval(selfEvalResult);
10317
10869
  runLogger.finish();
10318
- console.log(chalk18.bold.green("\n\u2714 All done!"));
10319
- console.log(chalk18.gray(` Spec : ${specFile}`));
10320
- if (savedDslFile) console.log(chalk18.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}`));
10321
10873
  if (generatedTestFiles.length > 0) {
10322
- console.log(chalk18.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
10874
+ console.log(chalk21.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
10323
10875
  }
10324
- console.log(chalk18.gray(` Working dir : ${workingDir}`));
10876
+ console.log(chalk21.gray(` Working dir : ${workingDir}`));
10325
10877
  if (workingDir !== currentDir) {
10326
- console.log(chalk18.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10878
+ console.log(chalk21.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
10327
10879
  }
10328
10880
  runLogger.printSummary();
10329
10881
  if (runSnapshot.fileCount > 0) {
10330
- console.log(chalk18.gray(` To undo changes: ai-spec restore ${runId}`));
10882
+ console.log(chalk21.gray(` To undo changes: ai-spec restore ${runId}`));
10331
10883
  }
10332
10884
  });
10333
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(
@@ -10344,24 +10896,24 @@ program.command("review").description("Run AI code review on current git diff ag
10344
10896
  const reviewer = new CodeReviewer(provider, currentDir);
10345
10897
  let specContent = "";
10346
10898
  let resolvedSpecFile;
10347
- if (specFile && await fs23.pathExists(specFile)) {
10348
- specContent = await fs23.readFile(specFile, "utf-8");
10899
+ if (specFile && await fs24.pathExists(specFile)) {
10900
+ specContent = await fs24.readFile(specFile, "utf-8");
10349
10901
  resolvedSpecFile = specFile;
10350
- console.log(chalk18.gray(`Using spec: ${specFile}`));
10902
+ console.log(chalk21.gray(`Using spec: ${specFile}`));
10351
10903
  } else {
10352
- const specsDir = path22.join(currentDir, "specs");
10353
- if (await fs23.pathExists(specsDir)) {
10354
- 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();
10355
10907
  if (files.length > 0) {
10356
- const latest = path22.join(specsDir, files[0]);
10357
- specContent = await fs23.readFile(latest, "utf-8");
10908
+ const latest = path23.join(specsDir, files[0]);
10909
+ specContent = await fs24.readFile(latest, "utf-8");
10358
10910
  resolvedSpecFile = latest;
10359
- console.log(chalk18.gray(`Auto-detected spec: specs/${files[0]}`));
10911
+ console.log(chalk21.gray(`Auto-detected spec: specs/${files[0]}`));
10360
10912
  }
10361
10913
  }
10362
10914
  }
10363
10915
  if (!specContent) {
10364
- console.log(chalk18.yellow("No spec file found. Running review without spec context."));
10916
+ console.log(chalk21.yellow("No spec file found. Running review without spec context."));
10365
10917
  }
10366
10918
  await reviewer.reviewCode(specContent, resolvedSpecFile);
10367
10919
  await reviewer.printScoreTrend();
@@ -10388,15 +10940,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
10388
10940
  auto: opts.auto
10389
10941
  });
10390
10942
  if (result.written) {
10391
- console.log(chalk18.blue("\n Summary:"));
10392
- console.log(chalk18.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
10393
- console.log(chalk18.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`));
10394
10946
  if (result.backupPath) {
10395
- console.log(chalk18.gray(` Backup: ${path22.basename(result.backupPath)}`));
10947
+ console.log(chalk21.gray(` Backup: ${path23.basename(result.backupPath)}`));
10396
10948
  }
10397
10949
  }
10398
10950
  } catch (err) {
10399
- console.error(chalk18.red(` \u2718 Consolidation failed: ${err.message}`));
10951
+ console.error(chalk21.red(` \u2718 Consolidation failed: ${err.message}`));
10400
10952
  process.exit(1);
10401
10953
  }
10402
10954
  return;
@@ -10404,75 +10956,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
10404
10956
  if (opts.global) {
10405
10957
  const existing = await loadGlobalConstitution([currentDir]);
10406
10958
  if (existing && !opts.force) {
10407
- console.log(chalk18.yellow(`
10959
+ console.log(chalk21.yellow(`
10408
10960
  Global constitution already exists at: ${existing.source}`));
10409
- console.log(chalk18.gray(" Use --force to overwrite it."));
10961
+ console.log(chalk21.gray(" Use --force to overwrite it."));
10410
10962
  return;
10411
10963
  }
10412
- console.log(chalk18.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10413
- console.log(chalk18.gray(` Provider: ${providerName}/${modelName}`));
10414
- console.log(chalk18.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..."));
10415
10967
  const loader = new ContextLoader(currentDir);
10416
10968
  const ctx = await loader.loadProjectContext();
10417
10969
  const summary = [
10418
10970
  `Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
10419
10971
  `Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
10420
10972
  ].join("\n");
10421
- const prompt = buildGlobalConstitutionPrompt([{ name: path22.basename(currentDir), summary }]);
10973
+ const prompt = buildGlobalConstitutionPrompt([{ name: path23.basename(currentDir), summary }]);
10422
10974
  let globalConstitution;
10423
10975
  try {
10424
10976
  globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
10425
10977
  } catch (err) {
10426
- console.error(chalk18.red(" \u2718 Failed to generate global constitution:"), err);
10978
+ console.error(chalk21.red(" \u2718 Failed to generate global constitution:"), err);
10427
10979
  process.exit(1);
10428
10980
  }
10429
10981
  const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
10430
- console.log(chalk18.green(`
10982
+ console.log(chalk21.green(`
10431
10983
  \u2714 Global constitution saved: ${saved2}`));
10432
- console.log(chalk18.gray(" This will be automatically merged into all project constitutions in this workspace."));
10433
- console.log(chalk18.gray(" Project-level rules always override global rules.\n"));
10434
- console.log(chalk18.bold(" Preview:"));
10435
- console.log(chalk18.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")));
10436
10988
  if (globalConstitution.split("\n").length > 12) {
10437
- console.log(chalk18.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
10989
+ console.log(chalk21.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
10438
10990
  }
10439
10991
  return;
10440
10992
  }
10441
- const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
10442
- if (!opts.force && await fs23.pathExists(constitutionPath)) {
10443
- console.log(chalk18.yellow(`
10993
+ const constitutionPath = path23.join(currentDir, CONSTITUTION_FILE);
10994
+ if (!opts.force && await fs24.pathExists(constitutionPath)) {
10995
+ console.log(chalk21.yellow(`
10444
10996
  ${CONSTITUTION_FILE} already exists.`));
10445
- console.log(chalk18.gray(" Use --force to overwrite it."));
10446
- console.log(chalk18.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}`));
10447
10999
  return;
10448
11000
  }
10449
- console.log(chalk18.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10450
- console.log(chalk18.gray(` Provider: ${providerName}/${modelName}`));
10451
- console.log(chalk18.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..."));
10452
11004
  const generator = new ConstitutionGenerator(provider);
10453
11005
  let constitution;
10454
11006
  try {
10455
11007
  constitution = await generator.generate(currentDir);
10456
11008
  } catch (err) {
10457
- console.error(chalk18.red(" \u2718 Failed to generate constitution:"), err);
11009
+ console.error(chalk21.red(" \u2718 Failed to generate constitution:"), err);
10458
11010
  process.exit(1);
10459
11011
  }
10460
11012
  const saved = await generator.saveConstitution(currentDir, constitution);
10461
- const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
11013
+ const globalResult = await loadGlobalConstitution([path23.dirname(currentDir)]);
10462
11014
  if (globalResult) {
10463
- console.log(chalk18.cyan(`
11015
+ console.log(chalk21.cyan(`
10464
11016
  \u2139 Global constitution detected: ${globalResult.source}`));
10465
- console.log(chalk18.gray(" It will be merged with this project constitution at runtime."));
10466
- console.log(chalk18.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."));
10467
11019
  }
10468
- console.log(chalk18.green(`
11020
+ console.log(chalk21.green(`
10469
11021
  \u2714 Constitution saved: ${saved}`));
10470
- console.log(chalk18.gray(" This file will be automatically used in all future `ai-spec create` runs."));
10471
- console.log(chalk18.gray(" Edit it to add custom rules or red lines for your project.\n"));
10472
- console.log(chalk18.bold(" Preview:"));
10473
- console.log(chalk18.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")));
10474
11026
  if (constitution.split("\n").length > 15) {
10475
- console.log(chalk18.gray(` ... (${constitution.split("\n").length} lines total)`));
11027
+ console.log(chalk21.gray(` ... (${constitution.split("\n").length} lines total)`));
10476
11028
  }
10477
11029
  });
10478
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(
@@ -10480,44 +11032,44 @@ program.command("config").description(`Set default configuration for this projec
10480
11032
  "Default code generation mode (claude-code|api|plan)"
10481
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) => {
10482
11034
  const currentDir = process.cwd();
10483
- const configPath = path22.join(currentDir, CONFIG_FILE);
11035
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10484
11036
  if (opts.clearKeys) {
10485
11037
  await clearAllKeys();
10486
- console.log(chalk18.green(`\u2714 All saved API keys cleared.`));
11038
+ console.log(chalk21.green(`\u2714 All saved API keys cleared.`));
10487
11039
  return;
10488
11040
  }
10489
11041
  if (opts.clearKey) {
10490
11042
  await clearKey(opts.clearKey);
10491
- console.log(chalk18.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
11043
+ console.log(chalk21.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
10492
11044
  return;
10493
11045
  }
10494
11046
  if (opts.listKeys) {
10495
- const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
11047
+ const store = await fs24.readJson(KEY_STORE_FILE).catch(() => ({}));
10496
11048
  const providers = Object.keys(store);
10497
11049
  if (providers.length === 0) {
10498
- console.log(chalk18.gray("No saved API keys."));
11050
+ console.log(chalk21.gray("No saved API keys."));
10499
11051
  } else {
10500
- console.log(chalk18.bold("Saved API keys:"));
11052
+ console.log(chalk21.bold("Saved API keys:"));
10501
11053
  for (const p of providers) {
10502
11054
  const k2 = store[p];
10503
- console.log(chalk18.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
11055
+ console.log(chalk21.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
10504
11056
  }
10505
- console.log(chalk18.gray(`
11057
+ console.log(chalk21.gray(`
10506
11058
  File: ${KEY_STORE_FILE}`));
10507
11059
  }
10508
11060
  return;
10509
11061
  }
10510
11062
  if (opts.reset) {
10511
- await fs23.writeJson(configPath, {}, { spaces: 2 });
10512
- console.log(chalk18.green(`\u2714 Config reset: ${configPath}`));
11063
+ await fs24.writeJson(configPath, {}, { spaces: 2 });
11064
+ console.log(chalk21.green(`\u2714 Config reset: ${configPath}`));
10513
11065
  return;
10514
11066
  }
10515
11067
  const existing = await loadConfig(currentDir);
10516
11068
  if (opts.show) {
10517
11069
  if (Object.keys(existing).length === 0) {
10518
- console.log(chalk18.gray("No config file found. Using built-in defaults."));
11070
+ console.log(chalk21.gray("No config file found. Using built-in defaults."));
10519
11071
  } else {
10520
- console.log(chalk18.bold(`${configPath}:`));
11072
+ console.log(chalk21.bold(`${configPath}:`));
10521
11073
  console.log(JSON.stringify(existing, null, 2));
10522
11074
  }
10523
11075
  return;
@@ -10531,27 +11083,27 @@ File: ${KEY_STORE_FILE}`));
10531
11083
  if (opts.minSpecScore !== void 0) {
10532
11084
  const score = parseInt(opts.minSpecScore, 10);
10533
11085
  if (isNaN(score) || score < 0 || score > 10) {
10534
- console.error(chalk18.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"));
10535
11087
  process.exit(1);
10536
11088
  }
10537
11089
  updated.minSpecScore = score;
10538
11090
  }
10539
- await fs23.writeJson(configPath, updated, { spaces: 2 });
10540
- console.log(chalk18.green(`\u2714 Config saved to ${configPath}`));
11091
+ await fs24.writeJson(configPath, updated, { spaces: 2 });
11092
+ console.log(chalk21.green(`\u2714 Config saved to ${configPath}`));
10541
11093
  console.log(JSON.stringify(updated, null, 2));
10542
11094
  });
10543
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) => {
10544
11096
  const currentDir = process.cwd();
10545
- const configPath = path22.join(currentDir, CONFIG_FILE);
11097
+ const configPath = path23.join(currentDir, CONFIG_FILE);
10546
11098
  if (opts.list) {
10547
- console.log(chalk18.bold("\nAvailable providers & models:\n"));
11099
+ console.log(chalk21.bold("\nAvailable providers & models:\n"));
10548
11100
  for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
10549
11101
  console.log(
10550
- ` ${chalk18.bold.cyan(key.padEnd(10))} ${chalk18.white(meta.displayName)}`
11102
+ ` ${chalk21.bold.cyan(key.padEnd(10))} ${chalk21.white(meta.displayName)}`
10551
11103
  );
10552
- console.log(chalk18.gray(` ${meta.description}`));
11104
+ console.log(chalk21.gray(` ${meta.description}`));
10553
11105
  console.log(
10554
- chalk18.gray(
11106
+ chalk21.gray(
10555
11107
  ` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
10556
11108
  )
10557
11109
  );
@@ -10560,10 +11112,10 @@ program.command("model").description("Interactively switch the active AI provide
10560
11112
  return;
10561
11113
  }
10562
11114
  const existing = await loadConfig(currentDir);
10563
- console.log(chalk18.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"));
10564
11116
  if (Object.keys(existing).length > 0) {
10565
11117
  console.log(
10566
- chalk18.gray(
11118
+ chalk21.gray(
10567
11119
  ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
10568
11120
  )
10569
11121
  );
@@ -10581,7 +11133,7 @@ program.command("model").description("Interactively switch the active AI provide
10581
11133
  const providerKey = await select3({
10582
11134
  message: `${label} \u2014 select provider:`,
10583
11135
  choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
10584
- name: `${meta2.displayName.padEnd(22)} ${chalk18.gray(meta2.description)}`,
11136
+ name: `${meta2.displayName.padEnd(22)} ${chalk21.gray(meta2.description)}`,
10585
11137
  value: key,
10586
11138
  short: meta2.displayName
10587
11139
  }))
@@ -10589,7 +11141,7 @@ program.command("model").description("Interactively switch the active AI provide
10589
11141
  const meta = PROVIDER_CATALOG[providerKey];
10590
11142
  const modelChoices = [
10591
11143
  ...meta.models.map((m) => ({ name: m, value: m })),
10592
- { name: chalk18.italic("\u270E Enter custom model name..."), value: "__custom__" }
11144
+ { name: chalk21.italic("\u270E Enter custom model name..."), value: "__custom__" }
10593
11145
  ];
10594
11146
  let chosenModel = await select3({
10595
11147
  message: `${label} \u2014 select model (${meta.displayName}):`,
@@ -10623,37 +11175,37 @@ program.command("model").description("Interactively switch the active AI provide
10623
11175
  if (!updated.codegen || updated.codegen === "claude-code") {
10624
11176
  updated.codegen = "api";
10625
11177
  console.log(
10626
- chalk18.yellow(
11178
+ chalk21.yellow(
10627
11179
  `
10628
11180
  \u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
10629
11181
  )
10630
11182
  );
10631
- console.log(chalk18.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`));
10632
11184
  }
10633
11185
  }
10634
11186
  }
10635
- console.log(chalk18.blue("\n Preview:"));
10636
- console.log(chalk18.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}`));
10637
11189
  if (updated.codegenProvider) {
10638
11190
  console.log(
10639
- chalk18.gray(
11191
+ chalk21.gray(
10640
11192
  ` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
10641
11193
  )
10642
11194
  );
10643
11195
  }
10644
11196
  const ok = await confirm2({ message: "Save to .ai-spec.json?", default: true });
10645
11197
  if (!ok) {
10646
- console.log(chalk18.gray(" Cancelled."));
11198
+ console.log(chalk21.gray(" Cancelled."));
10647
11199
  return;
10648
11200
  }
10649
- await fs23.writeJson(configPath, updated, { spaces: 2 });
10650
- console.log(chalk18.green(`
11201
+ await fs24.writeJson(configPath, updated, { spaces: 2 });
11202
+ console.log(chalk21.green(`
10651
11203
  \u2714 Saved to ${configPath}`));
10652
11204
  const providerToCheck = updated.provider ?? "gemini";
10653
11205
  const envKey = ENV_KEY_MAP[providerToCheck];
10654
11206
  if (envKey && !process.env[envKey]) {
10655
11207
  console.log(
10656
- chalk18.yellow(
11208
+ chalk21.yellow(
10657
11209
  ` \u26A0 Remember to set ${envKey} in your environment or .env file.`
10658
11210
  )
10659
11211
  );
@@ -10672,29 +11224,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10672
11224
  cliOpts,
10673
11225
  contractContextSection
10674
11226
  } = opts;
10675
- console.log(chalk18.blue(`
11227
+ console.log(chalk21.blue(`
10676
11228
  [${repoName}] Loading project context...`));
10677
11229
  const loader = new ContextLoader(repoAbsPath);
10678
11230
  let context = await loader.loadProjectContext();
10679
11231
  const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
10680
- console.log(chalk18.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
10681
- console.log(chalk18.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`));
10682
11234
  if (context.constitution && context.constitution.length > 6e3) {
10683
- console.log(chalk18.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`));
10684
11236
  }
10685
11237
  if (!context.constitution) {
10686
- console.log(chalk18.yellow(` Constitution: not found \u2014 auto-generating...`));
11238
+ console.log(chalk21.yellow(` Constitution: not found \u2014 auto-generating...`));
10687
11239
  try {
10688
11240
  const constitutionGen = new ConstitutionGenerator(specProvider);
10689
11241
  const constitutionContent = await constitutionGen.generate(repoAbsPath);
10690
11242
  await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
10691
11243
  context.constitution = constitutionContent;
10692
- console.log(chalk18.green(` Constitution: generated`));
11244
+ console.log(chalk21.green(` Constitution: generated`));
10693
11245
  } catch (err) {
10694
- console.log(chalk18.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
11246
+ console.log(chalk21.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
10695
11247
  }
10696
11248
  } else {
10697
- console.log(chalk18.green(` Constitution: found`));
11249
+ console.log(chalk21.green(` Constitution: found`));
10698
11250
  }
10699
11251
  let fullIdea = idea;
10700
11252
  if (contractContextSection) {
@@ -10702,58 +11254,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
10702
11254
 
10703
11255
  ${contractContextSection}`;
10704
11256
  }
10705
- console.log(chalk18.blue(` [${repoName}] Generating spec...`));
11257
+ console.log(chalk21.blue(` [${repoName}] Generating spec...`));
10706
11258
  let finalSpec;
10707
11259
  try {
10708
11260
  const result = await generateSpecWithTasks(specProvider, fullIdea, context);
10709
11261
  finalSpec = result.spec;
10710
- console.log(chalk18.green(` Spec generated.`));
11262
+ console.log(chalk21.green(` Spec generated.`));
10711
11263
  } catch (err) {
10712
- console.error(chalk18.red(` Spec generation failed: ${err.message}`));
11264
+ console.error(chalk21.red(` Spec generation failed: ${err.message}`));
10713
11265
  return { dsl: null, specFile: null };
10714
11266
  }
10715
11267
  let extractedDsl = null;
10716
11268
  if (!cliOpts.skipDsl) {
10717
- console.log(chalk18.blue(` [${repoName}] Extracting DSL...`));
11269
+ console.log(chalk21.blue(` [${repoName}] Extracting DSL...`));
10718
11270
  try {
10719
11271
  const dslExtractor = new DslExtractor(specProvider);
10720
11272
  const repoIsFrontend = isFrontendDeps(context.dependencies);
10721
11273
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
10722
11274
  if (extractedDsl) {
10723
- console.log(chalk18.green(` DSL extracted.`));
11275
+ console.log(chalk21.green(` DSL extracted.`));
10724
11276
  }
10725
11277
  } catch (err) {
10726
- console.log(chalk18.yellow(` DSL extraction failed: ${err.message}`));
11278
+ console.log(chalk21.yellow(` DSL extraction failed: ${err.message}`));
10727
11279
  }
10728
11280
  }
10729
11281
  const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
10730
11282
  const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
10731
11283
  let workingDir = repoAbsPath;
10732
11284
  if (!skipWorktreeForRepo) {
10733
- console.log(chalk18.blue(` [${repoName}] Setting up git worktree...`));
11285
+ console.log(chalk21.blue(` [${repoName}] Setting up git worktree...`));
10734
11286
  try {
10735
11287
  const worktreeManager = new GitWorktreeManager(repoAbsPath);
10736
11288
  const worktreePath = await worktreeManager.createWorktree(idea);
10737
11289
  if (worktreePath) workingDir = worktreePath;
10738
11290
  } catch (err) {
10739
- console.log(chalk18.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
11291
+ console.log(chalk21.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
10740
11292
  }
10741
11293
  } else {
10742
- console.log(chalk18.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
11294
+ console.log(chalk21.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
10743
11295
  }
10744
- const specsDir = path22.join(workingDir, "specs");
10745
- await fs23.ensureDir(specsDir);
11296
+ const specsDir = path23.join(workingDir, "specs");
11297
+ await fs24.ensureDir(specsDir);
10746
11298
  const featureSlug = slugify(idea);
10747
11299
  const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
10748
- await fs23.writeFile(specFile, finalSpec, "utf-8");
10749
- console.log(chalk18.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)}`));
10750
11302
  let savedDslFile = null;
10751
11303
  if (extractedDsl) {
10752
11304
  const dslExtractorForSave = new DslExtractor(specProvider);
10753
11305
  savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
10754
- console.log(chalk18.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
11306
+ console.log(chalk21.green(` DSL saved: ${path23.relative(repoAbsPath, savedDslFile)}`));
10755
11307
  }
10756
- console.log(chalk18.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
11308
+ console.log(chalk21.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
10757
11309
  try {
10758
11310
  const codegen = new CodeGenerator(codegenProvider, codegenMode);
10759
11311
  await codegen.generateCode(specFile, workingDir, context, {
@@ -10761,29 +11313,29 @@ ${contractContextSection}`;
10761
11313
  dslFilePath: savedDslFile ?? void 0,
10762
11314
  repoType: detectedRepoType
10763
11315
  });
10764
- console.log(chalk18.green(` Code generation complete.`));
11316
+ console.log(chalk21.green(` Code generation complete.`));
10765
11317
  } catch (err) {
10766
- console.log(chalk18.yellow(` Code generation failed: ${err.message}`));
11318
+ console.log(chalk21.yellow(` Code generation failed: ${err.message}`));
10767
11319
  }
10768
11320
  if (!cliOpts.skipTests && extractedDsl) {
10769
- console.log(chalk18.blue(` [${repoName}] Generating test skeletons...`));
11321
+ console.log(chalk21.blue(` [${repoName}] Generating test skeletons...`));
10770
11322
  try {
10771
11323
  const testGen = new TestGenerator(codegenProvider);
10772
11324
  const testFiles = await testGen.generate(extractedDsl, workingDir);
10773
- console.log(chalk18.green(` ${testFiles.length} test file(s) generated.`));
11325
+ console.log(chalk21.green(` ${testFiles.length} test file(s) generated.`));
10774
11326
  } catch (err) {
10775
- console.log(chalk18.yellow(` Test generation failed: ${err.message}`));
11327
+ console.log(chalk21.yellow(` Test generation failed: ${err.message}`));
10776
11328
  }
10777
11329
  }
10778
11330
  if (!cliOpts.skipErrorFeedback) {
10779
11331
  try {
10780
11332
  await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
10781
11333
  } catch (err) {
10782
- console.log(chalk18.yellow(` Error feedback failed: ${err.message}`));
11334
+ console.log(chalk21.yellow(` Error feedback failed: ${err.message}`));
10783
11335
  }
10784
11336
  }
10785
11337
  if (!cliOpts.skipReview) {
10786
- console.log(chalk18.blue(` [${repoName}] Running code review...`));
11338
+ console.log(chalk21.blue(` [${repoName}] Running code review...`));
10787
11339
  try {
10788
11340
  const reviewer = new CodeReviewer(specProvider);
10789
11341
  const originalDir = process.cwd();
@@ -10795,9 +11347,9 @@ ${contractContextSection}`;
10795
11347
  process.chdir(originalDir);
10796
11348
  }
10797
11349
  await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
10798
- console.log(chalk18.green(` Code review complete.`));
11350
+ console.log(chalk21.green(` Code review complete.`));
10799
11351
  } catch (err) {
10800
- console.log(chalk18.yellow(` Code review failed: ${err.message}`));
11352
+ console.log(chalk21.yellow(` Code review failed: ${err.message}`));
10801
11353
  }
10802
11354
  }
10803
11355
  return { dsl: extractedDsl, specFile };
@@ -10820,7 +11372,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10820
11372
  codegenModel: codegenModelName
10821
11373
  });
10822
11374
  const workspaceLoader = new WorkspaceLoader(currentDir);
10823
- console.log(chalk18.blue("\n[W1] Loading per-repo contexts..."));
11375
+ console.log(chalk21.blue("\n[W1] Loading per-repo contexts..."));
10824
11376
  const contexts = /* @__PURE__ */ new Map();
10825
11377
  const frontendContexts = /* @__PURE__ */ new Map();
10826
11378
  for (const repo of workspace.repos) {
@@ -10832,27 +11384,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10832
11384
  if (repo.role === "frontend" || repo.role === "mobile") {
10833
11385
  const fctx = await loadFrontendContext(repoAbsPath);
10834
11386
  frontendContexts.set(repo.name, fctx);
10835
- console.log(chalk18.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}`));
10836
11388
  } else {
10837
- console.log(chalk18.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)`));
10838
11390
  }
10839
11391
  } catch (err) {
10840
- console.log(chalk18.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
11392
+ console.log(chalk21.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
10841
11393
  }
10842
11394
  }
10843
- console.log(chalk18.blue("\n[W2] Decomposing requirement across repos..."));
11395
+ console.log(chalk21.blue("\n[W2] Decomposing requirement across repos..."));
10844
11396
  const decomposer = new RequirementDecomposer(specProvider);
10845
11397
  let decomposition;
10846
11398
  try {
10847
11399
  decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
10848
- console.log(chalk18.green(` Summary: ${decomposition.summary}`));
10849
- console.log(chalk18.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(", ")}`));
10850
11402
  if (decomposition.coordinationNotes) {
10851
- console.log(chalk18.gray(` Coordination: ${decomposition.coordinationNotes}`));
11403
+ console.log(chalk21.gray(` Coordination: ${decomposition.coordinationNotes}`));
10852
11404
  }
10853
11405
  } catch (err) {
10854
- console.error(chalk18.red(` Decomposition failed: ${err.message}`));
10855
- console.log(chalk18.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."));
10856
11408
  decomposition = {
10857
11409
  originalRequirement: idea,
10858
11410
  summary: idea,
@@ -10868,11 +11420,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10868
11420
  };
10869
11421
  }
10870
11422
  if (!opts.auto) {
10871
- console.log(chalk18.cyan("\n[W3] Decomposition Preview:"));
10872
- console.log(chalk18.cyan("\u2500".repeat(52)));
11423
+ console.log(chalk21.cyan("\n[W3] Decomposition Preview:"));
11424
+ console.log(chalk21.cyan("\u2500".repeat(52)));
10873
11425
  for (const r of decomposition.repos) {
10874
- console.log(chalk18.bold(` ${r.repoName} (${r.role})`));
10875
- console.log(chalk18.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 ? "..." : ""}`));
10876
11428
  if (r.uxDecisions) {
10877
11429
  const ux = r.uxDecisions;
10878
11430
  const uxSummary = [
@@ -10881,13 +11433,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10881
11433
  ux.optimisticUpdate ? "optimistic-update" : "",
10882
11434
  ux.errorRollback ? "rollback" : ""
10883
11435
  ].filter(Boolean).join(", ");
10884
- if (uxSummary) console.log(chalk18.cyan(` UX: ${uxSummary}`));
11436
+ if (uxSummary) console.log(chalk21.cyan(` UX: ${uxSummary}`));
10885
11437
  }
10886
11438
  if (r.dependsOnRepos.length > 0) {
10887
- console.log(chalk18.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
11439
+ console.log(chalk21.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
10888
11440
  }
10889
11441
  }
10890
- console.log(chalk18.cyan("\u2500".repeat(52)));
11442
+ console.log(chalk21.cyan("\u2500".repeat(52)));
10891
11443
  const gate = await select3({
10892
11444
  message: "Proceed with multi-repo pipeline?",
10893
11445
  choices: [
@@ -10896,24 +11448,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10896
11448
  ]
10897
11449
  });
10898
11450
  if (gate === "abort") {
10899
- console.log(chalk18.yellow(" Aborted."));
11451
+ console.log(chalk21.yellow(" Aborted."));
10900
11452
  process.exit(0);
10901
11453
  }
10902
11454
  }
10903
11455
  const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
10904
11456
  const contractDsls = /* @__PURE__ */ new Map();
10905
- console.log(chalk18.blue(`
11457
+ console.log(chalk21.blue(`
10906
11458
  [W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
10907
11459
  const results = [];
10908
11460
  for (const repoReq of sortedRepoRequirements) {
10909
11461
  const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
10910
11462
  if (!repoConfig) {
10911
- console.log(chalk18.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
11463
+ console.log(chalk21.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
10912
11464
  results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
10913
11465
  continue;
10914
11466
  }
10915
11467
  const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
10916
- console.log(chalk18.bold.blue(`
11468
+ console.log(chalk21.bold.blue(`
10917
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`));
10918
11470
  let contractContextSection;
10919
11471
  if (repoReq.dependsOnRepos.length > 0) {
@@ -10921,7 +11473,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10921
11473
  for (const depName of repoReq.dependsOnRepos) {
10922
11474
  const depDsl = contractDsls.get(depName);
10923
11475
  if (depDsl) {
10924
- console.log(chalk18.gray(` Using API contract from: ${depName}`));
11476
+ console.log(chalk21.gray(` Using API contract from: ${depName}`));
10925
11477
  const contract = buildFrontendApiContract(depDsl);
10926
11478
  contractParts.push(buildContractContextSection(contract));
10927
11479
  }
@@ -10941,7 +11493,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10941
11493
  frontendContext: frontendCtx
10942
11494
  });
10943
11495
  contractContextSection = void 0;
10944
- console.log(chalk18.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
11496
+ console.log(chalk21.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
10945
11497
  }
10946
11498
  try {
10947
11499
  const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
@@ -10958,22 +11510,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10958
11510
  });
10959
11511
  if (repoReq.isContractProvider && dsl) {
10960
11512
  contractDsls.set(repoReq.repoName, dsl);
10961
- console.log(chalk18.green(` Contract stored for downstream repos.`));
11513
+ console.log(chalk21.green(` Contract stored for downstream repos.`));
10962
11514
  }
10963
11515
  results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
10964
- console.log(chalk18.green(` \u2714 ${repoReq.repoName} complete`));
11516
+ console.log(chalk21.green(` \u2714 ${repoReq.repoName} complete`));
10965
11517
  } catch (err) {
10966
- console.error(chalk18.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
11518
+ console.error(chalk21.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
10967
11519
  results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
10968
11520
  }
10969
11521
  }
10970
- console.log(chalk18.bold.green("\n\u2714 Multi-repo pipeline complete!"));
10971
- console.log(chalk18.gray(` Workspace: ${workspace.name}`));
10972
- console.log(chalk18.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}`));
10973
11525
  console.log();
10974
11526
  for (const r of results) {
10975
- const icon = r.status === "success" ? chalk18.green("\u2714") : r.status === "failed" ? chalk18.red("\u2718") : chalk18.gray("\u2212");
10976
- const specInfo = r.specFile ? chalk18.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}`) : "";
10977
11529
  console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
10978
11530
  }
10979
11531
  return results;
@@ -10981,18 +11533,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
10981
11533
  var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
10982
11534
  workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
10983
11535
  const currentDir = process.cwd();
10984
- const configPath = path22.join(currentDir, WORKSPACE_CONFIG_FILE);
10985
- if (await fs23.pathExists(configPath)) {
11536
+ const configPath = path23.join(currentDir, WORKSPACE_CONFIG_FILE);
11537
+ if (await fs24.pathExists(configPath)) {
10986
11538
  const overwrite = await confirm2({
10987
11539
  message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
10988
11540
  default: false
10989
11541
  });
10990
11542
  if (!overwrite) {
10991
- console.log(chalk18.gray(" Cancelled."));
11543
+ console.log(chalk21.gray(" Cancelled."));
10992
11544
  return;
10993
11545
  }
10994
11546
  }
10995
- console.log(chalk18.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"));
10996
11548
  const workspaceName = await input({
10997
11549
  message: "Workspace name:",
10998
11550
  validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
@@ -11006,11 +11558,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11006
11558
  const workspaceLoader = new WorkspaceLoader(currentDir);
11007
11559
  const detected = await workspaceLoader.autoDetect();
11008
11560
  if (detected.length === 0) {
11009
- console.log(chalk18.yellow(" No recognizable repos found in sibling directories."));
11561
+ console.log(chalk21.yellow(" No recognizable repos found in sibling directories."));
11010
11562
  } else {
11011
- console.log(chalk18.cyan("\n Detected repos:"));
11563
+ console.log(chalk21.cyan("\n Detected repos:"));
11012
11564
  for (const r of detected) {
11013
- console.log(chalk18.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11565
+ console.log(chalk21.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11014
11566
  }
11015
11567
  const keepAll = await confirm2({
11016
11568
  message: `Include all ${detected.length} detected repo(s)?`,
@@ -11027,7 +11579,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11027
11579
  if (keep) repos.push(r);
11028
11580
  }
11029
11581
  }
11030
- console.log(chalk18.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
11582
+ console.log(chalk21.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
11031
11583
  }
11032
11584
  }
11033
11585
  const repoTypeChoices = [
@@ -11049,7 +11601,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11049
11601
  default: repos.length === 0
11050
11602
  });
11051
11603
  while (addMore) {
11052
- console.log(chalk18.cyan(`
11604
+ console.log(chalk21.cyan(`
11053
11605
  Adding repo #${repos.length + 1}`));
11054
11606
  const repoName = await input({
11055
11607
  message: "Repo name (e.g. api, web, app):",
@@ -11063,16 +11615,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11063
11615
  message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
11064
11616
  default: `./${repoName}`
11065
11617
  });
11066
- const absPath = path22.resolve(currentDir, repoPath);
11618
+ const absPath = path23.resolve(currentDir, repoPath);
11067
11619
  let detectedType = "unknown";
11068
11620
  let detectedRole = "shared";
11069
- if (await fs23.pathExists(absPath)) {
11621
+ if (await fs24.pathExists(absPath)) {
11070
11622
  const { type, role } = await detectRepoType(absPath);
11071
11623
  detectedType = type;
11072
11624
  detectedRole = role;
11073
- console.log(chalk18.gray(` Auto-detected: type=${type}, role=${role}`));
11625
+ console.log(chalk21.gray(` Auto-detected: type=${type}, role=${role}`));
11074
11626
  } else {
11075
- console.log(chalk18.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.`));
11076
11628
  }
11077
11629
  const repoType = await select3({
11078
11630
  message: `Repo type for "${repoName}":`,
@@ -11095,53 +11647,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
11095
11647
  type: repoType,
11096
11648
  role: repoRole
11097
11649
  });
11098
- console.log(chalk18.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
11650
+ console.log(chalk21.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
11099
11651
  addMore = await confirm2({
11100
11652
  message: "Add another repo?",
11101
11653
  default: false
11102
11654
  });
11103
11655
  }
11104
11656
  const workspaceConfig = { name: workspaceName, repos };
11105
- console.log(chalk18.cyan("\n Workspace summary:"));
11106
- console.log(chalk18.gray(` Name: ${workspaceName}`));
11657
+ console.log(chalk21.cyan("\n Workspace summary:"));
11658
+ console.log(chalk21.gray(` Name: ${workspaceName}`));
11107
11659
  for (const r of repos) {
11108
- console.log(chalk18.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11660
+ console.log(chalk21.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
11109
11661
  }
11110
11662
  const ok = await confirm2({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
11111
11663
  if (!ok) {
11112
- console.log(chalk18.gray(" Cancelled."));
11664
+ console.log(chalk21.gray(" Cancelled."));
11113
11665
  return;
11114
11666
  }
11115
11667
  const loader = new WorkspaceLoader(currentDir);
11116
11668
  const saved = await loader.save(workspaceConfig);
11117
- console.log(chalk18.green(`
11669
+ console.log(chalk21.green(`
11118
11670
  \u2714 Workspace saved: ${saved}`));
11119
- console.log(chalk18.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.`));
11120
11672
  });
11121
11673
  workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
11122
11674
  const currentDir = process.cwd();
11123
11675
  const loader = new WorkspaceLoader(currentDir);
11124
11676
  const config2 = await loader.load();
11125
11677
  if (!config2) {
11126
- console.log(chalk18.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
11127
- console.log(chalk18.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."));
11128
11680
  return;
11129
11681
  }
11130
- console.log(chalk18.bold(`
11682
+ console.log(chalk21.bold(`
11131
11683
  Workspace: ${config2.name}`));
11132
- console.log(chalk18.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
11133
- console.log(chalk18.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}):
11134
11686
  `));
11135
11687
  for (const repo of config2.repos) {
11136
11688
  const absPath = loader.resolveAbsPath(repo);
11137
- const exists = await fs23.pathExists(absPath);
11138
- const status = exists ? chalk18.green("found") : chalk18.red("not found");
11689
+ const exists = await fs24.pathExists(absPath);
11690
+ const status = exists ? chalk21.green("found") : chalk21.red("not found");
11139
11691
  console.log(
11140
- ` ${chalk18.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}`
11141
11693
  );
11142
- console.log(chalk18.gray(` path: ${absPath}`));
11694
+ console.log(chalk21.gray(` path: ${absPath}`));
11143
11695
  if (repo.constitution) {
11144
- console.log(chalk18.green(` constitution: found`));
11696
+ console.log(chalk21.green(` constitution: found`));
11145
11697
  }
11146
11698
  }
11147
11699
  });
@@ -11158,30 +11710,30 @@ program.command("update").description("Update an existing spec with a change req
11158
11710
  const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
11159
11711
  const apiKey = await resolveApiKey(providerName, opts.key);
11160
11712
  const provider = createProvider(providerName, apiKey, modelName);
11161
- console.log(chalk18.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"));
11162
- console.log(chalk18.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}`));
11163
11715
  const updateRunId = generateRunId();
11164
11716
  const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
11165
11717
  setActiveSnapshot(updateSnapshot);
11166
11718
  const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
11167
11719
  setActiveLogger(updateLogger);
11168
- console.log(chalk18.gray(` Run ID: ${updateRunId}`));
11720
+ console.log(chalk21.gray(` Run ID: ${updateRunId}`));
11169
11721
  let specPath = opts.spec ?? null;
11170
11722
  if (!specPath) {
11171
- const specsDir = path22.join(currentDir, "specs");
11723
+ const specsDir = path23.join(currentDir, "specs");
11172
11724
  const latest = await SpecUpdater.findLatestSpec(specsDir);
11173
11725
  if (!latest) {
11174
- console.error(chalk18.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>."));
11175
11727
  process.exit(1);
11176
11728
  }
11177
11729
  specPath = latest.filePath;
11178
- console.log(chalk18.gray(` Using spec: ${path22.relative(currentDir, specPath)} (v${latest.version})`));
11730
+ console.log(chalk21.gray(` Using spec: ${path23.relative(currentDir, specPath)} (v${latest.version})`));
11179
11731
  }
11180
- console.log(chalk18.gray(" Loading project context..."));
11732
+ console.log(chalk21.gray(" Loading project context..."));
11181
11733
  const loader = new ContextLoader(currentDir);
11182
11734
  const context = await loader.loadProjectContext();
11183
11735
  if (context.constitution && context.constitution.length > 6e3) {
11184
- console.log(chalk18.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`));
11185
11737
  }
11186
11738
  const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
11187
11739
  const { type: repoType } = await _detectRepoType(currentDir);
@@ -11193,19 +11745,19 @@ program.command("update").description("Update an existing spec with a change req
11193
11745
  repoType
11194
11746
  });
11195
11747
  } catch (err) {
11196
- console.error(chalk18.red(` Update failed: ${err.message}`));
11748
+ console.error(chalk21.red(` Update failed: ${err.message}`));
11197
11749
  process.exit(1);
11198
11750
  }
11199
- console.log(chalk18.green(`
11200
- \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)}`));
11201
11753
  if (result.newDslPath) {
11202
- console.log(chalk18.green(` \u2714 DSL updated: ${path22.relative(currentDir, result.newDslPath)}`));
11754
+ console.log(chalk21.green(` \u2714 DSL updated: ${path23.relative(currentDir, result.newDslPath)}`));
11203
11755
  }
11204
11756
  if (result.affectedFiles.length > 0) {
11205
- console.log(chalk18.cyan("\n Affected files:"));
11757
+ console.log(chalk21.cyan("\n Affected files:"));
11206
11758
  for (const f of result.affectedFiles) {
11207
- const icon = f.action === "create" ? chalk18.green("+") : chalk18.yellow("~");
11208
- console.log(` ${icon} ${f.file}: ${chalk18.gray(f.description)}`);
11759
+ const icon = f.action === "create" ? chalk21.green("+") : chalk21.yellow("~");
11760
+ console.log(` ${icon} ${f.file}: ${chalk21.gray(f.description)}`);
11209
11761
  }
11210
11762
  }
11211
11763
  if (opts.codegen && result.affectedFiles.length > 0) {
@@ -11213,9 +11765,9 @@ program.command("update").description("Update an existing spec with a change req
11213
11765
  const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
11214
11766
  const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
11215
11767
  const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
11216
- console.log(chalk18.blue("\n Regenerating affected files..."));
11768
+ console.log(chalk21.blue("\n Regenerating affected files..."));
11217
11769
  const codeGenerator = new CodeGenerator(codegenProvider, "api");
11218
- const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
11770
+ const specContent = await fs24.readFile(result.newSpecPath, "utf-8");
11219
11771
  const constitutionSection = context.constitution ? `
11220
11772
  === Project Constitution (MUST follow) ===
11221
11773
  ${context.constitution}
@@ -11226,10 +11778,10 @@ ${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
11226
11778
  ` : "";
11227
11779
  updateLogger.stageStart("update_codegen");
11228
11780
  for (const affected of result.affectedFiles) {
11229
- const fullPath = path22.join(currentDir, affected.file);
11781
+ const fullPath = path23.join(currentDir, affected.file);
11230
11782
  let existing = "";
11231
11783
  try {
11232
- existing = await fs23.readFile(fullPath, "utf-8");
11784
+ existing = await fs24.readFile(fullPath, "utf-8");
11233
11785
  } catch {
11234
11786
  }
11235
11787
  const codePrompt = `Apply this change to the file.
@@ -11243,23 +11795,23 @@ ${specContent}
11243
11795
  ${constitutionSection}${dslSection}
11244
11796
  === ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
11245
11797
  ${existing || "Create from scratch."}`;
11246
- process.stdout.write(` ${existing ? chalk18.yellow("~") : chalk18.green("+")} ${affected.file}... `);
11798
+ process.stdout.write(` ${existing ? chalk21.yellow("~") : chalk21.green("+")} ${affected.file}... `);
11247
11799
  try {
11248
11800
  const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
11249
11801
  const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
11250
11802
  const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
11251
- await fs23.ensureDir(path22.dirname(fullPath));
11803
+ await fs24.ensureDir(path23.dirname(fullPath));
11252
11804
  await updateSnapshot.snapshotFile(fullPath);
11253
- await fs23.writeFile(fullPath, content, "utf-8");
11805
+ await fs24.writeFile(fullPath, content, "utf-8");
11254
11806
  updateLogger.fileWritten(affected.file);
11255
- console.log(chalk18.green("\u2714"));
11807
+ console.log(chalk21.green("\u2714"));
11256
11808
  } catch (err) {
11257
11809
  updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
11258
- console.log(chalk18.red(`\u2718 ${err.message}`));
11810
+ console.log(chalk21.red(`\u2718 ${err.message}`));
11259
11811
  }
11260
11812
  }
11261
11813
  updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
11262
- const updatedSpecContent = await fs23.readFile(result.newSpecPath, "utf-8").catch(() => "");
11814
+ const updatedSpecContent = await fs24.readFile(result.newSpecPath, "utf-8").catch(() => "");
11263
11815
  if (updatedSpecContent) {
11264
11816
  const updateReviewer = new CodeReviewer(provider, currentDir);
11265
11817
  const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
@@ -11271,13 +11823,13 @@ ${existing || "Create from scratch."}`;
11271
11823
  updateLogger.finish();
11272
11824
  updateLogger.printSummary();
11273
11825
  if (updateSnapshot.fileCount > 0) {
11274
- console.log(chalk18.gray(` To undo changes: ai-spec restore ${updateRunId}`));
11826
+ console.log(chalk21.gray(` To undo changes: ai-spec restore ${updateRunId}`));
11275
11827
  }
11276
11828
  if (!opts.codegen && result.affectedFiles.length > 0) {
11277
- console.log(chalk18.blue("\n Next steps:"));
11278
- console.log(chalk18.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
11279
- console.log(chalk18.gray(` \u2022 Or update files manually based on the affected files list above`));
11280
- console.log(chalk18.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`));
11281
11833
  }
11282
11834
  });
11283
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) => {
@@ -11286,19 +11838,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
11286
11838
  if (!dslPath) {
11287
11839
  dslPath = await findLatestDslFile(currentDir);
11288
11840
  if (!dslPath) {
11289
- console.error(chalk18.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>."));
11290
11842
  process.exit(1);
11291
11843
  }
11292
- console.log(chalk18.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
11844
+ console.log(chalk21.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
11293
11845
  }
11294
11846
  let dsl;
11295
11847
  try {
11296
- dsl = await fs23.readJson(dslPath);
11848
+ dsl = await fs24.readJson(dslPath);
11297
11849
  } catch (err) {
11298
- console.error(chalk18.red(` Failed to read DSL: ${err.message}`));
11850
+ console.error(chalk21.red(` Failed to read DSL: ${err.message}`));
11299
11851
  process.exit(1);
11300
11852
  }
11301
- console.log(chalk18.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"));
11302
11854
  const format = opts.format === "json" ? "json" : "yaml";
11303
11855
  const serverUrl = opts.server || "http://localhost:3000";
11304
11856
  try {
@@ -11307,31 +11859,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
11307
11859
  serverUrl,
11308
11860
  outputPath: opts.output
11309
11861
  });
11310
- const rel = path22.relative(currentDir, outputPath);
11311
- console.log(chalk18.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
11312
- console.log(chalk18.gray(` Feature : ${dsl.feature.title}`));
11313
- console.log(chalk18.gray(` Endpoints: ${dsl.endpoints.length}`));
11314
- console.log(chalk18.gray(` Models : ${dsl.models.length}`));
11315
- console.log(chalk18.gray(` Server : ${serverUrl}`));
11316
- console.log(chalk18.blue("\n Next steps:"));
11317
- console.log(chalk18.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
11318
- console.log(chalk18.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`));
11319
11871
  } catch (err) {
11320
- console.error(chalk18.red(` Export failed: ${err.message}`));
11872
+ console.error(chalk21.red(` Export failed: ${err.message}`));
11321
11873
  process.exit(1);
11322
11874
  }
11323
11875
  });
11324
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) => {
11325
11877
  const currentDir = process.cwd();
11326
11878
  const port = parseInt(opts.port, 10) || 3001;
11327
- console.log(chalk18.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"));
11328
11880
  if (opts.restore) {
11329
- const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
11881
+ const frontendDir = opts.frontend ? path23.resolve(opts.frontend) : currentDir;
11330
11882
  const r = await restoreMockProxy(frontendDir);
11331
11883
  if (r.restored) {
11332
- console.log(chalk18.green(" \u2714 Proxy restored and mock server stopped."));
11884
+ console.log(chalk21.green(" \u2714 Proxy restored and mock server stopped."));
11333
11885
  } else {
11334
- console.log(chalk18.yellow(` ${r.note ?? "Nothing to restore."}`));
11886
+ console.log(chalk21.yellow(` ${r.note ?? "Nothing to restore."}`));
11335
11887
  }
11336
11888
  return;
11337
11889
  }
@@ -11339,32 +11891,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11339
11891
  const workspaceLoader = new WorkspaceLoader(currentDir);
11340
11892
  const workspaceConfig = await workspaceLoader.load();
11341
11893
  if (!workspaceConfig) {
11342
- console.error(chalk18.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.`));
11343
11895
  process.exit(1);
11344
11896
  }
11345
11897
  const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
11346
11898
  if (backendRepos.length === 0) {
11347
- console.log(chalk18.yellow(" No backend repos found in workspace."));
11899
+ console.log(chalk21.yellow(" No backend repos found in workspace."));
11348
11900
  return;
11349
11901
  }
11350
11902
  for (const repo of backendRepos) {
11351
11903
  const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
11352
- console.log(chalk18.cyan(`
11904
+ console.log(chalk21.cyan(`
11353
11905
  Repo: ${repo.name} (${repoAbsPath})`));
11354
11906
  const dslFile = await findLatestDslFile(repoAbsPath);
11355
11907
  if (!dslFile) {
11356
- console.log(chalk18.yellow(` No DSL file found \u2014 skipping.`));
11908
+ console.log(chalk21.yellow(` No DSL file found \u2014 skipping.`));
11357
11909
  continue;
11358
11910
  }
11359
- const dsl2 = await fs23.readJson(dslFile);
11911
+ const dsl2 = await fs24.readJson(dslFile);
11360
11912
  const result2 = await generateMockAssets(dsl2, repoAbsPath, {
11361
11913
  port,
11362
11914
  msw: opts.msw,
11363
11915
  proxy: opts.proxy
11364
11916
  });
11365
11917
  for (const f of result2.files) {
11366
- console.log(chalk18.green(` \u2714 ${f.path}`));
11367
- console.log(chalk18.gray(` ${f.description}`));
11918
+ console.log(chalk21.green(` \u2714 ${f.path}`));
11919
+ console.log(chalk21.gray(` ${f.description}`));
11368
11920
  }
11369
11921
  }
11370
11922
  return;
@@ -11374,19 +11926,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11374
11926
  dslPath = await findLatestDslFile(currentDir);
11375
11927
  if (!dslPath) {
11376
11928
  console.error(
11377
- chalk18.red(
11929
+ chalk21.red(
11378
11930
  " No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
11379
11931
  )
11380
11932
  );
11381
11933
  process.exit(1);
11382
11934
  }
11383
- console.log(chalk18.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
11935
+ console.log(chalk21.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
11384
11936
  }
11385
11937
  let dsl;
11386
11938
  try {
11387
- dsl = await fs23.readJson(dslPath);
11939
+ dsl = await fs24.readJson(dslPath);
11388
11940
  } catch (err) {
11389
- console.error(chalk18.red(` Failed to read DSL file: ${err.message}`));
11941
+ console.error(chalk21.red(` Failed to read DSL file: ${err.message}`));
11390
11942
  process.exit(1);
11391
11943
  }
11392
11944
  const result = await generateMockAssets(dsl, currentDir, {
@@ -11394,58 +11946,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
11394
11946
  msw: opts.msw,
11395
11947
  proxy: opts.proxy
11396
11948
  });
11397
- console.log(chalk18.green(`
11949
+ console.log(chalk21.green(`
11398
11950
  \u2714 Mock assets generated (${result.files.length} file(s)):`));
11399
11951
  for (const f of result.files) {
11400
- console.log(chalk18.green(` ${f.path}`));
11401
- console.log(chalk18.gray(` ${f.description}`));
11952
+ console.log(chalk21.green(` ${f.path}`));
11953
+ console.log(chalk21.gray(` ${f.description}`));
11402
11954
  }
11403
11955
  if (opts.serve) {
11404
- const serverJsPath = path22.join(currentDir, "mock", "server.js");
11405
- if (!await fs23.pathExists(serverJsPath)) {
11406
- console.error(chalk18.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."));
11407
11959
  process.exit(1);
11408
11960
  }
11409
11961
  const pid = startMockServerBackground(serverJsPath, port);
11410
- console.log(chalk18.green(`
11962
+ console.log(chalk21.green(`
11411
11963
  \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
11412
11964
  if (opts.frontend) {
11413
- const frontendDir = path22.resolve(opts.frontend);
11965
+ const frontendDir = path23.resolve(opts.frontend);
11414
11966
  const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
11415
11967
  await saveMockServerPid(frontendDir, pid);
11416
11968
  if (proxyResult.applied) {
11417
- console.log(chalk18.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11418
- console.log(chalk18.bold.cyan(`
11969
+ console.log(chalk21.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
11970
+ console.log(chalk21.bold.cyan(`
11419
11971
  Ready! Open a new terminal and run:`));
11420
- console.log(chalk18.white(` cd ${frontendDir}`));
11421
- console.log(chalk18.white(` ${proxyResult.devCommand}`));
11422
- console.log(chalk18.gray(`
11972
+ console.log(chalk21.white(` cd ${frontendDir}`));
11973
+ console.log(chalk21.white(` ${proxyResult.devCommand}`));
11974
+ console.log(chalk21.gray(`
11423
11975
  When done: ai-spec mock --restore --frontend ${frontendDir}`));
11424
11976
  } else {
11425
- console.log(chalk18.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
11426
- if (proxyResult.note) console.log(chalk18.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}`));
11427
11979
  }
11428
11980
  } else {
11429
- console.log(chalk18.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
11430
- console.log(chalk18.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}`));
11431
11983
  }
11432
11984
  return;
11433
11985
  }
11434
- console.log(chalk18.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"));
11435
- console.log(chalk18.white(` 1. Install express (if not already):`));
11436
- console.log(chalk18.gray(` npm install --save-dev express`));
11437
- console.log(chalk18.white(` 2. Start mock server:`));
11438
- console.log(chalk18.gray(` node mock/server.js`));
11439
- console.log(chalk18.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
11440
- console.log(chalk18.white(` 3. Configure your frontend to proxy API calls to:`));
11441
- console.log(chalk18.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}`));
11442
11994
  if (opts.proxy) {
11443
- console.log(chalk18.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)`));
11444
11996
  }
11445
11997
  if (opts.msw) {
11446
- console.log(chalk18.white(` 4. MSW: import and start the worker in your app entry:`));
11447
- console.log(chalk18.gray(` import { worker } from './mocks/browser';`));
11448
- console.log(chalk18.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();`));
11449
12001
  }
11450
12002
  });
11451
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) => {
@@ -11461,26 +12013,118 @@ program.command("learn").description("Append a lesson or engineering decision di
11461
12013
  }
11462
12014
  const result = await appendDirectLesson(currentDir, lesson.trim());
11463
12015
  if (result.appended) {
11464
- console.log(chalk18.green(`
12016
+ console.log(chalk21.green(`
11465
12017
  \u2714 Lesson appended to constitution \xA79`));
11466
- console.log(chalk18.gray(` File: .ai-spec-constitution.md`));
12018
+ console.log(chalk21.gray(` File: .ai-spec-constitution.md`));
11467
12019
  } else {
11468
- console.log(chalk18.yellow(`
12020
+ console.log(chalk21.yellow(`
11469
12021
  \u26A0 Not appended: ${result.reason}`));
11470
12022
  }
11471
12023
  });
11472
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) => {
11473
12025
  const currentDir = process.cwd();
11474
12026
  const snapshot = new RunSnapshot(currentDir, runId);
11475
- console.log(chalk18.blue(`Restoring run: ${runId}...`));
12027
+ console.log(chalk21.blue(`Restoring run: ${runId}...`));
11476
12028
  const restored = await snapshot.restore();
11477
12029
  if (restored.length === 0) {
11478
- console.log(chalk18.yellow(" No backup found for this run ID."));
12030
+ console.log(chalk21.yellow(" No backup found for this run ID."));
11479
12031
  } else {
11480
- restored.forEach((f) => console.log(chalk18.green(` \u2714 restored: ${f}`)));
11481
- console.log(chalk18.bold.green(`
12032
+ restored.forEach((f) => console.log(chalk21.green(` \u2714 restored: ${f}`)));
12033
+ console.log(chalk21.bold.green(`
11482
12034
  \u2714 ${restored.length} file(s) restored.`));
11483
12035
  }
11484
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
+ });
11485
12129
  program.parse();
11486
12130
  //# sourceMappingURL=index.mjs.map