ai-spec-dev 0.25.0 → 0.28.1
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 +10 -0
- package/README.md +11 -6
- package/RELEASE_LOG.md +102 -0
- package/cli/index.ts +50 -2
- package/core/code-generator.ts +13 -1
- package/core/error-feedback.ts +18 -7
- package/core/frontend-context-loader.ts +72 -24
- package/core/provider-utils.ts +90 -0
- package/core/reviewer.ts +46 -9
- package/core/run-logger.ts +136 -0
- package/core/run-snapshot.ts +84 -0
- package/core/spec-generator.ts +83 -67
- package/core/task-generator.ts +11 -3
- package/dist/cli/index.js +1347 -977
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1346 -976
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +432 -231
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +432 -231
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +54 -0
- package/purpose.md +159 -32
package/dist/index.js
CHANGED
|
@@ -172,6 +172,67 @@ model ExampleModel {
|
|
|
172
172
|
|
|
173
173
|
\u6839\u636E\u7528\u6237\u7684\u60F3\u6CD5\u548C\u9879\u76EE\u4E0A\u4E0B\u6587\u751F\u6210\u4E0A\u8FF0\u5B8C\u6574 Spec\u3002\u786E\u4FDD API \u8BBE\u8BA1\u4E0E\u73B0\u6709\u9879\u76EE\u7684\u8DEF\u7531\u98CE\u683C\u3001\u9519\u8BEF\u7801\u89C4\u8303\u4FDD\u6301\u4E00\u81F4\uFF0C\u6570\u636E\u6A21\u578B\u4E0E\u73B0\u6709 Prisma Schema \u534F\u8C03\u3002`;
|
|
174
174
|
|
|
175
|
+
// core/provider-utils.ts
|
|
176
|
+
var import_chalk = __toESM(require("chalk"));
|
|
177
|
+
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
178
|
+
var ProviderError = class extends Error {
|
|
179
|
+
constructor(message, kind, originalError) {
|
|
180
|
+
super(message);
|
|
181
|
+
this.kind = kind;
|
|
182
|
+
this.originalError = originalError;
|
|
183
|
+
this.name = "ProviderError";
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function classifyError(err, label) {
|
|
187
|
+
const e = err;
|
|
188
|
+
const status = e.status ?? e.response?.status;
|
|
189
|
+
if (status === 401 || status === 403)
|
|
190
|
+
return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
|
|
191
|
+
if (status === 429)
|
|
192
|
+
return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
|
|
193
|
+
if (e._timeout || e.message?.toLowerCase().includes("timed out"))
|
|
194
|
+
return new ProviderError(`Request timed out (${label})`, "timeout", err);
|
|
195
|
+
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
|
|
196
|
+
return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
|
|
197
|
+
return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
|
|
198
|
+
}
|
|
199
|
+
function isRetryable(err) {
|
|
200
|
+
const e = err;
|
|
201
|
+
const status = e.status ?? e.response?.status;
|
|
202
|
+
if (status === 401 || status === 403) return false;
|
|
203
|
+
if (status === 429 || status !== void 0 && status >= 500) return true;
|
|
204
|
+
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED") return true;
|
|
205
|
+
if (e.message?.toLowerCase().includes("timed out")) return true;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
async function withReliability(fn, opts) {
|
|
209
|
+
const { retries = 2, timeoutMs = 9e4, label = "AI call", onRetry } = opts ?? {};
|
|
210
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
211
|
+
try {
|
|
212
|
+
return await Promise.race([
|
|
213
|
+
fn(),
|
|
214
|
+
new Promise(
|
|
215
|
+
(_2, reject) => setTimeout(
|
|
216
|
+
() => reject(Object.assign(new Error(`timed out after ${timeoutMs / 1e3}s`), { _timeout: true })),
|
|
217
|
+
timeoutMs
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
]);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (!isRetryable(err) || attempt === retries) {
|
|
223
|
+
throw classifyError(err, label);
|
|
224
|
+
}
|
|
225
|
+
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
226
|
+
console.warn(
|
|
227
|
+
import_chalk.default.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + import_chalk.default.gray(` \u2014 ${err.message}`)
|
|
228
|
+
);
|
|
229
|
+
onRetry?.(attempt + 1, err);
|
|
230
|
+
await sleep(waitMs);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw new Error("unreachable");
|
|
234
|
+
}
|
|
235
|
+
|
|
175
236
|
// core/spec-generator.ts
|
|
176
237
|
function geminiRequestOptions() {
|
|
177
238
|
const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
@@ -317,12 +378,17 @@ var GeminiProvider = class {
|
|
|
317
378
|
this.modelName = modelName;
|
|
318
379
|
}
|
|
319
380
|
async generate(prompt, systemInstruction) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
381
|
+
return withReliability(
|
|
382
|
+
async () => {
|
|
383
|
+
const model = this.genAI.getGenerativeModel(
|
|
384
|
+
{ model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
|
|
385
|
+
geminiRequestOptions()
|
|
386
|
+
);
|
|
387
|
+
const result = await model.generateContent(prompt);
|
|
388
|
+
return result.response.text();
|
|
389
|
+
},
|
|
390
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
323
391
|
);
|
|
324
|
-
const result = await model.generateContent(prompt);
|
|
325
|
-
return result.response.text();
|
|
326
392
|
}
|
|
327
393
|
};
|
|
328
394
|
var ClaudeProvider = class {
|
|
@@ -334,15 +400,20 @@ var ClaudeProvider = class {
|
|
|
334
400
|
this.modelName = modelName;
|
|
335
401
|
}
|
|
336
402
|
async generate(prompt, systemInstruction) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
403
|
+
return withReliability(
|
|
404
|
+
async () => {
|
|
405
|
+
const message = await this.client.messages.create({
|
|
406
|
+
model: this.modelName,
|
|
407
|
+
max_tokens: 8192,
|
|
408
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
409
|
+
messages: [{ role: "user", content: prompt }]
|
|
410
|
+
});
|
|
411
|
+
const block = message.content[0];
|
|
412
|
+
if (block.type === "text") return block.text;
|
|
413
|
+
throw new Error("Unexpected response type from Claude API");
|
|
414
|
+
},
|
|
415
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
416
|
+
);
|
|
346
417
|
}
|
|
347
418
|
};
|
|
348
419
|
var OpenAICompatibleProvider = class {
|
|
@@ -362,19 +433,24 @@ var OpenAICompatibleProvider = class {
|
|
|
362
433
|
});
|
|
363
434
|
}
|
|
364
435
|
async generate(prompt, systemInstruction) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
436
|
+
return withReliability(
|
|
437
|
+
async () => {
|
|
438
|
+
const messages = [];
|
|
439
|
+
if (systemInstruction) {
|
|
440
|
+
const isOSeries = /^o[13]/.test(this.modelName);
|
|
441
|
+
const role = isOSeries ? "developer" : this.systemRole;
|
|
442
|
+
messages.push({ role, content: systemInstruction });
|
|
443
|
+
}
|
|
444
|
+
messages.push({ role: "user", content: prompt });
|
|
445
|
+
const completion = await this.client.chat.completions.create({
|
|
446
|
+
model: this.modelName,
|
|
447
|
+
messages,
|
|
448
|
+
...this.extraBody ? { extra_body: this.extraBody } : {}
|
|
449
|
+
});
|
|
450
|
+
return completion.choices[0].message.content ?? "";
|
|
451
|
+
},
|
|
452
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
453
|
+
);
|
|
378
454
|
}
|
|
379
455
|
};
|
|
380
456
|
var MiMoProvider = class {
|
|
@@ -387,32 +463,37 @@ var MiMoProvider = class {
|
|
|
387
463
|
this.modelName = modelName;
|
|
388
464
|
}
|
|
389
465
|
async generate(prompt, systemInstruction) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
466
|
+
return withReliability(
|
|
467
|
+
async () => {
|
|
468
|
+
const body = {
|
|
469
|
+
model: this.modelName,
|
|
470
|
+
max_tokens: 16384,
|
|
471
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
472
|
+
top_p: 0.95,
|
|
473
|
+
stream: false,
|
|
474
|
+
temperature: 1,
|
|
475
|
+
stop_sequences: null
|
|
476
|
+
};
|
|
477
|
+
if (systemInstruction) {
|
|
478
|
+
body.system = systemInstruction;
|
|
479
|
+
}
|
|
480
|
+
const response = await import_axios.default.post(this.baseUrl, body, {
|
|
481
|
+
headers: {
|
|
482
|
+
"api-key": this.apiKey,
|
|
483
|
+
"Content-Type": "application/json"
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
const data = response.data;
|
|
487
|
+
const blocks = data?.content ?? [];
|
|
488
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
489
|
+
if (textBlock?.text) return textBlock.text;
|
|
490
|
+
if (data?.stop_reason === "max_tokens") {
|
|
491
|
+
throw new Error(`MiMo response truncated (max_tokens reached). The prompt may be too long. Try a shorter spec or switch to a model with larger context.`);
|
|
492
|
+
}
|
|
493
|
+
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
494
|
+
},
|
|
495
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
496
|
+
);
|
|
416
497
|
}
|
|
417
498
|
};
|
|
418
499
|
function createProvider(providerName, apiKey, modelName) {
|
|
@@ -3868,10 +3949,10 @@ ${preview}
|
|
|
3868
3949
|
|
|
3869
3950
|
// core/spec-refiner.ts
|
|
3870
3951
|
var import_prompts = require("@inquirer/prompts");
|
|
3871
|
-
var
|
|
3952
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
3872
3953
|
|
|
3873
3954
|
// core/spec-versioning.ts
|
|
3874
|
-
var
|
|
3955
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
3875
3956
|
var fs4 = __toESM(require("fs-extra"));
|
|
3876
3957
|
function computeDiff(oldText, newText) {
|
|
3877
3958
|
const oldLines = oldText.split("\n");
|
|
@@ -3922,7 +4003,7 @@ function computeSimpleDiff(oldLines, newLines) {
|
|
|
3922
4003
|
var CONTEXT_LINES = 3;
|
|
3923
4004
|
function printDiff(diff) {
|
|
3924
4005
|
if (diff.added === 0 && diff.removed === 0) {
|
|
3925
|
-
console.log(
|
|
4006
|
+
console.log(import_chalk2.default.gray(" (no changes)"));
|
|
3926
4007
|
return;
|
|
3927
4008
|
}
|
|
3928
4009
|
const { lines } = diff;
|
|
@@ -3939,25 +4020,25 @@ function printDiff(diff) {
|
|
|
3939
4020
|
let prevIdx = -2;
|
|
3940
4021
|
for (const idx of sorted) {
|
|
3941
4022
|
if (idx > prevIdx + 1 && prevIdx !== -2) {
|
|
3942
|
-
console.log(
|
|
4023
|
+
console.log(import_chalk2.default.cyan(" @@"));
|
|
3943
4024
|
}
|
|
3944
4025
|
const l = lines[idx];
|
|
3945
4026
|
if (l.type === "added") {
|
|
3946
|
-
console.log(
|
|
4027
|
+
console.log(import_chalk2.default.green(` + ${l.content}`));
|
|
3947
4028
|
} else if (l.type === "removed") {
|
|
3948
|
-
console.log(
|
|
4029
|
+
console.log(import_chalk2.default.red(` - ${l.content}`));
|
|
3949
4030
|
} else {
|
|
3950
|
-
console.log(
|
|
4031
|
+
console.log(import_chalk2.default.gray(` ${l.content}`));
|
|
3951
4032
|
}
|
|
3952
4033
|
prevIdx = idx;
|
|
3953
4034
|
}
|
|
3954
4035
|
}
|
|
3955
4036
|
function printDiffSummary(diff, label) {
|
|
3956
4037
|
const parts = [];
|
|
3957
|
-
if (diff.added > 0) parts.push(
|
|
3958
|
-
if (diff.removed > 0) parts.push(
|
|
3959
|
-
if (parts.length === 0) parts.push(
|
|
3960
|
-
console.log(
|
|
4038
|
+
if (diff.added > 0) parts.push(import_chalk2.default.green(`+${diff.added}`));
|
|
4039
|
+
if (diff.removed > 0) parts.push(import_chalk2.default.red(`-${diff.removed}`));
|
|
4040
|
+
if (parts.length === 0) parts.push(import_chalk2.default.gray("no change"));
|
|
4041
|
+
console.log(import_chalk2.default.bold(` ${label}: `) + parts.join(" ") + import_chalk2.default.gray(` lines`));
|
|
3961
4042
|
}
|
|
3962
4043
|
|
|
3963
4044
|
// core/spec-refiner.ts
|
|
@@ -3969,16 +4050,16 @@ var SpecRefiner = class {
|
|
|
3969
4050
|
let currentSpec = initialSpec;
|
|
3970
4051
|
let round = 1;
|
|
3971
4052
|
while (true) {
|
|
3972
|
-
console.log(
|
|
4053
|
+
console.log(import_chalk3.default.cyan(`
|
|
3973
4054
|
\u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
3974
|
-
console.log(
|
|
4055
|
+
console.log(import_chalk3.default.gray(" Opening spec in editor. Save and close to continue."));
|
|
3975
4056
|
currentSpec = await (0, import_prompts.editor)({
|
|
3976
4057
|
message: "Review and edit the spec:",
|
|
3977
4058
|
default: currentSpec,
|
|
3978
4059
|
postfix: ".md",
|
|
3979
4060
|
waitForUserInput: false
|
|
3980
4061
|
});
|
|
3981
|
-
console.log(
|
|
4062
|
+
console.log(import_chalk3.default.green(" \u2714 Spec saved."));
|
|
3982
4063
|
const action = await (0, import_prompts.select)({
|
|
3983
4064
|
message: "What would you like to do?",
|
|
3984
4065
|
choices: [
|
|
@@ -3991,7 +4072,7 @@ var SpecRefiner = class {
|
|
|
3991
4072
|
break;
|
|
3992
4073
|
}
|
|
3993
4074
|
if (action === "ai") {
|
|
3994
|
-
console.log(
|
|
4075
|
+
console.log(import_chalk3.default.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
|
|
3995
4076
|
try {
|
|
3996
4077
|
const improved = await this.provider.generate(
|
|
3997
4078
|
`Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
|
|
@@ -4001,30 +4082,30 @@ Output ONLY the improved markdown spec, nothing else.
|
|
|
4001
4082
|
${currentSpec}`,
|
|
4002
4083
|
"You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
|
|
4003
4084
|
);
|
|
4004
|
-
console.log(
|
|
4085
|
+
console.log(import_chalk3.default.yellow("\n AI has suggested improvements. Opening diff in editor..."));
|
|
4005
4086
|
const acceptImproved = await (0, import_prompts.confirm)({
|
|
4006
4087
|
message: "Accept AI improvements? (opens editor so you can review first)",
|
|
4007
4088
|
default: true
|
|
4008
4089
|
});
|
|
4009
4090
|
if (acceptImproved) {
|
|
4010
4091
|
const diff = computeDiff(currentSpec, improved);
|
|
4011
|
-
console.log(
|
|
4092
|
+
console.log(import_chalk3.default.cyan("\n \u2500\u2500 AI Changes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4012
4093
|
printDiffSummary(diff, "AI edits");
|
|
4013
4094
|
printDiff(diff);
|
|
4014
|
-
console.log(
|
|
4095
|
+
console.log(import_chalk3.default.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
4015
4096
|
currentSpec = await (0, import_prompts.editor)({
|
|
4016
4097
|
message: "Review AI-improved spec (edit if needed, then save):",
|
|
4017
4098
|
default: improved,
|
|
4018
4099
|
postfix: ".md",
|
|
4019
4100
|
waitForUserInput: false
|
|
4020
4101
|
});
|
|
4021
|
-
console.log(
|
|
4102
|
+
console.log(import_chalk3.default.green(" \u2714 AI-improved spec accepted."));
|
|
4022
4103
|
} else {
|
|
4023
|
-
console.log(
|
|
4104
|
+
console.log(import_chalk3.default.gray(" AI improvements discarded. Keeping your version."));
|
|
4024
4105
|
}
|
|
4025
4106
|
} catch (err) {
|
|
4026
|
-
console.error(
|
|
4027
|
-
console.log(
|
|
4107
|
+
console.error(import_chalk3.default.red(" AI improvement failed:"), err);
|
|
4108
|
+
console.log(import_chalk3.default.gray(" Continuing with current spec."));
|
|
4028
4109
|
}
|
|
4029
4110
|
}
|
|
4030
4111
|
round++;
|
|
@@ -4034,10 +4115,10 @@ ${currentSpec}`,
|
|
|
4034
4115
|
};
|
|
4035
4116
|
|
|
4036
4117
|
// core/code-generator.ts
|
|
4037
|
-
var
|
|
4118
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
4038
4119
|
var import_child_process = require("child_process");
|
|
4039
4120
|
var path6 = __toESM(require("path"));
|
|
4040
|
-
var
|
|
4121
|
+
var fs10 = __toESM(require("fs-extra"));
|
|
4041
4122
|
|
|
4042
4123
|
// prompts/codegen.prompt.ts
|
|
4043
4124
|
var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
|
|
@@ -4262,9 +4343,51 @@ Actionable, concrete improvements.
|
|
|
4262
4343
|
Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
|
|
4263
4344
|
|
|
4264
4345
|
Be specific. Reference actual code, not vague principles.`;
|
|
4346
|
+
var reviewImpactComplexitySystemPrompt = `You are a Senior Staff Engineer assessing the RISK and MAINTAINABILITY of a code change.
|
|
4347
|
+
|
|
4348
|
+
Two previous review passes have already covered architecture compliance and implementation correctness. Do NOT repeat their findings.
|
|
4349
|
+
|
|
4350
|
+
Your job is to answer exactly two questions:
|
|
4351
|
+
|
|
4352
|
+
---
|
|
4353
|
+
|
|
4354
|
+
## \u{1F30A} \u5F71\u54CD\u9762\u8BC4\u4F30 (Impact Assessment)
|
|
4355
|
+
|
|
4356
|
+
Evaluate what this change touches beyond its own files:
|
|
4357
|
+
|
|
4358
|
+
1. **\u76F4\u63A5\u5F71\u54CD\u6587\u4EF6** \u2014 List the files created or modified.
|
|
4359
|
+
2. **\u95F4\u63A5\u5F71\u54CD\u8303\u56F4** \u2014 Which existing modules, API consumers, or downstream services will be affected? (e.g., shared utilities modified, public interfaces changed, database schema altered)
|
|
4360
|
+
3. **\u7834\u574F\u6027\u53D8\u66F4 (Breaking Changes)** \u2014 Are there changes requiring coordinated updates elsewhere?
|
|
4361
|
+
- API endpoint signature changes (path, method, required params)
|
|
4362
|
+
- Database schema changes (renamed columns, dropped fields, new NOT NULL constraints)
|
|
4363
|
+
- Config / env variable changes
|
|
4364
|
+
- Exported function/type renames
|
|
4365
|
+
- If none: state "\u65E0\u7834\u574F\u6027\u53D8\u66F4"
|
|
4366
|
+
4. **\u5F71\u54CD\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
|
|
4367
|
+
- \u4F4E: isolated to new files, no shared interface changes
|
|
4368
|
+
- \u4E2D: modifies shared utilities or existing public interfaces, backwards compatible
|
|
4369
|
+
- \u9AD8: breaking changes, or touches core auth / payment / data integrity paths
|
|
4370
|
+
|
|
4371
|
+
---
|
|
4372
|
+
|
|
4373
|
+
## \u{1F9EE} \u4EE3\u7801\u590D\u6742\u5EA6\u8BC4\u4F30 (Complexity Assessment)
|
|
4374
|
+
|
|
4375
|
+
Evaluate how hard the new code will be to understand and change in the future:
|
|
4376
|
+
|
|
4377
|
+
1. **\u8BA4\u77E5\u590D\u6742\u5EA6\u70ED\u70B9** \u2014 Identify the 1-3 most complex functions/methods. For each, explain WHY (deep nesting, multiple early returns, implicit state mutation, mixed abstraction levels).
|
|
4378
|
+
2. **\u8026\u5408\u5EA6\u5206\u6790** \u2014 How tightly is the new code coupled to existing modules? Are dependencies injected or hardcoded? Any circular dependency risks?
|
|
4379
|
+
3. **\u53EF\u7EF4\u62A4\u6027\u98CE\u9669** \u2014 Flag patterns that will cause pain when requirements change:
|
|
4380
|
+
- Magic numbers / hardcoded strings that should be constants
|
|
4381
|
+
- Business logic buried in framework lifecycle hooks or constructors
|
|
4382
|
+
- Implicit temporal coupling (functions that must be called in a specific order)
|
|
4383
|
+
4. **\u590D\u6742\u5EA6\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
|
|
4384
|
+
|
|
4385
|
+
---
|
|
4386
|
+
|
|
4387
|
+
Format using ONLY these two sections. Be specific. Reference file names and line ranges where relevant.`;
|
|
4265
4388
|
|
|
4266
4389
|
// core/task-generator.ts
|
|
4267
|
-
var
|
|
4390
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
4268
4391
|
var fs5 = __toESM(require("fs-extra"));
|
|
4269
4392
|
var path3 = __toESM(require("path"));
|
|
4270
4393
|
|
|
@@ -4418,31 +4541,36 @@ function parseTasks(raw) {
|
|
|
4418
4541
|
}
|
|
4419
4542
|
function printTasks(tasks) {
|
|
4420
4543
|
const layerColors = {
|
|
4421
|
-
data:
|
|
4422
|
-
infra:
|
|
4423
|
-
service:
|
|
4424
|
-
api:
|
|
4425
|
-
view:
|
|
4426
|
-
route:
|
|
4427
|
-
test:
|
|
4544
|
+
data: import_chalk4.default.magenta,
|
|
4545
|
+
infra: import_chalk4.default.gray,
|
|
4546
|
+
service: import_chalk4.default.blue,
|
|
4547
|
+
api: import_chalk4.default.cyan,
|
|
4548
|
+
view: import_chalk4.default.yellow,
|
|
4549
|
+
route: import_chalk4.default.white,
|
|
4550
|
+
test: import_chalk4.default.green
|
|
4428
4551
|
};
|
|
4429
|
-
console.log(
|
|
4552
|
+
console.log(import_chalk4.default.bold(`
|
|
4430
4553
|
Tasks (${tasks.length}):`));
|
|
4431
4554
|
for (const task of tasks) {
|
|
4432
|
-
const color = layerColors[task.layer] ??
|
|
4555
|
+
const color = layerColors[task.layer] ?? import_chalk4.default.white;
|
|
4433
4556
|
const badge = color(`[${task.layer}]`);
|
|
4434
|
-
const prio = task.priority === "high" ?
|
|
4435
|
-
console.log(` ${prio} ${
|
|
4557
|
+
const prio = task.priority === "high" ? import_chalk4.default.red("\u25CF") : task.priority === "medium" ? import_chalk4.default.yellow("\u25CF") : import_chalk4.default.gray("\u25CF");
|
|
4558
|
+
console.log(` ${prio} ${import_chalk4.default.bold(task.id)} ${badge} ${task.title}`);
|
|
4436
4559
|
}
|
|
4437
4560
|
}
|
|
4438
4561
|
async function loadTasksForSpec(specFilePath) {
|
|
4439
4562
|
const base = path3.basename(specFilePath, ".md");
|
|
4440
4563
|
const dir = path3.dirname(specFilePath);
|
|
4441
4564
|
const tasksFile = path3.join(dir, `${base}-tasks.json`);
|
|
4442
|
-
if (await fs5.pathExists(tasksFile))
|
|
4443
|
-
|
|
4565
|
+
if (!await fs5.pathExists(tasksFile)) return null;
|
|
4566
|
+
try {
|
|
4567
|
+
return await fs5.readJson(tasksFile);
|
|
4568
|
+
} catch {
|
|
4569
|
+
console.warn(
|
|
4570
|
+
import_chalk4.default.yellow(` \u26A0 Tasks file is corrupt or unreadable (${path3.basename(tasksFile)}).`) + import_chalk4.default.gray(` Re-run \`ai-spec tasks <spec>\` to regenerate.`)
|
|
4571
|
+
);
|
|
4572
|
+
return null;
|
|
4444
4573
|
}
|
|
4445
|
-
return null;
|
|
4446
4574
|
}
|
|
4447
4575
|
async function updateTaskStatus(specFilePath, taskId, status) {
|
|
4448
4576
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
@@ -4456,13 +4584,13 @@ async function updateTaskStatus(specFilePath, taskId, status) {
|
|
|
4456
4584
|
}
|
|
4457
4585
|
|
|
4458
4586
|
// core/dsl-extractor.ts
|
|
4459
|
-
var
|
|
4587
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
4460
4588
|
var fs6 = __toESM(require("fs-extra"));
|
|
4461
4589
|
var path4 = __toESM(require("path"));
|
|
4462
4590
|
var import_prompts2 = require("@inquirer/prompts");
|
|
4463
4591
|
|
|
4464
4592
|
// core/dsl-validator.ts
|
|
4465
|
-
var
|
|
4593
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
4466
4594
|
var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
4467
4595
|
var MAX_MODELS = 50;
|
|
4468
4596
|
var MAX_FIELDS_PER_MODEL = 100;
|
|
@@ -5033,7 +5161,7 @@ ${preview}`);
|
|
|
5033
5161
|
} catch {
|
|
5034
5162
|
}
|
|
5035
5163
|
}
|
|
5036
|
-
const httpImportRegex = /^import\s+(
|
|
5164
|
+
const httpImportRegex = /^import(?!\s+type)\s+(?:[\w*]+|\{[^}]+\})\s+from\s+['"]((?:@{1,2}|~|#)[/\\][^'"]+|\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*|axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)['"]/im;
|
|
5037
5165
|
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
5038
5166
|
try {
|
|
5039
5167
|
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
@@ -5046,31 +5174,51 @@ ${preview}`);
|
|
|
5046
5174
|
}
|
|
5047
5175
|
}
|
|
5048
5176
|
const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
|
|
5049
|
-
const paginationInterfaceRegex = new RegExp(
|
|
5050
|
-
`(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
|
|
5051
|
-
"s"
|
|
5052
|
-
);
|
|
5053
|
-
const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
|
|
5054
5177
|
for (const relPath of ctx.existingApiFiles) {
|
|
5055
5178
|
if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
|
|
5056
5179
|
try {
|
|
5057
5180
|
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
5058
5181
|
if (!paginationFieldNames.some((f) => content.includes(f))) continue;
|
|
5059
|
-
const
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5182
|
+
const lines = content.split("\n");
|
|
5183
|
+
let interfaceName = "";
|
|
5184
|
+
let interfaceBlock = "";
|
|
5185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5186
|
+
const m = lines[i].match(/(?:interface|type)\s+(\w*(?:Params|Query|Request|Filter|Page)\w*)\s*[={<]/);
|
|
5187
|
+
if (!m) continue;
|
|
5188
|
+
const blockLines = [];
|
|
5189
|
+
let depth = 0;
|
|
5190
|
+
for (let j2 = i; j2 < Math.min(i + 40, lines.length); j2++) {
|
|
5191
|
+
blockLines.push(lines[j2]);
|
|
5192
|
+
depth += (lines[j2].match(/\{/g) ?? []).length;
|
|
5193
|
+
depth -= (lines[j2].match(/\}/g) ?? []).length;
|
|
5194
|
+
if (depth === 0 && j2 > i) break;
|
|
5195
|
+
}
|
|
5196
|
+
const blockText = blockLines.join("\n");
|
|
5197
|
+
if (!paginationFieldNames.some((f) => new RegExp(`\\b${f}\\b`).test(blockText))) continue;
|
|
5198
|
+
interfaceName = m[1];
|
|
5199
|
+
interfaceBlock = blockText;
|
|
5200
|
+
break;
|
|
5201
|
+
}
|
|
5202
|
+
if (!interfaceName) continue;
|
|
5203
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5204
|
+
const line = lines[i];
|
|
5205
|
+
if (!/export\s+(async\s+)?(function|const)/.test(line)) continue;
|
|
5206
|
+
if (!line.includes(interfaceName)) continue;
|
|
5207
|
+
const fnLines = [];
|
|
5208
|
+
let depth = 0;
|
|
5209
|
+
for (let j2 = i; j2 < Math.min(i + 30, lines.length); j2++) {
|
|
5210
|
+
fnLines.push(lines[j2]);
|
|
5211
|
+
depth += (lines[j2].match(/\{/g) ?? []).length;
|
|
5212
|
+
depth -= (lines[j2].match(/\}/g) ?? []).length;
|
|
5213
|
+
if (depth === 0 && j2 > i) break;
|
|
5214
|
+
}
|
|
5068
5215
|
ctx.paginationExample = `// From ${relPath}
|
|
5069
|
-
${
|
|
5216
|
+
${interfaceBlock}
|
|
5070
5217
|
|
|
5071
|
-
${
|
|
5218
|
+
${fnLines.join("\n")}`;
|
|
5072
5219
|
break;
|
|
5073
5220
|
}
|
|
5221
|
+
if (ctx.paginationExample) break;
|
|
5074
5222
|
} catch {
|
|
5075
5223
|
}
|
|
5076
5224
|
}
|
|
@@ -5283,6 +5431,21 @@ Shared component structure patterns:`);
|
|
|
5283
5431
|
return lines.join("\n");
|
|
5284
5432
|
}
|
|
5285
5433
|
|
|
5434
|
+
// core/run-snapshot.ts
|
|
5435
|
+
var fs8 = __toESM(require("fs-extra"));
|
|
5436
|
+
var _activeSnapshot = null;
|
|
5437
|
+
function getActiveSnapshot() {
|
|
5438
|
+
return _activeSnapshot;
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
// core/run-logger.ts
|
|
5442
|
+
var fs9 = __toESM(require("fs-extra"));
|
|
5443
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
5444
|
+
var _activeLogger = null;
|
|
5445
|
+
function getActiveLogger() {
|
|
5446
|
+
return _activeLogger;
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5286
5449
|
// core/code-generator.ts
|
|
5287
5450
|
function buildSharedConfigSection(context) {
|
|
5288
5451
|
if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
|
|
@@ -5449,13 +5612,13 @@ var CodeGenerator = class {
|
|
|
5449
5612
|
let effectiveMode = this.mode;
|
|
5450
5613
|
if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
|
|
5451
5614
|
console.log(
|
|
5452
|
-
|
|
5615
|
+
import_chalk8.default.yellow(
|
|
5453
5616
|
`
|
|
5454
5617
|
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
5455
5618
|
)
|
|
5456
5619
|
);
|
|
5457
|
-
console.log(
|
|
5458
|
-
console.log(
|
|
5620
|
+
console.log(import_chalk8.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
5621
|
+
console.log(import_chalk8.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
5459
5622
|
`));
|
|
5460
5623
|
effectiveMode = "api";
|
|
5461
5624
|
}
|
|
@@ -5480,16 +5643,16 @@ var CodeGenerator = class {
|
|
|
5480
5643
|
}
|
|
5481
5644
|
}
|
|
5482
5645
|
async runClaudeCode(specFilePath, workingDir, options = {}) {
|
|
5483
|
-
console.log(
|
|
5646
|
+
console.log(import_chalk8.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5484
5647
|
if (!this.isClaudeCLIAvailable()) {
|
|
5485
|
-
console.log(
|
|
5486
|
-
console.log(
|
|
5648
|
+
console.log(import_chalk8.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
|
|
5649
|
+
console.log(import_chalk8.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
|
|
5487
5650
|
return this.runPlanMode(specFilePath);
|
|
5488
5651
|
}
|
|
5489
5652
|
const rtkAvailable = isRtkAvailable();
|
|
5490
5653
|
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
5491
5654
|
if (rtkAvailable) {
|
|
5492
|
-
console.log(
|
|
5655
|
+
console.log(import_chalk8.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
5493
5656
|
}
|
|
5494
5657
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5495
5658
|
if (options.auto && tasks && tasks.length > 0) {
|
|
@@ -5503,29 +5666,29 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5503
5666
|
Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
|
|
5504
5667
|
const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
|
|
5505
5668
|
const promptFile = path6.join(workingDir, ".claude-prompt.txt");
|
|
5506
|
-
await
|
|
5669
|
+
await fs10.writeFile(promptFile, promptContent, "utf-8");
|
|
5507
5670
|
if (options.auto) {
|
|
5508
|
-
console.log(
|
|
5509
|
-
console.log(
|
|
5671
|
+
console.log(import_chalk8.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
5672
|
+
console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
|
|
5510
5673
|
try {
|
|
5511
5674
|
(0, import_child_process.execSync)(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
|
|
5512
5675
|
cwd: workingDir,
|
|
5513
5676
|
stdio: "inherit"
|
|
5514
5677
|
});
|
|
5515
|
-
console.log(
|
|
5678
|
+
console.log(import_chalk8.default.green("\n \u2714 Claude Code completed."));
|
|
5516
5679
|
} catch {
|
|
5517
|
-
console.log(
|
|
5680
|
+
console.log(import_chalk8.default.yellow("\n Claude Code exited. Check output above."));
|
|
5518
5681
|
}
|
|
5519
5682
|
} else {
|
|
5520
|
-
console.log(
|
|
5521
|
-
console.log(
|
|
5522
|
-
if (tasks) console.log(
|
|
5523
|
-
console.log(
|
|
5683
|
+
console.log(import_chalk8.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
|
|
5684
|
+
console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
|
|
5685
|
+
if (tasks) console.log(import_chalk8.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
|
|
5686
|
+
console.log(import_chalk8.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
|
|
5524
5687
|
try {
|
|
5525
5688
|
(0, import_child_process.execSync)(claudeCmd, { cwd: workingDir, stdio: "inherit" });
|
|
5526
|
-
console.log(
|
|
5689
|
+
console.log(import_chalk8.default.green("\n \u2714 Claude Code session completed."));
|
|
5527
5690
|
} catch {
|
|
5528
|
-
console.log(
|
|
5691
|
+
console.log(import_chalk8.default.yellow("\n Claude Code session ended. Continuing workflow."));
|
|
5529
5692
|
}
|
|
5530
5693
|
}
|
|
5531
5694
|
}
|
|
@@ -5538,10 +5701,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5538
5701
|
const pending = tasks.filter((t) => t.status !== "done");
|
|
5539
5702
|
const doneCount = tasks.length - pending.length;
|
|
5540
5703
|
if (options.resume && doneCount > 0) {
|
|
5541
|
-
console.log(
|
|
5704
|
+
console.log(import_chalk8.default.cyan(`
|
|
5542
5705
|
Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
|
|
5543
5706
|
} else {
|
|
5544
|
-
console.log(
|
|
5707
|
+
console.log(import_chalk8.default.cyan(`
|
|
5545
5708
|
Incremental mode: ${tasks.length} tasks`));
|
|
5546
5709
|
}
|
|
5547
5710
|
let completed = doneCount;
|
|
@@ -5569,32 +5732,32 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
5569
5732
|
completed++;
|
|
5570
5733
|
} catch {
|
|
5571
5734
|
taskStatus = "failed";
|
|
5572
|
-
console.log(
|
|
5735
|
+
console.log(import_chalk8.default.yellow(`
|
|
5573
5736
|
\u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
|
|
5574
5737
|
}
|
|
5575
5738
|
await updateTaskStatus(specFilePath, task.id, taskStatus);
|
|
5576
5739
|
}
|
|
5577
5740
|
const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
|
|
5578
5741
|
console.log(
|
|
5579
|
-
|
|
5742
|
+
import_chalk8.default.bold(
|
|
5580
5743
|
`
|
|
5581
|
-
${successCount === tasks.length ?
|
|
5744
|
+
${successCount === tasks.length ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
|
|
5582
5745
|
)
|
|
5583
5746
|
);
|
|
5584
5747
|
}
|
|
5585
5748
|
// ── Mode: api ─────────────────────────────────────────────────────────────
|
|
5586
5749
|
async runApiMode(specFilePath, workingDir, context, options = {}) {
|
|
5587
5750
|
console.log(
|
|
5588
|
-
|
|
5751
|
+
import_chalk8.default.blue(
|
|
5589
5752
|
`
|
|
5590
5753
|
\u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
|
|
5591
5754
|
)
|
|
5592
5755
|
);
|
|
5593
5756
|
const systemPrompt = getCodeGenSystemPrompt(options.repoType);
|
|
5594
5757
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
5595
|
-
console.log(
|
|
5758
|
+
console.log(import_chalk8.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
5596
5759
|
}
|
|
5597
|
-
const spec = await
|
|
5760
|
+
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
5598
5761
|
const constitutionSection = context?.constitution ? `
|
|
5599
5762
|
=== Project Constitution (MUST follow) ===
|
|
5600
5763
|
${context.constitution}
|
|
@@ -5610,7 +5773,7 @@ ${buildDslContextSection(dsl)}
|
|
|
5610
5773
|
if (dsl) {
|
|
5611
5774
|
const cmpCount = dsl.components?.length ?? 0;
|
|
5612
5775
|
const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
|
|
5613
|
-
console.log(
|
|
5776
|
+
console.log(import_chalk8.default.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
|
|
5614
5777
|
}
|
|
5615
5778
|
const isFrontend = isFrontendDeps(context?.dependencies ?? []);
|
|
5616
5779
|
let frontendSection = "";
|
|
@@ -5619,13 +5782,13 @@ ${buildDslContextSection(dsl)}
|
|
|
5619
5782
|
frontendSection = `
|
|
5620
5783
|
${buildFrontendContextSection(fctx)}
|
|
5621
5784
|
`;
|
|
5622
|
-
console.log(
|
|
5785
|
+
console.log(import_chalk8.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
5623
5786
|
}
|
|
5624
5787
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5625
5788
|
if (tasks && tasks.length > 0) {
|
|
5626
5789
|
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
5627
5790
|
}
|
|
5628
|
-
console.log(
|
|
5791
|
+
console.log(import_chalk8.default.gray(" [1/2] Planning implementation files..."));
|
|
5629
5792
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
5630
5793
|
|
|
5631
5794
|
IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
|
|
@@ -5648,18 +5811,18 @@ Output ONLY a valid JSON array:
|
|
|
5648
5811
|
const planResponse = await this.provider.generate(planPrompt, systemPrompt);
|
|
5649
5812
|
filePlan = parseJsonArray(planResponse);
|
|
5650
5813
|
} catch (err) {
|
|
5651
|
-
console.error(
|
|
5814
|
+
console.error(import_chalk8.default.red(" Failed to generate file plan:"), err);
|
|
5652
5815
|
}
|
|
5653
5816
|
if (filePlan.length === 0) {
|
|
5654
|
-
console.log(
|
|
5817
|
+
console.log(import_chalk8.default.yellow(" Could not determine file plan. Falling back to plan mode."));
|
|
5655
5818
|
await this.runPlanMode(specFilePath);
|
|
5656
5819
|
return [];
|
|
5657
5820
|
}
|
|
5658
|
-
console.log(
|
|
5821
|
+
console.log(import_chalk8.default.cyan(`
|
|
5659
5822
|
Plan: ${filePlan.length} file(s) to process`));
|
|
5660
5823
|
filePlan.forEach((item) => {
|
|
5661
|
-
const icon = item.action === "create" ?
|
|
5662
|
-
console.log(` ${icon} ${item.file}: ${
|
|
5824
|
+
const icon = item.action === "create" ? import_chalk8.default.green("+") : import_chalk8.default.yellow("~");
|
|
5825
|
+
console.log(` ${icon} ${item.file}: ${import_chalk8.default.gray(item.description)}`);
|
|
5663
5826
|
});
|
|
5664
5827
|
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
5665
5828
|
return files;
|
|
@@ -5668,13 +5831,13 @@ Output ONLY a valid JSON array:
|
|
|
5668
5831
|
const pendingTasks = tasks.filter((t) => t.status !== "done");
|
|
5669
5832
|
const doneCount = tasks.length - pendingTasks.length;
|
|
5670
5833
|
if (options.resume && doneCount > 0) {
|
|
5671
|
-
console.log(
|
|
5672
|
-
Task-based generation (resume): ${tasks.length} tasks (${
|
|
5834
|
+
console.log(import_chalk8.default.cyan(`
|
|
5835
|
+
Task-based generation (resume): ${tasks.length} tasks (${import_chalk8.default.green(doneCount + " already done")}, skipping)`));
|
|
5673
5836
|
} else if (doneCount > 0) {
|
|
5674
|
-
console.log(
|
|
5675
|
-
Task-based generation: ${tasks.length} tasks (${
|
|
5837
|
+
console.log(import_chalk8.default.cyan(`
|
|
5838
|
+
Task-based generation: ${tasks.length} tasks (${import_chalk8.default.green(doneCount + " already done")}, resuming from checkpoint)`));
|
|
5676
5839
|
} else {
|
|
5677
|
-
console.log(
|
|
5840
|
+
console.log(import_chalk8.default.cyan(`
|
|
5678
5841
|
Task-based generation: ${tasks.length} tasks`));
|
|
5679
5842
|
}
|
|
5680
5843
|
const sharedConfigPaths = new Set(
|
|
@@ -5706,9 +5869,9 @@ Output ONLY a valid JSON array:
|
|
|
5706
5869
|
const pct = Math.round(completedTasks / tasks.length * 100);
|
|
5707
5870
|
const barWidth = 20;
|
|
5708
5871
|
const filled = Math.round(pct / 100 * barWidth);
|
|
5709
|
-
const bar =
|
|
5872
|
+
const bar = import_chalk8.default.green("\u2588".repeat(filled)) + import_chalk8.default.gray("\u2591".repeat(barWidth - filled));
|
|
5710
5873
|
console.log(
|
|
5711
|
-
|
|
5874
|
+
import_chalk8.default.bold(`
|
|
5712
5875
|
[${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
|
|
5713
5876
|
);
|
|
5714
5877
|
} else {
|
|
@@ -5716,12 +5879,12 @@ Output ONLY a valid JSON array:
|
|
|
5716
5879
|
}
|
|
5717
5880
|
const executeTask = async (task, batchIsParallel) => {
|
|
5718
5881
|
if (task.filesToTouch.length === 0) {
|
|
5719
|
-
if (!batchIsParallel) console.log(
|
|
5882
|
+
if (!batchIsParallel) console.log(import_chalk8.default.gray(" No files specified, skipping."));
|
|
5720
5883
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
5721
5884
|
}
|
|
5722
5885
|
const filePlan = await Promise.all(
|
|
5723
5886
|
task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
|
|
5724
|
-
const exists = await
|
|
5887
|
+
const exists = await fs10.pathExists(path6.join(workingDir, f));
|
|
5725
5888
|
return {
|
|
5726
5889
|
file: f,
|
|
5727
5890
|
action: exists ? "modify" : "create",
|
|
@@ -5761,7 +5924,7 @@ ${taskContext}`,
|
|
|
5761
5924
|
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
|
|
5762
5925
|
if (isCodeFile || isViewFile) {
|
|
5763
5926
|
try {
|
|
5764
|
-
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await
|
|
5927
|
+
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path6.join(workingDir, writtenFile), "utf-8");
|
|
5765
5928
|
generatedFileCache.set(writtenFile, content);
|
|
5766
5929
|
} catch {
|
|
5767
5930
|
}
|
|
@@ -5773,7 +5936,12 @@ ${taskContext}`,
|
|
|
5773
5936
|
const layerResults = [];
|
|
5774
5937
|
for (const batch of taskBatches) {
|
|
5775
5938
|
const batchIsParallel = batch.length > 1;
|
|
5776
|
-
const batchResultPromises = batch.map(
|
|
5939
|
+
const batchResultPromises = batch.map(
|
|
5940
|
+
(task) => executeTask(task, batchIsParallel).catch((err) => {
|
|
5941
|
+
console.log(import_chalk8.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
|
|
5942
|
+
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
5943
|
+
})
|
|
5944
|
+
);
|
|
5777
5945
|
const batchResults = await Promise.all(batchResultPromises);
|
|
5778
5946
|
layerResults.push(...batchResults);
|
|
5779
5947
|
await updateCacheFromBatch(batchResults);
|
|
@@ -5786,14 +5954,14 @@ ${taskContext}`,
|
|
|
5786
5954
|
totalFiles += result.total;
|
|
5787
5955
|
allGeneratedFiles.push(...result.files);
|
|
5788
5956
|
if (isParallel) {
|
|
5789
|
-
const icon = result.success === result.total ?
|
|
5957
|
+
const icon = result.success === result.total ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!");
|
|
5790
5958
|
const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
|
|
5791
5959
|
console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
|
|
5792
5960
|
}
|
|
5793
5961
|
const taskStatus = result.success === result.total ? "done" : "failed";
|
|
5794
5962
|
await updateTaskStatus(specFilePath, result.task.id, taskStatus);
|
|
5795
5963
|
if (taskStatus === "failed") {
|
|
5796
|
-
console.log(
|
|
5964
|
+
console.log(import_chalk8.default.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
|
|
5797
5965
|
}
|
|
5798
5966
|
}
|
|
5799
5967
|
completedTasks += layerTasks.length;
|
|
@@ -5808,7 +5976,7 @@ ${taskContext}`,
|
|
|
5808
5976
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
5809
5977
|
purpose = `Add to this file: import ${newModuleNames.join(", ")} from their respective paths and register them in the export/default array. Do NOT remove any existing imports.`;
|
|
5810
5978
|
}
|
|
5811
|
-
console.log(
|
|
5979
|
+
console.log(import_chalk8.default.gray(`
|
|
5812
5980
|
+ updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
|
|
5813
5981
|
const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
5814
5982
|
await this.generateFiles(
|
|
@@ -5826,17 +5994,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
5826
5994
|
}
|
|
5827
5995
|
}
|
|
5828
5996
|
console.log(
|
|
5829
|
-
|
|
5997
|
+
import_chalk8.default.bold(
|
|
5830
5998
|
`
|
|
5831
|
-
${totalSuccess === totalFiles ?
|
|
5999
|
+
${totalSuccess === totalFiles ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
|
|
5832
6000
|
)
|
|
5833
6001
|
);
|
|
5834
6002
|
return allGeneratedFiles;
|
|
5835
6003
|
}
|
|
5836
6004
|
async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
|
|
5837
|
-
const prefix = taskLabel ? ` [${
|
|
6005
|
+
const prefix = taskLabel ? ` [${import_chalk8.default.cyan(taskLabel)}] ` : " ";
|
|
5838
6006
|
if (!taskLabel) {
|
|
5839
|
-
console.log(
|
|
6007
|
+
console.log(import_chalk8.default.gray(`
|
|
5840
6008
|
Generating ${filePlan.length} file(s)...`));
|
|
5841
6009
|
}
|
|
5842
6010
|
let successCount = 0;
|
|
@@ -5844,8 +6012,8 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
5844
6012
|
for (const item of filePlan) {
|
|
5845
6013
|
const fullPath = path6.join(workingDir, item.file);
|
|
5846
6014
|
let existingContent = "";
|
|
5847
|
-
if (await
|
|
5848
|
-
existingContent = await
|
|
6015
|
+
if (await fs10.pathExists(fullPath)) {
|
|
6016
|
+
existingContent = await fs10.readFile(fullPath, "utf-8");
|
|
5849
6017
|
}
|
|
5850
6018
|
const codePrompt = `Implement this file.
|
|
5851
6019
|
|
|
@@ -5860,19 +6028,21 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
5860
6028
|
try {
|
|
5861
6029
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
5862
6030
|
const fileContent = stripCodeFences(raw);
|
|
5863
|
-
await
|
|
5864
|
-
await
|
|
5865
|
-
|
|
6031
|
+
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
6032
|
+
await fs10.ensureDir(path6.dirname(fullPath));
|
|
6033
|
+
await fs10.writeFile(fullPath, fileContent, "utf-8");
|
|
6034
|
+
getActiveLogger()?.fileWritten(item.file);
|
|
6035
|
+
console.log(`${prefix}${existingContent ? import_chalk8.default.yellow("~") : import_chalk8.default.green("+")} ${import_chalk8.default.bold(item.file)} ${import_chalk8.default.green("\u2714")}`);
|
|
5866
6036
|
successCount++;
|
|
5867
6037
|
writtenFiles.push(item.file);
|
|
5868
6038
|
} catch (err) {
|
|
5869
|
-
console.log(`${prefix}${
|
|
6039
|
+
console.log(`${prefix}${import_chalk8.default.red("\u2718")} ${import_chalk8.default.bold(item.file)} \u2014 ${import_chalk8.default.red(err.message)}`);
|
|
5870
6040
|
}
|
|
5871
6041
|
}
|
|
5872
6042
|
if (!taskLabel) {
|
|
5873
6043
|
console.log(
|
|
5874
|
-
|
|
5875
|
-
` ${successCount === filePlan.length ?
|
|
6044
|
+
import_chalk8.default.bold(
|
|
6045
|
+
` ${successCount === filePlan.length ? import_chalk8.default.green("\u2714") : import_chalk8.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
|
|
5876
6046
|
)
|
|
5877
6047
|
);
|
|
5878
6048
|
}
|
|
@@ -5880,8 +6050,8 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
5880
6050
|
}
|
|
5881
6051
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
5882
6052
|
async runPlanMode(specFilePath) {
|
|
5883
|
-
console.log(
|
|
5884
|
-
const spec = await
|
|
6053
|
+
console.log(import_chalk8.default.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"));
|
|
6054
|
+
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
5885
6055
|
const plan = await this.provider.generate(
|
|
5886
6056
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
5887
6057
|
Be specific about:
|
|
@@ -5893,7 +6063,7 @@ Be specific about:
|
|
|
5893
6063
|
${spec}`,
|
|
5894
6064
|
"You are a senior developer creating an actionable implementation guide."
|
|
5895
6065
|
);
|
|
5896
|
-
console.log(
|
|
6066
|
+
console.log(import_chalk8.default.cyan("\n") + plan);
|
|
5897
6067
|
}
|
|
5898
6068
|
};
|
|
5899
6069
|
function topoSortLayerTasks(tasks) {
|
|
@@ -5944,32 +6114,32 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
5944
6114
|
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
5945
6115
|
const barWidth = 20;
|
|
5946
6116
|
const filled = Math.round(pct / 100 * barWidth);
|
|
5947
|
-
const bar =
|
|
6117
|
+
const bar = import_chalk8.default.green("\u2588".repeat(filled)) + import_chalk8.default.gray("\u2591".repeat(barWidth - filled));
|
|
5948
6118
|
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
5949
6119
|
if (mode === "skip") {
|
|
5950
6120
|
console.log(
|
|
5951
|
-
|
|
6121
|
+
import_chalk8.default.gray(`
|
|
5952
6122
|
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
5953
6123
|
);
|
|
5954
6124
|
} else {
|
|
5955
6125
|
console.log(
|
|
5956
|
-
|
|
6126
|
+
import_chalk8.default.bold(`
|
|
5957
6127
|
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
5958
6128
|
);
|
|
5959
6129
|
}
|
|
5960
6130
|
}
|
|
5961
6131
|
|
|
5962
6132
|
// core/reviewer.ts
|
|
5963
|
-
var
|
|
6133
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
5964
6134
|
var import_child_process2 = require("child_process");
|
|
5965
6135
|
var path7 = __toESM(require("path"));
|
|
5966
|
-
var
|
|
6136
|
+
var fs11 = __toESM(require("fs-extra"));
|
|
5967
6137
|
var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5968
6138
|
async function loadReviewHistory(projectRoot) {
|
|
5969
6139
|
const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
5970
6140
|
try {
|
|
5971
|
-
if (await
|
|
5972
|
-
return await
|
|
6141
|
+
if (await fs11.pathExists(historyPath)) {
|
|
6142
|
+
return await fs11.readJson(historyPath);
|
|
5973
6143
|
}
|
|
5974
6144
|
} catch {
|
|
5975
6145
|
}
|
|
@@ -5980,7 +6150,7 @@ async function appendReviewHistory(projectRoot, entry) {
|
|
|
5980
6150
|
const existing = await loadReviewHistory(projectRoot);
|
|
5981
6151
|
const updated = [...existing, entry].slice(-20);
|
|
5982
6152
|
try {
|
|
5983
|
-
await
|
|
6153
|
+
await fs11.writeJson(historyPath, updated, { spaces: 2 });
|
|
5984
6154
|
} catch {
|
|
5985
6155
|
}
|
|
5986
6156
|
}
|
|
@@ -5988,6 +6158,14 @@ function extractScore(reviewText) {
|
|
|
5988
6158
|
const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
5989
6159
|
return match ? parseFloat(match[1]) : 0;
|
|
5990
6160
|
}
|
|
6161
|
+
function extractImpactLevel(reviewText) {
|
|
6162
|
+
const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
|
|
6163
|
+
return match ? match[1] : void 0;
|
|
6164
|
+
}
|
|
6165
|
+
function extractComplexityLevel(reviewText) {
|
|
6166
|
+
const match = reviewText.match(/复杂度等级[::]\s*(低|中|高)/);
|
|
6167
|
+
return match ? match[1] : void 0;
|
|
6168
|
+
}
|
|
5991
6169
|
function extractTopIssues(reviewText) {
|
|
5992
6170
|
const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
|
|
5993
6171
|
return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
|
|
@@ -6033,15 +6211,14 @@ var CodeReviewer = class {
|
|
|
6033
6211
|
};
|
|
6034
6212
|
}
|
|
6035
6213
|
/**
|
|
6036
|
-
*
|
|
6214
|
+
* Three-pass review:
|
|
6037
6215
|
* Pass 1 — architecture (spec compliance, layer separation, auth)
|
|
6038
6216
|
* Pass 2 — implementation details (validation, error handling, edge cases)
|
|
6039
6217
|
* + historical issue recurrence check
|
|
6040
|
-
*
|
|
6041
|
-
* Falls back to single-pass if the two-pass flag is not set.
|
|
6218
|
+
* Pass 3 — impact assessment + code complexity
|
|
6042
6219
|
*/
|
|
6043
|
-
async
|
|
6044
|
-
console.log(
|
|
6220
|
+
async runThreePassReview(specContent, codeContext, specFile) {
|
|
6221
|
+
console.log(import_chalk9.default.gray(" Pass 1/3: Architecture review..."));
|
|
6045
6222
|
const archPrompt = `Review the architecture of this change.
|
|
6046
6223
|
|
|
6047
6224
|
=== Feature Spec ===
|
|
@@ -6050,7 +6227,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
6050
6227
|
=== Code ===
|
|
6051
6228
|
${codeContext}`;
|
|
6052
6229
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
6053
|
-
console.log(
|
|
6230
|
+
console.log(import_chalk9.default.gray(" Pass 2/3: Implementation review..."));
|
|
6054
6231
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6055
6232
|
const historyContext = buildHistoryContext(history);
|
|
6056
6233
|
const implPrompt = `Review the implementation details of this change.
|
|
@@ -6065,61 +6242,85 @@ ${codeContext}
|
|
|
6065
6242
|
${archReview}
|
|
6066
6243
|
${historyContext}`;
|
|
6067
6244
|
const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
|
|
6068
|
-
|
|
6245
|
+
console.log(import_chalk9.default.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6246
|
+
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6069
6247
|
|
|
6070
|
-
|
|
6248
|
+
=== Feature Spec ===
|
|
6249
|
+
${specContent || "(No spec \u2014 review for general code quality)"}
|
|
6071
6250
|
|
|
6251
|
+
=== Code ===
|
|
6252
|
+
${codeContext}
|
|
6253
|
+
|
|
6254
|
+
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
6255
|
+
${archReview}
|
|
6256
|
+
|
|
6257
|
+
=== Implementation Review (Pass 2 \u2014 do NOT repeat) ===
|
|
6072
6258
|
${implReview}`;
|
|
6259
|
+
const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
|
|
6260
|
+
const sep = "\u2500".repeat(52);
|
|
6261
|
+
const combined = `${archReview}
|
|
6262
|
+
|
|
6263
|
+
${sep}
|
|
6264
|
+
|
|
6265
|
+
${implReview}
|
|
6266
|
+
|
|
6267
|
+
${sep}
|
|
6268
|
+
|
|
6269
|
+
${impactReview}`;
|
|
6073
6270
|
const score = extractScore(implReview) || extractScore(archReview);
|
|
6074
6271
|
const topIssues = extractTopIssues(implReview);
|
|
6272
|
+
const impactLevel = extractImpactLevel(impactReview);
|
|
6273
|
+
const complexityLevel = extractComplexityLevel(impactReview);
|
|
6075
6274
|
if (score > 0 && specFile) {
|
|
6076
6275
|
await appendReviewHistory(this.projectRoot, {
|
|
6077
6276
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
6078
6277
|
specFile: path7.relative(this.projectRoot, specFile),
|
|
6079
6278
|
score,
|
|
6080
|
-
topIssues
|
|
6279
|
+
topIssues,
|
|
6280
|
+
...impactLevel ? { impactLevel } : {},
|
|
6281
|
+
...complexityLevel ? { complexityLevel } : {}
|
|
6081
6282
|
});
|
|
6082
6283
|
}
|
|
6083
6284
|
return combined;
|
|
6084
6285
|
}
|
|
6085
6286
|
async reviewCode(specContent, specFile) {
|
|
6086
|
-
console.log(
|
|
6287
|
+
console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6087
6288
|
const diff = this.getGitDiff();
|
|
6088
6289
|
if (!diff.trim()) {
|
|
6089
6290
|
console.log(
|
|
6090
|
-
|
|
6291
|
+
import_chalk9.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
|
|
6091
6292
|
);
|
|
6092
|
-
console.log(
|
|
6293
|
+
console.log(import_chalk9.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
|
|
6093
6294
|
return "No changes";
|
|
6094
6295
|
}
|
|
6095
6296
|
const { files, added, removed } = this.getDiffStats(diff);
|
|
6096
6297
|
console.log(
|
|
6097
|
-
|
|
6298
|
+
import_chalk9.default.gray(` Diff: ${files} file(s), ${import_chalk9.default.green("+" + added)} ${import_chalk9.default.red("-" + removed)}`)
|
|
6098
6299
|
);
|
|
6099
6300
|
console.log(
|
|
6100
|
-
|
|
6301
|
+
import_chalk9.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6101
6302
|
);
|
|
6102
6303
|
const codeContext = diff.slice(0, 1e4);
|
|
6103
|
-
const reviewResult = await this.
|
|
6104
|
-
console.log(
|
|
6304
|
+
const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
|
|
6305
|
+
console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6105
6306
|
console.log(reviewResult);
|
|
6106
|
-
console.log(
|
|
6307
|
+
console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6107
6308
|
return reviewResult;
|
|
6108
6309
|
}
|
|
6109
6310
|
/**
|
|
6110
6311
|
* Review directly from generated file contents (for api mode where git diff is empty).
|
|
6111
6312
|
*/
|
|
6112
6313
|
async reviewFiles(specContent, filePaths, workingDir, specFile) {
|
|
6113
|
-
console.log(
|
|
6114
|
-
console.log(
|
|
6314
|
+
console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6315
|
+
console.log(import_chalk9.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
|
|
6115
6316
|
console.log(
|
|
6116
|
-
|
|
6317
|
+
import_chalk9.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6117
6318
|
);
|
|
6118
6319
|
let filesSection = "";
|
|
6119
6320
|
for (const filePath of filePaths) {
|
|
6120
6321
|
const fullPath = path7.join(workingDir, filePath);
|
|
6121
6322
|
try {
|
|
6122
|
-
const content = await
|
|
6323
|
+
const content = await fs11.readFile(fullPath, "utf-8");
|
|
6123
6324
|
filesSection += `
|
|
6124
6325
|
|
|
6125
6326
|
=== ${filePath} ===
|
|
@@ -6133,33 +6334,33 @@ ${content.slice(0, 3e3)}`;
|
|
|
6133
6334
|
(file not found)`;
|
|
6134
6335
|
}
|
|
6135
6336
|
}
|
|
6136
|
-
const reviewResult = await this.
|
|
6137
|
-
console.log(
|
|
6337
|
+
const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
|
|
6338
|
+
console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6138
6339
|
console.log(reviewResult);
|
|
6139
|
-
console.log(
|
|
6340
|
+
console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6140
6341
|
return reviewResult;
|
|
6141
6342
|
}
|
|
6142
6343
|
/** Print score trend from history (last N reviews) */
|
|
6143
6344
|
async printScoreTrend(limit = 5) {
|
|
6144
6345
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6145
6346
|
if (history.length === 0) {
|
|
6146
|
-
console.log(
|
|
6347
|
+
console.log(import_chalk9.default.gray(" No review history yet."));
|
|
6147
6348
|
return;
|
|
6148
6349
|
}
|
|
6149
6350
|
const recent = history.slice(-limit);
|
|
6150
|
-
console.log(
|
|
6351
|
+
console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Score 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"));
|
|
6151
6352
|
for (const entry of recent) {
|
|
6152
6353
|
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
6153
|
-
const color = entry.score >= 8 ?
|
|
6354
|
+
const color = entry.score >= 8 ? import_chalk9.default.green : entry.score >= 6 ? import_chalk9.default.yellow : import_chalk9.default.red;
|
|
6154
6355
|
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path7.basename(entry.specFile)}`);
|
|
6155
6356
|
}
|
|
6156
|
-
console.log(
|
|
6357
|
+
console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6157
6358
|
}
|
|
6158
6359
|
};
|
|
6159
6360
|
|
|
6160
6361
|
// core/constitution-generator.ts
|
|
6161
|
-
var
|
|
6162
|
-
var
|
|
6362
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
6363
|
+
var fs12 = __toESM(require("fs-extra"));
|
|
6163
6364
|
var path8 = __toESM(require("path"));
|
|
6164
6365
|
|
|
6165
6366
|
// prompts/constitution.prompt.ts
|
|
@@ -6241,7 +6442,7 @@ var ConstitutionGenerator = class {
|
|
|
6241
6442
|
}
|
|
6242
6443
|
async saveConstitution(projectRoot, content) {
|
|
6243
6444
|
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6244
|
-
await
|
|
6445
|
+
await fs12.writeFile(filePath, content, "utf-8");
|
|
6245
6446
|
return filePath;
|
|
6246
6447
|
}
|
|
6247
6448
|
};
|
|
@@ -6300,15 +6501,15 @@ ${sections.join("\n")}
|
|
|
6300
6501
|
}
|
|
6301
6502
|
async function loadConstitution(projectRoot) {
|
|
6302
6503
|
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6303
|
-
if (await
|
|
6304
|
-
return
|
|
6504
|
+
if (await fs12.pathExists(filePath)) {
|
|
6505
|
+
return fs12.readFile(filePath, "utf-8");
|
|
6305
6506
|
}
|
|
6306
6507
|
return void 0;
|
|
6307
6508
|
}
|
|
6308
6509
|
function printConstitutionHint(exists) {
|
|
6309
6510
|
if (!exists) {
|
|
6310
6511
|
console.log(
|
|
6311
|
-
|
|
6512
|
+
import_chalk10.default.yellow(
|
|
6312
6513
|
" \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
|
|
6313
6514
|
)
|
|
6314
6515
|
);
|
|
@@ -6353,8 +6554,8 @@ function parseSpecAndTasks(raw) {
|
|
|
6353
6554
|
// git/worktree.ts
|
|
6354
6555
|
var import_child_process3 = require("child_process");
|
|
6355
6556
|
var path9 = __toESM(require("path"));
|
|
6356
|
-
var
|
|
6357
|
-
var
|
|
6557
|
+
var fs13 = __toESM(require("fs-extra"));
|
|
6558
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
6358
6559
|
var GitWorktreeManager = class {
|
|
6359
6560
|
constructor(baseDir) {
|
|
6360
6561
|
this.baseDir = baseDir;
|
|
@@ -6381,30 +6582,30 @@ var GitWorktreeManager = class {
|
|
|
6381
6582
|
for (const dir of candidates) {
|
|
6382
6583
|
const src = path9.join(this.baseDir, dir);
|
|
6383
6584
|
const dest = path9.join(worktreePath, dir);
|
|
6384
|
-
if (!await
|
|
6385
|
-
if (await
|
|
6585
|
+
if (!await fs13.pathExists(src)) continue;
|
|
6586
|
+
if (await fs13.pathExists(dest)) continue;
|
|
6386
6587
|
try {
|
|
6387
|
-
await
|
|
6388
|
-
console.log(
|
|
6588
|
+
await fs13.ensureSymlink(src, dest, "dir");
|
|
6589
|
+
console.log(import_chalk11.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
6389
6590
|
} catch (err) {
|
|
6390
|
-
console.log(
|
|
6391
|
-
console.log(
|
|
6591
|
+
console.log(import_chalk11.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
6592
|
+
console.log(import_chalk11.default.yellow(` Run \`npm install\` inside the worktree manually.`));
|
|
6392
6593
|
}
|
|
6393
6594
|
}
|
|
6394
6595
|
}
|
|
6395
6596
|
async createWorktree(idea) {
|
|
6396
6597
|
if (!this.isGitRepo()) {
|
|
6397
|
-
console.log(
|
|
6598
|
+
console.log(import_chalk11.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
|
|
6398
6599
|
return null;
|
|
6399
6600
|
}
|
|
6400
6601
|
const featureName = this.sanitizeFeatureName(idea);
|
|
6401
6602
|
const branchName = `feature/${featureName}`;
|
|
6402
6603
|
const repoName = path9.basename(this.baseDir);
|
|
6403
6604
|
const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
6404
|
-
console.log(
|
|
6605
|
+
console.log(import_chalk11.default.cyan(`
|
|
6405
6606
|
--- Setting up Git Worktree ---`));
|
|
6406
|
-
if (await
|
|
6407
|
-
console.log(
|
|
6607
|
+
if (await fs13.pathExists(worktreePath)) {
|
|
6608
|
+
console.log(import_chalk11.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
6408
6609
|
await this.linkDependencies(worktreePath);
|
|
6409
6610
|
return worktreePath;
|
|
6410
6611
|
}
|
|
@@ -6418,7 +6619,7 @@ var GitWorktreeManager = class {
|
|
|
6418
6619
|
branchExists = true;
|
|
6419
6620
|
} catch {
|
|
6420
6621
|
}
|
|
6421
|
-
console.log(
|
|
6622
|
+
console.log(import_chalk11.default.gray(`Creating worktree at: ${worktreePath}`));
|
|
6422
6623
|
if (branchExists) {
|
|
6423
6624
|
(0, import_child_process3.execSync)(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6424
6625
|
cwd: this.baseDir,
|
|
@@ -6431,12 +6632,12 @@ var GitWorktreeManager = class {
|
|
|
6431
6632
|
});
|
|
6432
6633
|
}
|
|
6433
6634
|
console.log(
|
|
6434
|
-
|
|
6635
|
+
import_chalk11.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
|
|
6435
6636
|
);
|
|
6436
6637
|
await this.linkDependencies(worktreePath);
|
|
6437
6638
|
return worktreePath;
|
|
6438
6639
|
} catch (error) {
|
|
6439
|
-
console.error(
|
|
6640
|
+
console.error(import_chalk11.default.red("Failed to create git worktree:"), error);
|
|
6440
6641
|
return null;
|
|
6441
6642
|
}
|
|
6442
6643
|
}
|