ai-spec-dev 0.42.0 → 0.55.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/README.md +86 -40
- package/cli/commands/config.ts +129 -1
- package/cli/commands/create.ts +246 -11
- package/cli/commands/fix-history.ts +176 -0
- package/cli/commands/init.ts +344 -106
- package/cli/index.ts +3 -7
- package/cli/pipeline/helpers.ts +6 -0
- package/cli/pipeline/multi-repo.ts +291 -26
- package/cli/pipeline/single-repo.ts +103 -2
- package/cli/utils.ts +95 -4
- package/core/code-generator.ts +63 -14
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/cross-stack-verifier.ts +395 -0
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +3 -2
- package/core/fix-history.ts +333 -0
- package/core/import-fixer.ts +827 -0
- package/core/import-verifier.ts +569 -0
- package/core/knowledge-memory.ts +55 -6
- package/core/openapi-exporter.ts +3 -2
- package/core/repo-store.ts +95 -0
- package/core/reviewer.ts +14 -13
- package/core/run-logger.ts +3 -4
- package/core/run-snapshot.ts +2 -3
- package/core/run-trend.ts +3 -4
- package/core/self-evaluator.ts +44 -7
- package/core/spec-generator.ts +30 -45
- package/core/token-budget.ts +3 -8
- package/core/types-generator.ts +2 -2
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +3889 -1937
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3888 -1936
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +292 -181
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -181
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/cross-stack-verifier.test.ts +301 -0
- package/tests/fix-history.test.ts +335 -0
- package/tests/import-fixer.test.ts +944 -0
- package/tests/import-verifier.test.ts +420 -0
- package/tests/knowledge-memory.test.ts +40 -0
- package/tests/self-evaluator.test.ts +97 -0
- package/cli/commands/model.ts +0 -156
- package/cli/commands/scan.ts +0 -99
- package/cli/commands/workspace.ts +0 -219
- package/demo-backend/.ai-spec-constitution.md +0 -65
- package/demo-backend/package.json +0 -21
- package/demo-backend/prisma/schema.prisma +0 -22
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
- package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
- package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
- package/demo-backend/src/index.ts +0 -17
- package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
- package/demo-backend/src/routes/bookmark.routes.ts +0 -11
- package/demo-backend/src/routes/index.ts +0 -8
- package/demo-backend/src/services/bookmark.service.test.ts +0 -433
- package/demo-backend/src/services/bookmark.service.ts +0 -261
- package/demo-backend/tsconfig.json +0 -12
- package/demo-frontend/.ai-spec-constitution.md +0 -95
- package/demo-frontend/package.json +0 -23
- package/demo-frontend/src/App.tsx +0 -12
- package/demo-frontend/src/main.tsx +0 -9
- package/demo-frontend/tsconfig.json +0 -13
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
3
3
|
import Anthropic from "@anthropic-ai/sdk";
|
|
4
4
|
import OpenAI from "openai";
|
|
5
|
-
import axios from "axios";
|
|
6
5
|
import { ProxyAgent } from "undici";
|
|
7
6
|
|
|
8
7
|
// prompts/spec.prompt.ts
|
|
@@ -295,7 +294,9 @@ var PROVIDER_CATALOG = {
|
|
|
295
294
|
displayName: "MiMo (Xiaomi)",
|
|
296
295
|
description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
|
|
297
296
|
models: ["mimo-v2-pro"],
|
|
298
|
-
envKey: "MIMO_API_KEY"
|
|
297
|
+
envKey: "MIMO_API_KEY",
|
|
298
|
+
// Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
|
|
299
|
+
fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
|
|
299
300
|
// baseURL not used — MiMo has a dedicated provider class
|
|
300
301
|
},
|
|
301
302
|
gemini: {
|
|
@@ -338,12 +339,12 @@ var PROVIDER_CATALOG = {
|
|
|
338
339
|
},
|
|
339
340
|
deepseek: {
|
|
340
341
|
displayName: "DeepSeek",
|
|
341
|
-
description: "DeepSeek
|
|
342
|
+
description: "DeepSeek V3.2 (chat) / R1 (reasoner) \u2014 alias auto-tracks latest stable",
|
|
342
343
|
models: [
|
|
343
344
|
"deepseek-chat",
|
|
344
|
-
// DeepSeek
|
|
345
|
+
// V3.2 (alias auto-updates as DeepSeek releases new versions)
|
|
345
346
|
"deepseek-reasoner"
|
|
346
|
-
//
|
|
347
|
+
// R1 (reasoning model)
|
|
347
348
|
],
|
|
348
349
|
envKey: "DEEPSEEK_API_KEY",
|
|
349
350
|
baseURL: "https://api.deepseek.com/v1"
|
|
@@ -464,8 +465,8 @@ var ClaudeProvider = class {
|
|
|
464
465
|
...systemInstruction ? { system: systemInstruction } : {},
|
|
465
466
|
messages: [{ role: "user", content: prompt }]
|
|
466
467
|
});
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
468
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
469
|
+
if (textBlock) return textBlock.text;
|
|
469
470
|
throw new Error("Unexpected response type from Claude API");
|
|
470
471
|
},
|
|
471
472
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
@@ -510,43 +511,29 @@ var OpenAICompatibleProvider = class {
|
|
|
510
511
|
}
|
|
511
512
|
};
|
|
512
513
|
var MiMoProvider = class {
|
|
514
|
+
client;
|
|
513
515
|
providerName = "mimo";
|
|
514
516
|
modelName;
|
|
515
|
-
apiKey;
|
|
516
|
-
baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
|
|
517
517
|
constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
|
|
518
|
-
|
|
518
|
+
const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
|
|
519
|
+
this.client = new Anthropic({ apiKey, baseURL });
|
|
519
520
|
this.modelName = modelName;
|
|
520
521
|
}
|
|
521
522
|
async generate(prompt, systemInstruction) {
|
|
522
523
|
return withReliability(
|
|
523
524
|
async () => {
|
|
524
|
-
const
|
|
525
|
+
const stream = this.client.messages.stream({
|
|
525
526
|
model: this.modelName,
|
|
526
|
-
max_tokens:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
stream: false,
|
|
530
|
-
temperature: 1,
|
|
531
|
-
stop_sequences: null
|
|
532
|
-
};
|
|
533
|
-
if (systemInstruction) {
|
|
534
|
-
body.system = systemInstruction;
|
|
535
|
-
}
|
|
536
|
-
const response = await axios.post(this.baseUrl, body, {
|
|
537
|
-
headers: {
|
|
538
|
-
"api-key": this.apiKey,
|
|
539
|
-
"Content-Type": "application/json"
|
|
540
|
-
}
|
|
527
|
+
max_tokens: 65536,
|
|
528
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
529
|
+
messages: [{ role: "user", content: prompt }]
|
|
541
530
|
});
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
531
|
+
const message = await stream.finalMessage();
|
|
532
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
533
|
+
if (textBlock) return textBlock.text;
|
|
534
|
+
const thinkBlock = message.content.find((b) => b.type === "thinking");
|
|
535
|
+
if (thinkBlock) return thinkBlock.thinking;
|
|
536
|
+
return message.content.map((b) => b.text ?? "").join("");
|
|
550
537
|
},
|
|
551
538
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
552
539
|
);
|
|
@@ -4180,8 +4167,8 @@ ${currentSpec}`,
|
|
|
4180
4167
|
// core/code-generator.ts
|
|
4181
4168
|
import chalk10 from "chalk";
|
|
4182
4169
|
import { execSync as execSync2, spawnSync } from "child_process";
|
|
4183
|
-
import * as
|
|
4184
|
-
import * as
|
|
4170
|
+
import * as path7 from "path";
|
|
4171
|
+
import * as fs11 from "fs-extra";
|
|
4185
4172
|
|
|
4186
4173
|
// prompts/codegen.prompt.ts
|
|
4187
4174
|
var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
|
|
@@ -4851,32 +4838,32 @@ function validateDsl(raw) {
|
|
|
4851
4838
|
}
|
|
4852
4839
|
return { valid: true, dsl: raw };
|
|
4853
4840
|
}
|
|
4854
|
-
function validateFeature(raw,
|
|
4841
|
+
function validateFeature(raw, path11, errors) {
|
|
4855
4842
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4856
|
-
errors.push({ path:
|
|
4843
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4857
4844
|
return;
|
|
4858
4845
|
}
|
|
4859
4846
|
const f = raw;
|
|
4860
|
-
requireNonEmptyString(f["id"], `${
|
|
4861
|
-
requireNonEmptyString(f["title"], `${
|
|
4862
|
-
requireNonEmptyString(f["description"], `${
|
|
4847
|
+
requireNonEmptyString(f["id"], `${path11}.id`, errors);
|
|
4848
|
+
requireNonEmptyString(f["title"], `${path11}.title`, errors);
|
|
4849
|
+
requireNonEmptyString(f["description"], `${path11}.description`, errors);
|
|
4863
4850
|
}
|
|
4864
|
-
function validateModel(raw,
|
|
4851
|
+
function validateModel(raw, path11, errors) {
|
|
4865
4852
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4866
|
-
errors.push({ path:
|
|
4853
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4867
4854
|
return;
|
|
4868
4855
|
}
|
|
4869
4856
|
const m = raw;
|
|
4870
|
-
requireNonEmptyString(m["name"], `${
|
|
4857
|
+
requireNonEmptyString(m["name"], `${path11}.name`, errors);
|
|
4871
4858
|
if (!Array.isArray(m["fields"])) {
|
|
4872
|
-
errors.push({ path: `${
|
|
4859
|
+
errors.push({ path: `${path11}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4873
4860
|
} else {
|
|
4874
4861
|
const fields = m["fields"];
|
|
4875
4862
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4876
|
-
errors.push({ path: `${
|
|
4863
|
+
errors.push({ path: `${path11}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4877
4864
|
}
|
|
4878
4865
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4879
|
-
validateModelField(fields[j2], `${
|
|
4866
|
+
validateModelField(fields[j2], `${path11}.fields[${j2}]`, errors);
|
|
4880
4867
|
}
|
|
4881
4868
|
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
4882
4869
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
@@ -4885,7 +4872,7 @@ function validateModel(raw, path10, errors) {
|
|
|
4885
4872
|
const name = f["name"];
|
|
4886
4873
|
if (seenFieldNames.has(name)) {
|
|
4887
4874
|
errors.push({
|
|
4888
|
-
path: `${
|
|
4875
|
+
path: `${path11}.fields[${j2}].name`,
|
|
4889
4876
|
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
4890
4877
|
});
|
|
4891
4878
|
} else {
|
|
@@ -4896,184 +4883,184 @@ function validateModel(raw, path10, errors) {
|
|
|
4896
4883
|
}
|
|
4897
4884
|
if (m["relations"] !== void 0) {
|
|
4898
4885
|
if (!Array.isArray(m["relations"])) {
|
|
4899
|
-
errors.push({ path: `${
|
|
4886
|
+
errors.push({ path: `${path11}.relations`, message: "Must be an array of strings if present" });
|
|
4900
4887
|
} else {
|
|
4901
4888
|
const rels = m["relations"];
|
|
4902
4889
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4903
4890
|
if (typeof rels[j2] !== "string") {
|
|
4904
|
-
errors.push({ path: `${
|
|
4891
|
+
errors.push({ path: `${path11}.relations[${j2}]`, message: "Must be a string" });
|
|
4905
4892
|
}
|
|
4906
4893
|
}
|
|
4907
4894
|
}
|
|
4908
4895
|
}
|
|
4909
4896
|
}
|
|
4910
|
-
function validateModelField(raw,
|
|
4897
|
+
function validateModelField(raw, path11, errors) {
|
|
4911
4898
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4912
|
-
errors.push({ path:
|
|
4899
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4913
4900
|
return;
|
|
4914
4901
|
}
|
|
4915
4902
|
const f = raw;
|
|
4916
|
-
requireNonEmptyString(f["name"], `${
|
|
4917
|
-
requireNonEmptyString(f["type"], `${
|
|
4903
|
+
requireNonEmptyString(f["name"], `${path11}.name`, errors);
|
|
4904
|
+
requireNonEmptyString(f["type"], `${path11}.type`, errors);
|
|
4918
4905
|
if (typeof f["required"] !== "boolean") {
|
|
4919
|
-
errors.push({ path: `${
|
|
4906
|
+
errors.push({ path: `${path11}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4920
4907
|
}
|
|
4921
4908
|
}
|
|
4922
|
-
function validateEndpoint(raw,
|
|
4909
|
+
function validateEndpoint(raw, path11, errors) {
|
|
4923
4910
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4924
|
-
errors.push({ path:
|
|
4911
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4925
4912
|
return;
|
|
4926
4913
|
}
|
|
4927
4914
|
const e = raw;
|
|
4928
|
-
requireNonEmptyString(e["id"], `${
|
|
4929
|
-
requireNonEmptyString(e["description"], `${
|
|
4915
|
+
requireNonEmptyString(e["id"], `${path11}.id`, errors);
|
|
4916
|
+
requireNonEmptyString(e["description"], `${path11}.description`, errors);
|
|
4930
4917
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4931
4918
|
errors.push({
|
|
4932
|
-
path: `${
|
|
4919
|
+
path: `${path11}.method`,
|
|
4933
4920
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4934
4921
|
});
|
|
4935
4922
|
}
|
|
4936
4923
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4937
4924
|
errors.push({
|
|
4938
|
-
path: `${
|
|
4925
|
+
path: `${path11}.path`,
|
|
4939
4926
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4940
4927
|
});
|
|
4941
4928
|
}
|
|
4942
4929
|
if (typeof e["auth"] !== "boolean") {
|
|
4943
|
-
errors.push({ path: `${
|
|
4930
|
+
errors.push({ path: `${path11}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4944
4931
|
}
|
|
4945
4932
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4946
4933
|
errors.push({
|
|
4947
|
-
path: `${
|
|
4934
|
+
path: `${path11}.successStatus`,
|
|
4948
4935
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4949
4936
|
});
|
|
4950
4937
|
}
|
|
4951
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
4938
|
+
requireNonEmptyString(e["successDescription"], `${path11}.successDescription`, errors);
|
|
4952
4939
|
if (e["request"] !== void 0) {
|
|
4953
|
-
validateRequestSchema(e["request"], `${
|
|
4940
|
+
validateRequestSchema(e["request"], `${path11}.request`, errors);
|
|
4954
4941
|
}
|
|
4955
4942
|
if (e["errors"] !== void 0) {
|
|
4956
4943
|
if (!Array.isArray(e["errors"])) {
|
|
4957
|
-
errors.push({ path: `${
|
|
4944
|
+
errors.push({ path: `${path11}.errors`, message: "Must be an array if present" });
|
|
4958
4945
|
} else {
|
|
4959
4946
|
const errs = e["errors"];
|
|
4960
4947
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4961
|
-
errors.push({ path: `${
|
|
4948
|
+
errors.push({ path: `${path11}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4962
4949
|
}
|
|
4963
4950
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4964
|
-
validateResponseError(errs[j2], `${
|
|
4951
|
+
validateResponseError(errs[j2], `${path11}.errors[${j2}]`, errors);
|
|
4965
4952
|
}
|
|
4966
4953
|
}
|
|
4967
4954
|
}
|
|
4968
4955
|
}
|
|
4969
|
-
function validateRequestSchema(raw,
|
|
4956
|
+
function validateRequestSchema(raw, path11, errors) {
|
|
4970
4957
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4971
|
-
errors.push({ path:
|
|
4958
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4972
4959
|
return;
|
|
4973
4960
|
}
|
|
4974
4961
|
const r = raw;
|
|
4975
4962
|
for (const key of ["body", "query", "params"]) {
|
|
4976
4963
|
if (r[key] !== void 0) {
|
|
4977
|
-
validateFieldMap(r[key], `${
|
|
4964
|
+
validateFieldMap(r[key], `${path11}.${key}`, errors);
|
|
4978
4965
|
}
|
|
4979
4966
|
}
|
|
4980
4967
|
}
|
|
4981
|
-
function validateFieldMap(raw,
|
|
4968
|
+
function validateFieldMap(raw, path11, errors) {
|
|
4982
4969
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4983
|
-
errors.push({ path:
|
|
4970
|
+
errors.push({ path: path11, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4984
4971
|
return;
|
|
4985
4972
|
}
|
|
4986
4973
|
const map = raw;
|
|
4987
4974
|
for (const [k2, v2] of Object.entries(map)) {
|
|
4988
4975
|
if (typeof v2 !== "string") {
|
|
4989
|
-
errors.push({ path: `${
|
|
4976
|
+
errors.push({ path: `${path11}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4990
4977
|
}
|
|
4991
4978
|
}
|
|
4992
4979
|
}
|
|
4993
|
-
function validateResponseError(raw,
|
|
4980
|
+
function validateResponseError(raw, path11, errors) {
|
|
4994
4981
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4995
|
-
errors.push({ path:
|
|
4982
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4996
4983
|
return;
|
|
4997
4984
|
}
|
|
4998
4985
|
const e = raw;
|
|
4999
4986
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5000
|
-
errors.push({ path: `${
|
|
4987
|
+
errors.push({ path: `${path11}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5001
4988
|
}
|
|
5002
|
-
requireNonEmptyString(e["code"], `${
|
|
5003
|
-
requireNonEmptyString(e["description"], `${
|
|
4989
|
+
requireNonEmptyString(e["code"], `${path11}.code`, errors);
|
|
4990
|
+
requireNonEmptyString(e["description"], `${path11}.description`, errors);
|
|
5004
4991
|
}
|
|
5005
|
-
function validateBehavior(raw,
|
|
4992
|
+
function validateBehavior(raw, path11, errors) {
|
|
5006
4993
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5007
|
-
errors.push({ path:
|
|
4994
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5008
4995
|
return;
|
|
5009
4996
|
}
|
|
5010
4997
|
const b = raw;
|
|
5011
|
-
requireNonEmptyString(b["id"], `${
|
|
5012
|
-
requireNonEmptyString(b["description"], `${
|
|
4998
|
+
requireNonEmptyString(b["id"], `${path11}.id`, errors);
|
|
4999
|
+
requireNonEmptyString(b["description"], `${path11}.description`, errors);
|
|
5013
5000
|
if (b["constraints"] !== void 0) {
|
|
5014
5001
|
if (!Array.isArray(b["constraints"])) {
|
|
5015
|
-
errors.push({ path: `${
|
|
5002
|
+
errors.push({ path: `${path11}.constraints`, message: "Must be an array of strings if present" });
|
|
5016
5003
|
} else {
|
|
5017
5004
|
const cs2 = b["constraints"];
|
|
5018
5005
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5019
5006
|
if (typeof cs2[j2] !== "string") {
|
|
5020
|
-
errors.push({ path: `${
|
|
5007
|
+
errors.push({ path: `${path11}.constraints[${j2}]`, message: "Must be a string" });
|
|
5021
5008
|
}
|
|
5022
5009
|
}
|
|
5023
5010
|
}
|
|
5024
5011
|
}
|
|
5025
5012
|
}
|
|
5026
|
-
function validateComponent(raw,
|
|
5013
|
+
function validateComponent(raw, path11, errors) {
|
|
5027
5014
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5028
|
-
errors.push({ path:
|
|
5015
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5029
5016
|
return;
|
|
5030
5017
|
}
|
|
5031
5018
|
const c = raw;
|
|
5032
|
-
requireNonEmptyString(c["id"], `${
|
|
5033
|
-
requireNonEmptyString(c["name"], `${
|
|
5034
|
-
requireNonEmptyString(c["description"], `${
|
|
5019
|
+
requireNonEmptyString(c["id"], `${path11}.id`, errors);
|
|
5020
|
+
requireNonEmptyString(c["name"], `${path11}.name`, errors);
|
|
5021
|
+
requireNonEmptyString(c["description"], `${path11}.description`, errors);
|
|
5035
5022
|
if (c["props"] !== void 0) {
|
|
5036
5023
|
if (!Array.isArray(c["props"])) {
|
|
5037
|
-
errors.push({ path: `${
|
|
5024
|
+
errors.push({ path: `${path11}.props`, message: "Must be an array if present" });
|
|
5038
5025
|
} else {
|
|
5039
5026
|
const props = c["props"];
|
|
5040
5027
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5041
5028
|
const p = props[j2];
|
|
5042
5029
|
if (typeof p !== "object" || p === null) {
|
|
5043
|
-
errors.push({ path: `${
|
|
5030
|
+
errors.push({ path: `${path11}.props[${j2}]`, message: "Must be an object" });
|
|
5044
5031
|
continue;
|
|
5045
5032
|
}
|
|
5046
|
-
requireNonEmptyString(p["name"], `${
|
|
5047
|
-
requireNonEmptyString(p["type"], `${
|
|
5033
|
+
requireNonEmptyString(p["name"], `${path11}.props[${j2}].name`, errors);
|
|
5034
|
+
requireNonEmptyString(p["type"], `${path11}.props[${j2}].type`, errors);
|
|
5048
5035
|
if (typeof p["required"] !== "boolean") {
|
|
5049
|
-
errors.push({ path: `${
|
|
5036
|
+
errors.push({ path: `${path11}.props[${j2}].required`, message: "Must be boolean" });
|
|
5050
5037
|
}
|
|
5051
5038
|
}
|
|
5052
5039
|
}
|
|
5053
5040
|
}
|
|
5054
5041
|
if (c["events"] !== void 0) {
|
|
5055
5042
|
if (!Array.isArray(c["events"])) {
|
|
5056
|
-
errors.push({ path: `${
|
|
5043
|
+
errors.push({ path: `${path11}.events`, message: "Must be an array if present" });
|
|
5057
5044
|
} else {
|
|
5058
5045
|
const events = c["events"];
|
|
5059
5046
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5060
5047
|
const e = events[j2];
|
|
5061
5048
|
if (typeof e !== "object" || e === null) {
|
|
5062
|
-
errors.push({ path: `${
|
|
5049
|
+
errors.push({ path: `${path11}.events[${j2}]`, message: "Must be an object" });
|
|
5063
5050
|
continue;
|
|
5064
5051
|
}
|
|
5065
|
-
requireNonEmptyString(e["name"], `${
|
|
5052
|
+
requireNonEmptyString(e["name"], `${path11}.events[${j2}].name`, errors);
|
|
5066
5053
|
}
|
|
5067
5054
|
}
|
|
5068
5055
|
}
|
|
5069
5056
|
if (c["state"] !== void 0) {
|
|
5070
5057
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5071
|
-
errors.push({ path: `${
|
|
5058
|
+
errors.push({ path: `${path11}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5072
5059
|
}
|
|
5073
5060
|
}
|
|
5074
5061
|
if (c["apiCalls"] !== void 0) {
|
|
5075
5062
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5076
|
-
errors.push({ path: `${
|
|
5063
|
+
errors.push({ path: `${path11}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5077
5064
|
}
|
|
5078
5065
|
}
|
|
5079
5066
|
}
|
|
@@ -5135,10 +5122,10 @@ function crossReferenceChecks(obj, errors) {
|
|
|
5135
5122
|
}
|
|
5136
5123
|
}
|
|
5137
5124
|
}
|
|
5138
|
-
function requireNonEmptyString(v2,
|
|
5125
|
+
function requireNonEmptyString(v2, path11, errors) {
|
|
5139
5126
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5140
5127
|
errors.push({
|
|
5141
|
-
path:
|
|
5128
|
+
path: path11,
|
|
5142
5129
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5143
5130
|
});
|
|
5144
5131
|
}
|
|
@@ -5151,13 +5138,11 @@ function typeLabel(v2) {
|
|
|
5151
5138
|
|
|
5152
5139
|
// core/token-budget.ts
|
|
5153
5140
|
import chalk6 from "chalk";
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5160
|
-
}
|
|
5141
|
+
|
|
5142
|
+
// core/config-defaults.ts
|
|
5143
|
+
var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5144
|
+
var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
|
|
5145
|
+
var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
|
|
5161
5146
|
var DEFAULT_TOKEN_BUDGETS = {
|
|
5162
5147
|
gemini: 9e5,
|
|
5163
5148
|
claude: 18e4,
|
|
@@ -5165,8 +5150,18 @@ var DEFAULT_TOKEN_BUDGETS = {
|
|
|
5165
5150
|
deepseek: 6e4,
|
|
5166
5151
|
default: 1e5
|
|
5167
5152
|
};
|
|
5153
|
+
|
|
5154
|
+
// core/token-budget.ts
|
|
5155
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5156
|
+
function estimateTokens(text) {
|
|
5157
|
+
if (!text) return 0;
|
|
5158
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5159
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5160
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5161
|
+
}
|
|
5162
|
+
var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
|
|
5168
5163
|
function getDefaultBudget(providerName) {
|
|
5169
|
-
return
|
|
5164
|
+
return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
|
|
5170
5165
|
}
|
|
5171
5166
|
|
|
5172
5167
|
// core/dsl-extractor.ts
|
|
@@ -6019,6 +6014,101 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6019
6014
|
}
|
|
6020
6015
|
}
|
|
6021
6016
|
|
|
6017
|
+
// core/fix-history.ts
|
|
6018
|
+
import * as fs10 from "fs-extra";
|
|
6019
|
+
import * as path6 from "path";
|
|
6020
|
+
var FIX_HISTORY_FILE = ".ai-spec-fix-history.json";
|
|
6021
|
+
var FIX_HISTORY_VERSION = "1.0";
|
|
6022
|
+
async function loadFixHistory(repoRoot) {
|
|
6023
|
+
const filePath = path6.join(repoRoot, FIX_HISTORY_FILE);
|
|
6024
|
+
if (!await fs10.pathExists(filePath)) {
|
|
6025
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6026
|
+
}
|
|
6027
|
+
try {
|
|
6028
|
+
const data = await fs10.readJson(filePath);
|
|
6029
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.entries)) {
|
|
6030
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6031
|
+
}
|
|
6032
|
+
return {
|
|
6033
|
+
version: typeof data.version === "string" ? data.version : FIX_HISTORY_VERSION,
|
|
6034
|
+
entries: data.entries
|
|
6035
|
+
};
|
|
6036
|
+
} catch {
|
|
6037
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
function aggregateFixPatterns(history) {
|
|
6041
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6042
|
+
for (const entry of history.entries) {
|
|
6043
|
+
if (!byKey.has(entry.patternKey)) byKey.set(entry.patternKey, []);
|
|
6044
|
+
byKey.get(entry.patternKey).push(entry);
|
|
6045
|
+
}
|
|
6046
|
+
const aggregates = [];
|
|
6047
|
+
for (const [patternKey, entries] of byKey) {
|
|
6048
|
+
entries.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
6049
|
+
const first = entries[0];
|
|
6050
|
+
const last = entries[entries.length - 1];
|
|
6051
|
+
const uniqueRunIds = new Set(entries.map((e) => e.runId)).size;
|
|
6052
|
+
aggregates.push({
|
|
6053
|
+
patternKey,
|
|
6054
|
+
count: entries.length,
|
|
6055
|
+
firstSeen: first.ts,
|
|
6056
|
+
lastSeen: last.ts,
|
|
6057
|
+
uniqueRunIds,
|
|
6058
|
+
source: last.brokenImport.source,
|
|
6059
|
+
names: last.brokenImport.names,
|
|
6060
|
+
reason: last.brokenImport.reason,
|
|
6061
|
+
fix: {
|
|
6062
|
+
kind: last.fix.kind,
|
|
6063
|
+
target: last.fix.target,
|
|
6064
|
+
stage: last.fix.stage
|
|
6065
|
+
}
|
|
6066
|
+
});
|
|
6067
|
+
}
|
|
6068
|
+
aggregates.sort((a, b) => {
|
|
6069
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
6070
|
+
return b.lastSeen.localeCompare(a.lastSeen);
|
|
6071
|
+
});
|
|
6072
|
+
return aggregates;
|
|
6073
|
+
}
|
|
6074
|
+
function buildHallucinationAvoidanceSection(history, opts = {}) {
|
|
6075
|
+
const minCount = opts.minCount ?? 1;
|
|
6076
|
+
const maxItems = opts.maxItems ?? 10;
|
|
6077
|
+
const patterns = aggregateFixPatterns(history).filter((p) => p.count >= minCount);
|
|
6078
|
+
if (patterns.length === 0) return null;
|
|
6079
|
+
const top = patterns.slice(0, maxItems);
|
|
6080
|
+
const lines = [
|
|
6081
|
+
"=== Prior Hallucinations in This Project (DO NOT REPEAT) ===",
|
|
6082
|
+
"",
|
|
6083
|
+
"The following imports were previously hallucinated by AI codegen in this",
|
|
6084
|
+
"project and had to be auto-fixed. When generating new files, actively avoid",
|
|
6085
|
+
"these exact imports \u2014 they were wrong in the past and will be wrong again.",
|
|
6086
|
+
""
|
|
6087
|
+
];
|
|
6088
|
+
for (const p of top) {
|
|
6089
|
+
const namesLabel = p.names.length > 0 ? `{ ${p.names.join(", ")} }` : "(no names)";
|
|
6090
|
+
const reasonLabel = p.reason === "file_not_found" ? "file did not exist" : "named export did not exist";
|
|
6091
|
+
const countLabel = p.count === 1 ? "1x" : `${p.count}x`;
|
|
6092
|
+
const dateLabel = p.lastSeen.slice(0, 10);
|
|
6093
|
+
lines.push(`\u274C Do NOT: import ${namesLabel} from '${p.source}'`);
|
|
6094
|
+
lines.push(` Reason: ${reasonLabel} (seen ${countLabel}, last ${dateLabel})`);
|
|
6095
|
+
if (p.fix.kind === "create_file") {
|
|
6096
|
+
lines.push(` Previously fixed by creating: ${p.fix.target}`);
|
|
6097
|
+
} else if (p.fix.kind === "rewrite_import") {
|
|
6098
|
+
lines.push(` Previously fixed by rewriting the import path`);
|
|
6099
|
+
} else {
|
|
6100
|
+
lines.push(` Previously fixed by appending to: ${p.fix.target}`);
|
|
6101
|
+
}
|
|
6102
|
+
lines.push("");
|
|
6103
|
+
}
|
|
6104
|
+
if (patterns.length > maxItems) {
|
|
6105
|
+
lines.push(`(${patterns.length - maxItems} more pattern(s) hidden \u2014 run \`ai-spec fix-history\` to see all)`);
|
|
6106
|
+
lines.push("");
|
|
6107
|
+
}
|
|
6108
|
+
lines.push("=== End of Prior Hallucinations ===");
|
|
6109
|
+
return lines.join("\n");
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6022
6112
|
// core/code-generator.ts
|
|
6023
6113
|
var CodeGenerator = class {
|
|
6024
6114
|
constructor(provider, mode = "claude-code") {
|
|
@@ -6083,8 +6173,8 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
6083
6173
|
Files: ${t.filesToTouch.join(", ")}
|
|
6084
6174
|
Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
|
|
6085
6175
|
const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
|
|
6086
|
-
const promptFile =
|
|
6087
|
-
await
|
|
6176
|
+
const promptFile = path7.join(workingDir, ".claude-prompt.txt");
|
|
6177
|
+
await fs11.writeFile(promptFile, promptContent, "utf-8");
|
|
6088
6178
|
if (options.auto) {
|
|
6089
6179
|
console.log(chalk10.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6090
6180
|
console.log(chalk10.gray(` Spec: ${specFilePath}`));
|
|
@@ -6177,7 +6267,7 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
6177
6267
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
6178
6268
|
console.log(chalk10.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
6179
6269
|
}
|
|
6180
|
-
const spec = await
|
|
6270
|
+
const spec = await fs11.readFile(specFilePath, "utf-8");
|
|
6181
6271
|
let constitutionSection = context?.constitution ? `
|
|
6182
6272
|
=== Project Constitution (MUST follow) ===
|
|
6183
6273
|
${context.constitution}
|
|
@@ -6204,7 +6294,24 @@ ${buildFrontendContextSection(fctx)}
|
|
|
6204
6294
|
`;
|
|
6205
6295
|
console.log(chalk10.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
6206
6296
|
}
|
|
6207
|
-
|
|
6297
|
+
let fixHistorySection = "";
|
|
6298
|
+
if (options.injectFixHistory !== false) {
|
|
6299
|
+
try {
|
|
6300
|
+
const history = await loadFixHistory(workingDir);
|
|
6301
|
+
const section = buildHallucinationAvoidanceSection(history, {
|
|
6302
|
+
maxItems: options.fixHistoryInjectMax ?? 10
|
|
6303
|
+
});
|
|
6304
|
+
if (section) {
|
|
6305
|
+
fixHistorySection = `
|
|
6306
|
+
${section}
|
|
6307
|
+
`;
|
|
6308
|
+
const patternCount = (section.match(/❌ Do NOT/g) ?? []).length;
|
|
6309
|
+
console.log(chalk10.cyan(` \u2713 Injected ${patternCount} prior hallucination pattern(s) from fix-history`));
|
|
6310
|
+
}
|
|
6311
|
+
} catch {
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection + fixHistorySection;
|
|
6208
6315
|
const estimatedTokenCount = estimateTokens(allContextText);
|
|
6209
6316
|
const budget = getDefaultBudget(this.provider.providerName);
|
|
6210
6317
|
if (estimatedTokenCount > budget * 0.7) {
|
|
@@ -6223,7 +6330,7 @@ ${buildFrontendContextSection(fctx)}
|
|
|
6223
6330
|
}
|
|
6224
6331
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
6225
6332
|
if (tasks && tasks.length > 0) {
|
|
6226
|
-
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6333
|
+
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection + fixHistorySection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6227
6334
|
}
|
|
6228
6335
|
console.log(chalk10.gray(" [1/2] Planning implementation files..."));
|
|
6229
6336
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
@@ -6234,7 +6341,7 @@ IMPORTANT: Check the "Frontend Project Context" section below. Extend existing h
|
|
|
6234
6341
|
|
|
6235
6342
|
=== Feature Spec ===
|
|
6236
6343
|
${spec}
|
|
6237
|
-
${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
|
|
6344
|
+
${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}${fixHistorySection}
|
|
6238
6345
|
=== Project Context ===
|
|
6239
6346
|
${contextSummary}
|
|
6240
6347
|
|
|
@@ -6261,7 +6368,7 @@ Output ONLY a valid JSON array:
|
|
|
6261
6368
|
const icon = item.action === "create" ? chalk10.green("+") : chalk10.yellow("~");
|
|
6262
6369
|
console.log(` ${icon} ${item.file}: ${chalk10.gray(item.description)}`);
|
|
6263
6370
|
});
|
|
6264
|
-
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
6371
|
+
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection + fixHistorySection, systemPrompt);
|
|
6265
6372
|
return files;
|
|
6266
6373
|
}
|
|
6267
6374
|
async runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection, frontendSection = "", sharedConfigSection = "", options = {}, systemPrompt = getCodeGenSystemPrompt(), context) {
|
|
@@ -6321,7 +6428,7 @@ Output ONLY a valid JSON array:
|
|
|
6321
6428
|
}
|
|
6322
6429
|
const filePlan = await Promise.all(
|
|
6323
6430
|
task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
|
|
6324
|
-
const exists = await
|
|
6431
|
+
const exists = await fs11.pathExists(path7.join(workingDir, f));
|
|
6325
6432
|
return {
|
|
6326
6433
|
file: f,
|
|
6327
6434
|
action: exists ? "modify" : "create",
|
|
@@ -6361,7 +6468,7 @@ ${taskContext}`,
|
|
|
6361
6468
|
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
|
|
6362
6469
|
if (isCodeFile || isViewFile) {
|
|
6363
6470
|
try {
|
|
6364
|
-
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await
|
|
6471
|
+
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs11.readFile(path7.join(workingDir, writtenFile), "utf-8");
|
|
6365
6472
|
generatedFileCache.set(writtenFile, content);
|
|
6366
6473
|
} catch {
|
|
6367
6474
|
}
|
|
@@ -6371,19 +6478,28 @@ ${taskContext}`,
|
|
|
6371
6478
|
};
|
|
6372
6479
|
const taskBatches = topoSortLayerTasks(layerTasks);
|
|
6373
6480
|
const layerResults = [];
|
|
6481
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? 3);
|
|
6374
6482
|
for (const batch of taskBatches) {
|
|
6375
6483
|
const batchIsParallel = batch.length > 1;
|
|
6376
|
-
const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
|
|
6377
|
-
const settled = await Promise.allSettled(batchResultPromises);
|
|
6378
6484
|
const batchResults = [];
|
|
6379
|
-
for (let
|
|
6380
|
-
const
|
|
6381
|
-
if (
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6485
|
+
for (let chunkStart = 0; chunkStart < batch.length; chunkStart += maxConcurrency) {
|
|
6486
|
+
const chunk = batch.slice(chunkStart, chunkStart + maxConcurrency);
|
|
6487
|
+
if (batchIsParallel && batch.length > maxConcurrency) {
|
|
6488
|
+
const chunkIdx = Math.floor(chunkStart / maxConcurrency) + 1;
|
|
6489
|
+
const totalChunks = Math.ceil(batch.length / maxConcurrency);
|
|
6490
|
+
console.log(chalk10.gray(` \u21B3 chunk ${chunkIdx}/${totalChunks} (${chunk.length} tasks, concurrency cap: ${maxConcurrency})`));
|
|
6491
|
+
}
|
|
6492
|
+
const chunkResultPromises = chunk.map((task) => executeTask(task, batchIsParallel));
|
|
6493
|
+
const settled = await Promise.allSettled(chunkResultPromises);
|
|
6494
|
+
for (let i = 0; i < settled.length; i++) {
|
|
6495
|
+
const outcome = settled[i];
|
|
6496
|
+
if (outcome.status === "fulfilled") {
|
|
6497
|
+
batchResults.push(outcome.value);
|
|
6498
|
+
} else {
|
|
6499
|
+
const task = chunk[i];
|
|
6500
|
+
console.log(chalk10.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
|
|
6501
|
+
batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
|
|
6502
|
+
}
|
|
6387
6503
|
}
|
|
6388
6504
|
}
|
|
6389
6505
|
layerResults.push(...batchResults);
|
|
@@ -6413,7 +6529,7 @@ ${taskContext}`,
|
|
|
6413
6529
|
const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
|
|
6414
6530
|
for (const sharedFile of context.sharedConfigFiles) {
|
|
6415
6531
|
if (processedSharedConfigs.has(sharedFile.path)) continue;
|
|
6416
|
-
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) =>
|
|
6532
|
+
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path7.basename(f).replace(/\.[jt]sx?$/, ""));
|
|
6417
6533
|
if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
|
|
6418
6534
|
let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
|
|
6419
6535
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
@@ -6453,10 +6569,10 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
6453
6569
|
let successCount = 0;
|
|
6454
6570
|
const writtenFiles = [];
|
|
6455
6571
|
for (const item of filePlan) {
|
|
6456
|
-
const fullPath =
|
|
6572
|
+
const fullPath = path7.join(workingDir, item.file);
|
|
6457
6573
|
let existingContent = "";
|
|
6458
|
-
if (await
|
|
6459
|
-
existingContent = await
|
|
6574
|
+
if (await fs11.pathExists(fullPath)) {
|
|
6575
|
+
existingContent = await fs11.readFile(fullPath, "utf-8");
|
|
6460
6576
|
}
|
|
6461
6577
|
const codePrompt = `Implement this file.
|
|
6462
6578
|
|
|
@@ -6473,8 +6589,8 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6473
6589
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
6474
6590
|
const fileContent = stripCodeFences(raw);
|
|
6475
6591
|
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
6476
|
-
await
|
|
6477
|
-
await
|
|
6592
|
+
await fs11.ensureDir(path7.dirname(fullPath));
|
|
6593
|
+
await fs11.writeFile(fullPath, fileContent, "utf-8");
|
|
6478
6594
|
getActiveLogger()?.fileWritten(item.file);
|
|
6479
6595
|
fileSpinner.succeed(`${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)}`);
|
|
6480
6596
|
successCount++;
|
|
@@ -6495,7 +6611,7 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6495
6611
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
6496
6612
|
async runPlanMode(specFilePath) {
|
|
6497
6613
|
console.log(chalk10.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6498
|
-
const spec = await
|
|
6614
|
+
const spec = await fs11.readFile(specFilePath, "utf-8");
|
|
6499
6615
|
const plan = await this.provider.generate(
|
|
6500
6616
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
6501
6617
|
Be specific about:
|
|
@@ -6514,13 +6630,13 @@ ${spec}`,
|
|
|
6514
6630
|
// core/reviewer.ts
|
|
6515
6631
|
import chalk12 from "chalk";
|
|
6516
6632
|
import { execSync as execSync3 } from "child_process";
|
|
6517
|
-
import * as
|
|
6518
|
-
import * as
|
|
6633
|
+
import * as path9 from "path";
|
|
6634
|
+
import * as fs13 from "fs-extra";
|
|
6519
6635
|
|
|
6520
6636
|
// core/constitution-generator.ts
|
|
6521
6637
|
import chalk11 from "chalk";
|
|
6522
|
-
import * as
|
|
6523
|
-
import * as
|
|
6638
|
+
import * as fs12 from "fs-extra";
|
|
6639
|
+
import * as path8 from "path";
|
|
6524
6640
|
|
|
6525
6641
|
// prompts/constitution.prompt.ts
|
|
6526
6642
|
var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
|
|
@@ -6600,8 +6716,8 @@ var ConstitutionGenerator = class {
|
|
|
6600
6716
|
return this.provider.generate(prompt, constitutionSystemPrompt);
|
|
6601
6717
|
}
|
|
6602
6718
|
async saveConstitution(projectRoot, content) {
|
|
6603
|
-
const filePath =
|
|
6604
|
-
await
|
|
6719
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6720
|
+
await fs12.writeFile(filePath, content, "utf-8");
|
|
6605
6721
|
return filePath;
|
|
6606
6722
|
}
|
|
6607
6723
|
};
|
|
@@ -6627,7 +6743,7 @@ ${context.routeSummary}
|
|
|
6627
6743
|
}
|
|
6628
6744
|
if (context.schema) {
|
|
6629
6745
|
parts.push(`=== Prisma Schema ===
|
|
6630
|
-
${context.schema.slice(0,
|
|
6746
|
+
${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
|
|
6631
6747
|
`);
|
|
6632
6748
|
}
|
|
6633
6749
|
if (context.errorPatterns) {
|
|
@@ -6659,9 +6775,9 @@ ${sections.join("\n")}
|
|
|
6659
6775
|
return parts.join("\n");
|
|
6660
6776
|
}
|
|
6661
6777
|
async function loadConstitution(projectRoot) {
|
|
6662
|
-
const filePath =
|
|
6663
|
-
if (await
|
|
6664
|
-
return
|
|
6778
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6779
|
+
if (await fs12.pathExists(filePath)) {
|
|
6780
|
+
return fs12.readFile(filePath, "utf-8");
|
|
6665
6781
|
}
|
|
6666
6782
|
return void 0;
|
|
6667
6783
|
}
|
|
@@ -6677,10 +6793,10 @@ function printConstitutionHint(exists) {
|
|
|
6677
6793
|
|
|
6678
6794
|
// core/reviewer.ts
|
|
6679
6795
|
async function loadAccumulatedLessons(projectRoot) {
|
|
6680
|
-
const constitutionPath =
|
|
6796
|
+
const constitutionPath = path9.join(projectRoot, CONSTITUTION_FILE);
|
|
6681
6797
|
let content;
|
|
6682
6798
|
try {
|
|
6683
|
-
content = await
|
|
6799
|
+
content = await fs13.readFile(constitutionPath, "utf-8");
|
|
6684
6800
|
} catch {
|
|
6685
6801
|
return null;
|
|
6686
6802
|
}
|
|
@@ -6691,23 +6807,23 @@ async function loadAccumulatedLessons(projectRoot) {
|
|
|
6691
6807
|
const nextSection = section.slice(marker.length).match(/\n## \d/);
|
|
6692
6808
|
return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
|
|
6693
6809
|
}
|
|
6694
|
-
var REVIEW_HISTORY_FILE =
|
|
6810
|
+
var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
|
|
6695
6811
|
async function loadReviewHistory(projectRoot) {
|
|
6696
|
-
const historyPath =
|
|
6812
|
+
const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6697
6813
|
try {
|
|
6698
|
-
if (await
|
|
6699
|
-
return await
|
|
6814
|
+
if (await fs13.pathExists(historyPath)) {
|
|
6815
|
+
return await fs13.readJson(historyPath);
|
|
6700
6816
|
}
|
|
6701
6817
|
} catch {
|
|
6702
6818
|
}
|
|
6703
6819
|
return [];
|
|
6704
6820
|
}
|
|
6705
6821
|
async function appendReviewHistory(projectRoot, entry) {
|
|
6706
|
-
const historyPath =
|
|
6822
|
+
const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6707
6823
|
const existing = await loadReviewHistory(projectRoot);
|
|
6708
6824
|
const updated = [...existing, entry].slice(-20);
|
|
6709
6825
|
try {
|
|
6710
|
-
await
|
|
6826
|
+
await fs13.writeJson(historyPath, updated, { spaces: 2 });
|
|
6711
6827
|
} catch {
|
|
6712
6828
|
}
|
|
6713
6829
|
}
|
|
@@ -6741,7 +6857,7 @@ function buildHistoryContext(history) {
|
|
|
6741
6857
|
const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
|
|
6742
6858
|
for (const entry of recent) {
|
|
6743
6859
|
lines.push(`
|
|
6744
|
-
[${entry.date}] ${
|
|
6860
|
+
[${entry.date}] ${path9.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
|
|
6745
6861
|
entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
|
|
6746
6862
|
}
|
|
6747
6863
|
return lines.join("\n") + "\n";
|
|
@@ -6822,15 +6938,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
6822
6938
|
${codeContext}`;
|
|
6823
6939
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
6824
6940
|
console.log(chalk12.gray(" Pass 2/3: Implementation review..."));
|
|
6941
|
+
const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
|
|
6825
6942
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6826
6943
|
const historyContext = buildHistoryContext(history);
|
|
6827
6944
|
const implPrompt = `Review the implementation details of this change.
|
|
6828
6945
|
|
|
6829
|
-
=== Feature Spec ===
|
|
6830
|
-
${
|
|
6831
|
-
|
|
6832
|
-
=== Code ===
|
|
6833
|
-
${codeContext}
|
|
6946
|
+
=== Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
|
|
6947
|
+
${specDigest}
|
|
6834
6948
|
|
|
6835
6949
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
|
|
6836
6950
|
${archReview}
|
|
@@ -6839,11 +6953,8 @@ ${historyContext}`;
|
|
|
6839
6953
|
console.log(chalk12.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6840
6954
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6841
6955
|
|
|
6842
|
-
=== Feature Spec ===
|
|
6843
|
-
${
|
|
6844
|
-
|
|
6845
|
-
=== Code ===
|
|
6846
|
-
${codeContext}
|
|
6956
|
+
=== Feature Spec (digest) ===
|
|
6957
|
+
${specDigest}
|
|
6847
6958
|
|
|
6848
6959
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
6849
6960
|
${archReview}
|
|
@@ -6866,7 +6977,7 @@ ${sep}
|
|
|
6866
6977
|
if (score > 0 && specFile) {
|
|
6867
6978
|
await appendReviewHistory(this.projectRoot, {
|
|
6868
6979
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
6869
|
-
specFile:
|
|
6980
|
+
specFile: path9.relative(this.projectRoot, specFile),
|
|
6870
6981
|
score,
|
|
6871
6982
|
...complianceScore > 0 ? { complianceScore } : {},
|
|
6872
6983
|
topIssues,
|
|
@@ -6911,14 +7022,14 @@ ${sep}
|
|
|
6911
7022
|
);
|
|
6912
7023
|
let filesSection = "";
|
|
6913
7024
|
for (const filePath of filePaths) {
|
|
6914
|
-
const fullPath =
|
|
7025
|
+
const fullPath = path9.join(workingDir, filePath);
|
|
6915
7026
|
try {
|
|
6916
|
-
const content = await
|
|
7027
|
+
const content = await fs13.readFile(fullPath, "utf-8");
|
|
6917
7028
|
filesSection += `
|
|
6918
7029
|
|
|
6919
7030
|
=== ${filePath} ===
|
|
6920
|
-
${content.slice(0,
|
|
6921
|
-
if (content.length >
|
|
7031
|
+
${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
|
|
7032
|
+
if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
|
|
6922
7033
|
... (truncated, ${content.length} chars total)`;
|
|
6923
7034
|
} catch {
|
|
6924
7035
|
filesSection += `
|
|
@@ -6947,7 +7058,7 @@ ${content.slice(0, 3e3)}`;
|
|
|
6947
7058
|
const color = entry.score >= 8 ? chalk12.green : entry.score >= 6 ? chalk12.yellow : chalk12.red;
|
|
6948
7059
|
const impactTag = entry.impactLevel ? chalk12.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk12.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk12.yellow(entry.impactLevel) : chalk12.green(entry.impactLevel)}`) : "";
|
|
6949
7060
|
const complexityTag = entry.complexityLevel ? chalk12.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk12.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk12.yellow(entry.complexityLevel) : chalk12.green(entry.complexityLevel)}`) : "";
|
|
6950
|
-
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${
|
|
7061
|
+
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path9.basename(entry.specFile)}`);
|
|
6951
7062
|
}
|
|
6952
7063
|
console.log(chalk12.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\u2500\u2500\u2500\u2500\u2500"));
|
|
6953
7064
|
}
|
|
@@ -7000,8 +7111,8 @@ function parseSpecAndTasks(raw) {
|
|
|
7000
7111
|
|
|
7001
7112
|
// git/worktree.ts
|
|
7002
7113
|
import { execSync as execSync4 } from "child_process";
|
|
7003
|
-
import * as
|
|
7004
|
-
import * as
|
|
7114
|
+
import * as path10 from "path";
|
|
7115
|
+
import * as fs14 from "fs-extra";
|
|
7005
7116
|
import chalk13 from "chalk";
|
|
7006
7117
|
var GitWorktreeManager = class {
|
|
7007
7118
|
constructor(baseDir) {
|
|
@@ -7027,12 +7138,12 @@ var GitWorktreeManager = class {
|
|
|
7027
7138
|
async linkDependencies(worktreePath) {
|
|
7028
7139
|
const candidates = ["node_modules", "vendor"];
|
|
7029
7140
|
for (const dir of candidates) {
|
|
7030
|
-
const src =
|
|
7031
|
-
const dest =
|
|
7032
|
-
if (!await
|
|
7033
|
-
if (await
|
|
7141
|
+
const src = path10.join(this.baseDir, dir);
|
|
7142
|
+
const dest = path10.join(worktreePath, dir);
|
|
7143
|
+
if (!await fs14.pathExists(src)) continue;
|
|
7144
|
+
if (await fs14.pathExists(dest)) continue;
|
|
7034
7145
|
try {
|
|
7035
|
-
await
|
|
7146
|
+
await fs14.ensureSymlink(src, dest, "dir");
|
|
7036
7147
|
console.log(chalk13.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
7037
7148
|
} catch (err) {
|
|
7038
7149
|
console.log(chalk13.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
@@ -7047,11 +7158,11 @@ var GitWorktreeManager = class {
|
|
|
7047
7158
|
}
|
|
7048
7159
|
const featureName = this.sanitizeFeatureName(idea);
|
|
7049
7160
|
const branchName = `feature/${featureName}`;
|
|
7050
|
-
const repoName =
|
|
7051
|
-
const worktreePath =
|
|
7161
|
+
const repoName = path10.basename(this.baseDir);
|
|
7162
|
+
const worktreePath = path10.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
7052
7163
|
console.log(chalk13.cyan(`
|
|
7053
7164
|
--- Setting up Git Worktree ---`));
|
|
7054
|
-
if (await
|
|
7165
|
+
if (await fs14.pathExists(worktreePath)) {
|
|
7055
7166
|
console.log(chalk13.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
7056
7167
|
await this.linkDependencies(worktreePath);
|
|
7057
7168
|
return worktreePath;
|