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