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