ai-spec-dev 0.24.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 +1 -4
- package/README.md +11 -3
- 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.mjs
CHANGED
|
@@ -109,6 +109,67 @@ model ExampleModel {
|
|
|
109
109
|
|
|
110
110
|
\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`;
|
|
111
111
|
|
|
112
|
+
// core/provider-utils.ts
|
|
113
|
+
import chalk from "chalk";
|
|
114
|
+
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
115
|
+
var ProviderError = class extends Error {
|
|
116
|
+
constructor(message, kind, originalError) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.kind = kind;
|
|
119
|
+
this.originalError = originalError;
|
|
120
|
+
this.name = "ProviderError";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function classifyError(err, label) {
|
|
124
|
+
const e = err;
|
|
125
|
+
const status = e.status ?? e.response?.status;
|
|
126
|
+
if (status === 401 || status === 403)
|
|
127
|
+
return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
|
|
128
|
+
if (status === 429)
|
|
129
|
+
return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
|
|
130
|
+
if (e._timeout || e.message?.toLowerCase().includes("timed out"))
|
|
131
|
+
return new ProviderError(`Request timed out (${label})`, "timeout", err);
|
|
132
|
+
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
|
|
133
|
+
return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
|
|
134
|
+
return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
|
|
135
|
+
}
|
|
136
|
+
function isRetryable(err) {
|
|
137
|
+
const e = err;
|
|
138
|
+
const status = e.status ?? e.response?.status;
|
|
139
|
+
if (status === 401 || status === 403) return false;
|
|
140
|
+
if (status === 429 || status !== void 0 && status >= 500) return true;
|
|
141
|
+
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED") return true;
|
|
142
|
+
if (e.message?.toLowerCase().includes("timed out")) return true;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
async function withReliability(fn, opts) {
|
|
146
|
+
const { retries = 2, timeoutMs = 9e4, label = "AI call", onRetry } = opts ?? {};
|
|
147
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
148
|
+
try {
|
|
149
|
+
return await Promise.race([
|
|
150
|
+
fn(),
|
|
151
|
+
new Promise(
|
|
152
|
+
(_2, reject) => setTimeout(
|
|
153
|
+
() => reject(Object.assign(new Error(`timed out after ${timeoutMs / 1e3}s`), { _timeout: true })),
|
|
154
|
+
timeoutMs
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
]);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (!isRetryable(err) || attempt === retries) {
|
|
160
|
+
throw classifyError(err, label);
|
|
161
|
+
}
|
|
162
|
+
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
163
|
+
console.warn(
|
|
164
|
+
chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
|
|
165
|
+
);
|
|
166
|
+
onRetry?.(attempt + 1, err);
|
|
167
|
+
await sleep(waitMs);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new Error("unreachable");
|
|
171
|
+
}
|
|
172
|
+
|
|
112
173
|
// core/spec-generator.ts
|
|
113
174
|
function geminiRequestOptions() {
|
|
114
175
|
const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
@@ -254,12 +315,17 @@ var GeminiProvider = class {
|
|
|
254
315
|
this.modelName = modelName;
|
|
255
316
|
}
|
|
256
317
|
async generate(prompt, systemInstruction) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
318
|
+
return withReliability(
|
|
319
|
+
async () => {
|
|
320
|
+
const model = this.genAI.getGenerativeModel(
|
|
321
|
+
{ model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
|
|
322
|
+
geminiRequestOptions()
|
|
323
|
+
);
|
|
324
|
+
const result = await model.generateContent(prompt);
|
|
325
|
+
return result.response.text();
|
|
326
|
+
},
|
|
327
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
260
328
|
);
|
|
261
|
-
const result = await model.generateContent(prompt);
|
|
262
|
-
return result.response.text();
|
|
263
329
|
}
|
|
264
330
|
};
|
|
265
331
|
var ClaudeProvider = class {
|
|
@@ -271,15 +337,20 @@ var ClaudeProvider = class {
|
|
|
271
337
|
this.modelName = modelName;
|
|
272
338
|
}
|
|
273
339
|
async generate(prompt, systemInstruction) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
340
|
+
return withReliability(
|
|
341
|
+
async () => {
|
|
342
|
+
const message = await this.client.messages.create({
|
|
343
|
+
model: this.modelName,
|
|
344
|
+
max_tokens: 8192,
|
|
345
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
346
|
+
messages: [{ role: "user", content: prompt }]
|
|
347
|
+
});
|
|
348
|
+
const block = message.content[0];
|
|
349
|
+
if (block.type === "text") return block.text;
|
|
350
|
+
throw new Error("Unexpected response type from Claude API");
|
|
351
|
+
},
|
|
352
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
353
|
+
);
|
|
283
354
|
}
|
|
284
355
|
};
|
|
285
356
|
var OpenAICompatibleProvider = class {
|
|
@@ -299,19 +370,24 @@ var OpenAICompatibleProvider = class {
|
|
|
299
370
|
});
|
|
300
371
|
}
|
|
301
372
|
async generate(prompt, systemInstruction) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
373
|
+
return withReliability(
|
|
374
|
+
async () => {
|
|
375
|
+
const messages = [];
|
|
376
|
+
if (systemInstruction) {
|
|
377
|
+
const isOSeries = /^o[13]/.test(this.modelName);
|
|
378
|
+
const role = isOSeries ? "developer" : this.systemRole;
|
|
379
|
+
messages.push({ role, content: systemInstruction });
|
|
380
|
+
}
|
|
381
|
+
messages.push({ role: "user", content: prompt });
|
|
382
|
+
const completion = await this.client.chat.completions.create({
|
|
383
|
+
model: this.modelName,
|
|
384
|
+
messages,
|
|
385
|
+
...this.extraBody ? { extra_body: this.extraBody } : {}
|
|
386
|
+
});
|
|
387
|
+
return completion.choices[0].message.content ?? "";
|
|
388
|
+
},
|
|
389
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
390
|
+
);
|
|
315
391
|
}
|
|
316
392
|
};
|
|
317
393
|
var MiMoProvider = class {
|
|
@@ -324,32 +400,37 @@ var MiMoProvider = class {
|
|
|
324
400
|
this.modelName = modelName;
|
|
325
401
|
}
|
|
326
402
|
async generate(prompt, systemInstruction) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
403
|
+
return withReliability(
|
|
404
|
+
async () => {
|
|
405
|
+
const body = {
|
|
406
|
+
model: this.modelName,
|
|
407
|
+
max_tokens: 16384,
|
|
408
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
409
|
+
top_p: 0.95,
|
|
410
|
+
stream: false,
|
|
411
|
+
temperature: 1,
|
|
412
|
+
stop_sequences: null
|
|
413
|
+
};
|
|
414
|
+
if (systemInstruction) {
|
|
415
|
+
body.system = systemInstruction;
|
|
416
|
+
}
|
|
417
|
+
const response = await axios.post(this.baseUrl, body, {
|
|
418
|
+
headers: {
|
|
419
|
+
"api-key": this.apiKey,
|
|
420
|
+
"Content-Type": "application/json"
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
const data = response.data;
|
|
424
|
+
const blocks = data?.content ?? [];
|
|
425
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
426
|
+
if (textBlock?.text) return textBlock.text;
|
|
427
|
+
if (data?.stop_reason === "max_tokens") {
|
|
428
|
+
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.`);
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
431
|
+
},
|
|
432
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
433
|
+
);
|
|
353
434
|
}
|
|
354
435
|
};
|
|
355
436
|
function createProvider(providerName, apiKey, modelName) {
|
|
@@ -3805,10 +3886,10 @@ ${preview}
|
|
|
3805
3886
|
|
|
3806
3887
|
// core/spec-refiner.ts
|
|
3807
3888
|
import { editor, confirm, select } from "@inquirer/prompts";
|
|
3808
|
-
import
|
|
3889
|
+
import chalk3 from "chalk";
|
|
3809
3890
|
|
|
3810
3891
|
// core/spec-versioning.ts
|
|
3811
|
-
import
|
|
3892
|
+
import chalk2 from "chalk";
|
|
3812
3893
|
import * as fs4 from "fs-extra";
|
|
3813
3894
|
function computeDiff(oldText, newText) {
|
|
3814
3895
|
const oldLines = oldText.split("\n");
|
|
@@ -3859,7 +3940,7 @@ function computeSimpleDiff(oldLines, newLines) {
|
|
|
3859
3940
|
var CONTEXT_LINES = 3;
|
|
3860
3941
|
function printDiff(diff) {
|
|
3861
3942
|
if (diff.added === 0 && diff.removed === 0) {
|
|
3862
|
-
console.log(
|
|
3943
|
+
console.log(chalk2.gray(" (no changes)"));
|
|
3863
3944
|
return;
|
|
3864
3945
|
}
|
|
3865
3946
|
const { lines } = diff;
|
|
@@ -3876,25 +3957,25 @@ function printDiff(diff) {
|
|
|
3876
3957
|
let prevIdx = -2;
|
|
3877
3958
|
for (const idx of sorted) {
|
|
3878
3959
|
if (idx > prevIdx + 1 && prevIdx !== -2) {
|
|
3879
|
-
console.log(
|
|
3960
|
+
console.log(chalk2.cyan(" @@"));
|
|
3880
3961
|
}
|
|
3881
3962
|
const l = lines[idx];
|
|
3882
3963
|
if (l.type === "added") {
|
|
3883
|
-
console.log(
|
|
3964
|
+
console.log(chalk2.green(` + ${l.content}`));
|
|
3884
3965
|
} else if (l.type === "removed") {
|
|
3885
|
-
console.log(
|
|
3966
|
+
console.log(chalk2.red(` - ${l.content}`));
|
|
3886
3967
|
} else {
|
|
3887
|
-
console.log(
|
|
3968
|
+
console.log(chalk2.gray(` ${l.content}`));
|
|
3888
3969
|
}
|
|
3889
3970
|
prevIdx = idx;
|
|
3890
3971
|
}
|
|
3891
3972
|
}
|
|
3892
3973
|
function printDiffSummary(diff, label) {
|
|
3893
3974
|
const parts = [];
|
|
3894
|
-
if (diff.added > 0) parts.push(
|
|
3895
|
-
if (diff.removed > 0) parts.push(
|
|
3896
|
-
if (parts.length === 0) parts.push(
|
|
3897
|
-
console.log(
|
|
3975
|
+
if (diff.added > 0) parts.push(chalk2.green(`+${diff.added}`));
|
|
3976
|
+
if (diff.removed > 0) parts.push(chalk2.red(`-${diff.removed}`));
|
|
3977
|
+
if (parts.length === 0) parts.push(chalk2.gray("no change"));
|
|
3978
|
+
console.log(chalk2.bold(` ${label}: `) + parts.join(" ") + chalk2.gray(` lines`));
|
|
3898
3979
|
}
|
|
3899
3980
|
|
|
3900
3981
|
// core/spec-refiner.ts
|
|
@@ -3906,16 +3987,16 @@ var SpecRefiner = class {
|
|
|
3906
3987
|
let currentSpec = initialSpec;
|
|
3907
3988
|
let round = 1;
|
|
3908
3989
|
while (true) {
|
|
3909
|
-
console.log(
|
|
3990
|
+
console.log(chalk3.cyan(`
|
|
3910
3991
|
\u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
3911
|
-
console.log(
|
|
3992
|
+
console.log(chalk3.gray(" Opening spec in editor. Save and close to continue."));
|
|
3912
3993
|
currentSpec = await editor({
|
|
3913
3994
|
message: "Review and edit the spec:",
|
|
3914
3995
|
default: currentSpec,
|
|
3915
3996
|
postfix: ".md",
|
|
3916
3997
|
waitForUserInput: false
|
|
3917
3998
|
});
|
|
3918
|
-
console.log(
|
|
3999
|
+
console.log(chalk3.green(" \u2714 Spec saved."));
|
|
3919
4000
|
const action = await select({
|
|
3920
4001
|
message: "What would you like to do?",
|
|
3921
4002
|
choices: [
|
|
@@ -3928,7 +4009,7 @@ var SpecRefiner = class {
|
|
|
3928
4009
|
break;
|
|
3929
4010
|
}
|
|
3930
4011
|
if (action === "ai") {
|
|
3931
|
-
console.log(
|
|
4012
|
+
console.log(chalk3.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
|
|
3932
4013
|
try {
|
|
3933
4014
|
const improved = await this.provider.generate(
|
|
3934
4015
|
`Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
|
|
@@ -3938,30 +4019,30 @@ Output ONLY the improved markdown spec, nothing else.
|
|
|
3938
4019
|
${currentSpec}`,
|
|
3939
4020
|
"You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
|
|
3940
4021
|
);
|
|
3941
|
-
console.log(
|
|
4022
|
+
console.log(chalk3.yellow("\n AI has suggested improvements. Opening diff in editor..."));
|
|
3942
4023
|
const acceptImproved = await confirm({
|
|
3943
4024
|
message: "Accept AI improvements? (opens editor so you can review first)",
|
|
3944
4025
|
default: true
|
|
3945
4026
|
});
|
|
3946
4027
|
if (acceptImproved) {
|
|
3947
4028
|
const diff = computeDiff(currentSpec, improved);
|
|
3948
|
-
console.log(
|
|
4029
|
+
console.log(chalk3.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"));
|
|
3949
4030
|
printDiffSummary(diff, "AI edits");
|
|
3950
4031
|
printDiff(diff);
|
|
3951
|
-
console.log(
|
|
4032
|
+
console.log(chalk3.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"));
|
|
3952
4033
|
currentSpec = await editor({
|
|
3953
4034
|
message: "Review AI-improved spec (edit if needed, then save):",
|
|
3954
4035
|
default: improved,
|
|
3955
4036
|
postfix: ".md",
|
|
3956
4037
|
waitForUserInput: false
|
|
3957
4038
|
});
|
|
3958
|
-
console.log(
|
|
4039
|
+
console.log(chalk3.green(" \u2714 AI-improved spec accepted."));
|
|
3959
4040
|
} else {
|
|
3960
|
-
console.log(
|
|
4041
|
+
console.log(chalk3.gray(" AI improvements discarded. Keeping your version."));
|
|
3961
4042
|
}
|
|
3962
4043
|
} catch (err) {
|
|
3963
|
-
console.error(
|
|
3964
|
-
console.log(
|
|
4044
|
+
console.error(chalk3.red(" AI improvement failed:"), err);
|
|
4045
|
+
console.log(chalk3.gray(" Continuing with current spec."));
|
|
3965
4046
|
}
|
|
3966
4047
|
}
|
|
3967
4048
|
round++;
|
|
@@ -3971,10 +4052,10 @@ ${currentSpec}`,
|
|
|
3971
4052
|
};
|
|
3972
4053
|
|
|
3973
4054
|
// core/code-generator.ts
|
|
3974
|
-
import
|
|
4055
|
+
import chalk8 from "chalk";
|
|
3975
4056
|
import { execSync } from "child_process";
|
|
3976
4057
|
import * as path6 from "path";
|
|
3977
|
-
import * as
|
|
4058
|
+
import * as fs10 from "fs-extra";
|
|
3978
4059
|
|
|
3979
4060
|
// prompts/codegen.prompt.ts
|
|
3980
4061
|
var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
|
|
@@ -4199,9 +4280,51 @@ Actionable, concrete improvements.
|
|
|
4199
4280
|
Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
|
|
4200
4281
|
|
|
4201
4282
|
Be specific. Reference actual code, not vague principles.`;
|
|
4283
|
+
var reviewImpactComplexitySystemPrompt = `You are a Senior Staff Engineer assessing the RISK and MAINTAINABILITY of a code change.
|
|
4284
|
+
|
|
4285
|
+
Two previous review passes have already covered architecture compliance and implementation correctness. Do NOT repeat their findings.
|
|
4286
|
+
|
|
4287
|
+
Your job is to answer exactly two questions:
|
|
4288
|
+
|
|
4289
|
+
---
|
|
4290
|
+
|
|
4291
|
+
## \u{1F30A} \u5F71\u54CD\u9762\u8BC4\u4F30 (Impact Assessment)
|
|
4292
|
+
|
|
4293
|
+
Evaluate what this change touches beyond its own files:
|
|
4294
|
+
|
|
4295
|
+
1. **\u76F4\u63A5\u5F71\u54CD\u6587\u4EF6** \u2014 List the files created or modified.
|
|
4296
|
+
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)
|
|
4297
|
+
3. **\u7834\u574F\u6027\u53D8\u66F4 (Breaking Changes)** \u2014 Are there changes requiring coordinated updates elsewhere?
|
|
4298
|
+
- API endpoint signature changes (path, method, required params)
|
|
4299
|
+
- Database schema changes (renamed columns, dropped fields, new NOT NULL constraints)
|
|
4300
|
+
- Config / env variable changes
|
|
4301
|
+
- Exported function/type renames
|
|
4302
|
+
- If none: state "\u65E0\u7834\u574F\u6027\u53D8\u66F4"
|
|
4303
|
+
4. **\u5F71\u54CD\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
|
|
4304
|
+
- \u4F4E: isolated to new files, no shared interface changes
|
|
4305
|
+
- \u4E2D: modifies shared utilities or existing public interfaces, backwards compatible
|
|
4306
|
+
- \u9AD8: breaking changes, or touches core auth / payment / data integrity paths
|
|
4307
|
+
|
|
4308
|
+
---
|
|
4309
|
+
|
|
4310
|
+
## \u{1F9EE} \u4EE3\u7801\u590D\u6742\u5EA6\u8BC4\u4F30 (Complexity Assessment)
|
|
4311
|
+
|
|
4312
|
+
Evaluate how hard the new code will be to understand and change in the future:
|
|
4313
|
+
|
|
4314
|
+
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).
|
|
4315
|
+
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?
|
|
4316
|
+
3. **\u53EF\u7EF4\u62A4\u6027\u98CE\u9669** \u2014 Flag patterns that will cause pain when requirements change:
|
|
4317
|
+
- Magic numbers / hardcoded strings that should be constants
|
|
4318
|
+
- Business logic buried in framework lifecycle hooks or constructors
|
|
4319
|
+
- Implicit temporal coupling (functions that must be called in a specific order)
|
|
4320
|
+
4. **\u590D\u6742\u5EA6\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
|
|
4321
|
+
|
|
4322
|
+
---
|
|
4323
|
+
|
|
4324
|
+
Format using ONLY these two sections. Be specific. Reference file names and line ranges where relevant.`;
|
|
4202
4325
|
|
|
4203
4326
|
// core/task-generator.ts
|
|
4204
|
-
import
|
|
4327
|
+
import chalk4 from "chalk";
|
|
4205
4328
|
import * as fs5 from "fs-extra";
|
|
4206
4329
|
import * as path3 from "path";
|
|
4207
4330
|
|
|
@@ -4355,31 +4478,36 @@ function parseTasks(raw) {
|
|
|
4355
4478
|
}
|
|
4356
4479
|
function printTasks(tasks) {
|
|
4357
4480
|
const layerColors = {
|
|
4358
|
-
data:
|
|
4359
|
-
infra:
|
|
4360
|
-
service:
|
|
4361
|
-
api:
|
|
4362
|
-
view:
|
|
4363
|
-
route:
|
|
4364
|
-
test:
|
|
4481
|
+
data: chalk4.magenta,
|
|
4482
|
+
infra: chalk4.gray,
|
|
4483
|
+
service: chalk4.blue,
|
|
4484
|
+
api: chalk4.cyan,
|
|
4485
|
+
view: chalk4.yellow,
|
|
4486
|
+
route: chalk4.white,
|
|
4487
|
+
test: chalk4.green
|
|
4365
4488
|
};
|
|
4366
|
-
console.log(
|
|
4489
|
+
console.log(chalk4.bold(`
|
|
4367
4490
|
Tasks (${tasks.length}):`));
|
|
4368
4491
|
for (const task of tasks) {
|
|
4369
|
-
const color = layerColors[task.layer] ??
|
|
4492
|
+
const color = layerColors[task.layer] ?? chalk4.white;
|
|
4370
4493
|
const badge = color(`[${task.layer}]`);
|
|
4371
|
-
const prio = task.priority === "high" ?
|
|
4372
|
-
console.log(` ${prio} ${
|
|
4494
|
+
const prio = task.priority === "high" ? chalk4.red("\u25CF") : task.priority === "medium" ? chalk4.yellow("\u25CF") : chalk4.gray("\u25CF");
|
|
4495
|
+
console.log(` ${prio} ${chalk4.bold(task.id)} ${badge} ${task.title}`);
|
|
4373
4496
|
}
|
|
4374
4497
|
}
|
|
4375
4498
|
async function loadTasksForSpec(specFilePath) {
|
|
4376
4499
|
const base = path3.basename(specFilePath, ".md");
|
|
4377
4500
|
const dir = path3.dirname(specFilePath);
|
|
4378
4501
|
const tasksFile = path3.join(dir, `${base}-tasks.json`);
|
|
4379
|
-
if (await fs5.pathExists(tasksFile))
|
|
4380
|
-
|
|
4502
|
+
if (!await fs5.pathExists(tasksFile)) return null;
|
|
4503
|
+
try {
|
|
4504
|
+
return await fs5.readJson(tasksFile);
|
|
4505
|
+
} catch {
|
|
4506
|
+
console.warn(
|
|
4507
|
+
chalk4.yellow(` \u26A0 Tasks file is corrupt or unreadable (${path3.basename(tasksFile)}).`) + chalk4.gray(` Re-run \`ai-spec tasks <spec>\` to regenerate.`)
|
|
4508
|
+
);
|
|
4509
|
+
return null;
|
|
4381
4510
|
}
|
|
4382
|
-
return null;
|
|
4383
4511
|
}
|
|
4384
4512
|
async function updateTaskStatus(specFilePath, taskId, status) {
|
|
4385
4513
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
@@ -4393,13 +4521,13 @@ async function updateTaskStatus(specFilePath, taskId, status) {
|
|
|
4393
4521
|
}
|
|
4394
4522
|
|
|
4395
4523
|
// core/dsl-extractor.ts
|
|
4396
|
-
import
|
|
4524
|
+
import chalk6 from "chalk";
|
|
4397
4525
|
import * as fs6 from "fs-extra";
|
|
4398
4526
|
import * as path4 from "path";
|
|
4399
4527
|
import { select as select2 } from "@inquirer/prompts";
|
|
4400
4528
|
|
|
4401
4529
|
// core/dsl-validator.ts
|
|
4402
|
-
import
|
|
4530
|
+
import chalk5 from "chalk";
|
|
4403
4531
|
var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
4404
4532
|
var MAX_MODELS = 50;
|
|
4405
4533
|
var MAX_FIELDS_PER_MODEL = 100;
|
|
@@ -4970,7 +5098,7 @@ ${preview}`);
|
|
|
4970
5098
|
} catch {
|
|
4971
5099
|
}
|
|
4972
5100
|
}
|
|
4973
|
-
const httpImportRegex = /^import\s+(
|
|
5101
|
+
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;
|
|
4974
5102
|
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
4975
5103
|
try {
|
|
4976
5104
|
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
@@ -4983,31 +5111,51 @@ ${preview}`);
|
|
|
4983
5111
|
}
|
|
4984
5112
|
}
|
|
4985
5113
|
const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
|
|
4986
|
-
const paginationInterfaceRegex = new RegExp(
|
|
4987
|
-
`(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
|
|
4988
|
-
"s"
|
|
4989
|
-
);
|
|
4990
|
-
const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
|
|
4991
5114
|
for (const relPath of ctx.existingApiFiles) {
|
|
4992
5115
|
if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
|
|
4993
5116
|
try {
|
|
4994
5117
|
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
4995
5118
|
if (!paginationFieldNames.some((f) => content.includes(f))) continue;
|
|
4996
|
-
const
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5119
|
+
const lines = content.split("\n");
|
|
5120
|
+
let interfaceName = "";
|
|
5121
|
+
let interfaceBlock = "";
|
|
5122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5123
|
+
const m = lines[i].match(/(?:interface|type)\s+(\w*(?:Params|Query|Request|Filter|Page)\w*)\s*[={<]/);
|
|
5124
|
+
if (!m) continue;
|
|
5125
|
+
const blockLines = [];
|
|
5126
|
+
let depth = 0;
|
|
5127
|
+
for (let j2 = i; j2 < Math.min(i + 40, lines.length); j2++) {
|
|
5128
|
+
blockLines.push(lines[j2]);
|
|
5129
|
+
depth += (lines[j2].match(/\{/g) ?? []).length;
|
|
5130
|
+
depth -= (lines[j2].match(/\}/g) ?? []).length;
|
|
5131
|
+
if (depth === 0 && j2 > i) break;
|
|
5132
|
+
}
|
|
5133
|
+
const blockText = blockLines.join("\n");
|
|
5134
|
+
if (!paginationFieldNames.some((f) => new RegExp(`\\b${f}\\b`).test(blockText))) continue;
|
|
5135
|
+
interfaceName = m[1];
|
|
5136
|
+
interfaceBlock = blockText;
|
|
5137
|
+
break;
|
|
5138
|
+
}
|
|
5139
|
+
if (!interfaceName) continue;
|
|
5140
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5141
|
+
const line = lines[i];
|
|
5142
|
+
if (!/export\s+(async\s+)?(function|const)/.test(line)) continue;
|
|
5143
|
+
if (!line.includes(interfaceName)) continue;
|
|
5144
|
+
const fnLines = [];
|
|
5145
|
+
let depth = 0;
|
|
5146
|
+
for (let j2 = i; j2 < Math.min(i + 30, lines.length); j2++) {
|
|
5147
|
+
fnLines.push(lines[j2]);
|
|
5148
|
+
depth += (lines[j2].match(/\{/g) ?? []).length;
|
|
5149
|
+
depth -= (lines[j2].match(/\}/g) ?? []).length;
|
|
5150
|
+
if (depth === 0 && j2 > i) break;
|
|
5151
|
+
}
|
|
5005
5152
|
ctx.paginationExample = `// From ${relPath}
|
|
5006
|
-
${
|
|
5153
|
+
${interfaceBlock}
|
|
5007
5154
|
|
|
5008
|
-
${
|
|
5155
|
+
${fnLines.join("\n")}`;
|
|
5009
5156
|
break;
|
|
5010
5157
|
}
|
|
5158
|
+
if (ctx.paginationExample) break;
|
|
5011
5159
|
} catch {
|
|
5012
5160
|
}
|
|
5013
5161
|
}
|
|
@@ -5220,6 +5368,21 @@ Shared component structure patterns:`);
|
|
|
5220
5368
|
return lines.join("\n");
|
|
5221
5369
|
}
|
|
5222
5370
|
|
|
5371
|
+
// core/run-snapshot.ts
|
|
5372
|
+
import * as fs8 from "fs-extra";
|
|
5373
|
+
var _activeSnapshot = null;
|
|
5374
|
+
function getActiveSnapshot() {
|
|
5375
|
+
return _activeSnapshot;
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
// core/run-logger.ts
|
|
5379
|
+
import * as fs9 from "fs-extra";
|
|
5380
|
+
import chalk7 from "chalk";
|
|
5381
|
+
var _activeLogger = null;
|
|
5382
|
+
function getActiveLogger() {
|
|
5383
|
+
return _activeLogger;
|
|
5384
|
+
}
|
|
5385
|
+
|
|
5223
5386
|
// core/code-generator.ts
|
|
5224
5387
|
function buildSharedConfigSection(context) {
|
|
5225
5388
|
if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
|
|
@@ -5386,13 +5549,13 @@ var CodeGenerator = class {
|
|
|
5386
5549
|
let effectiveMode = this.mode;
|
|
5387
5550
|
if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
|
|
5388
5551
|
console.log(
|
|
5389
|
-
|
|
5552
|
+
chalk8.yellow(
|
|
5390
5553
|
`
|
|
5391
5554
|
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
5392
5555
|
)
|
|
5393
5556
|
);
|
|
5394
|
-
console.log(
|
|
5395
|
-
console.log(
|
|
5557
|
+
console.log(chalk8.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
5558
|
+
console.log(chalk8.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
5396
5559
|
`));
|
|
5397
5560
|
effectiveMode = "api";
|
|
5398
5561
|
}
|
|
@@ -5417,16 +5580,16 @@ var CodeGenerator = class {
|
|
|
5417
5580
|
}
|
|
5418
5581
|
}
|
|
5419
5582
|
async runClaudeCode(specFilePath, workingDir, options = {}) {
|
|
5420
|
-
console.log(
|
|
5583
|
+
console.log(chalk8.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5421
5584
|
if (!this.isClaudeCLIAvailable()) {
|
|
5422
|
-
console.log(
|
|
5423
|
-
console.log(
|
|
5585
|
+
console.log(chalk8.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
|
|
5586
|
+
console.log(chalk8.gray(" Install: npm install -g @anthropic-ai/claude-code"));
|
|
5424
5587
|
return this.runPlanMode(specFilePath);
|
|
5425
5588
|
}
|
|
5426
5589
|
const rtkAvailable = isRtkAvailable();
|
|
5427
5590
|
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
5428
5591
|
if (rtkAvailable) {
|
|
5429
|
-
console.log(
|
|
5592
|
+
console.log(chalk8.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
5430
5593
|
}
|
|
5431
5594
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5432
5595
|
if (options.auto && tasks && tasks.length > 0) {
|
|
@@ -5440,29 +5603,29 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5440
5603
|
Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
|
|
5441
5604
|
const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
|
|
5442
5605
|
const promptFile = path6.join(workingDir, ".claude-prompt.txt");
|
|
5443
|
-
await
|
|
5606
|
+
await fs10.writeFile(promptFile, promptContent, "utf-8");
|
|
5444
5607
|
if (options.auto) {
|
|
5445
|
-
console.log(
|
|
5446
|
-
console.log(
|
|
5608
|
+
console.log(chalk8.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
5609
|
+
console.log(chalk8.gray(` Spec: ${specFilePath}`));
|
|
5447
5610
|
try {
|
|
5448
5611
|
execSync(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
|
|
5449
5612
|
cwd: workingDir,
|
|
5450
5613
|
stdio: "inherit"
|
|
5451
5614
|
});
|
|
5452
|
-
console.log(
|
|
5615
|
+
console.log(chalk8.green("\n \u2714 Claude Code completed."));
|
|
5453
5616
|
} catch {
|
|
5454
|
-
console.log(
|
|
5617
|
+
console.log(chalk8.yellow("\n Claude Code exited. Check output above."));
|
|
5455
5618
|
}
|
|
5456
5619
|
} else {
|
|
5457
|
-
console.log(
|
|
5458
|
-
console.log(
|
|
5459
|
-
if (tasks) console.log(
|
|
5460
|
-
console.log(
|
|
5620
|
+
console.log(chalk8.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
|
|
5621
|
+
console.log(chalk8.gray(` Spec: ${specFilePath}`));
|
|
5622
|
+
if (tasks) console.log(chalk8.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
|
|
5623
|
+
console.log(chalk8.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
|
|
5461
5624
|
try {
|
|
5462
5625
|
execSync(claudeCmd, { cwd: workingDir, stdio: "inherit" });
|
|
5463
|
-
console.log(
|
|
5626
|
+
console.log(chalk8.green("\n \u2714 Claude Code session completed."));
|
|
5464
5627
|
} catch {
|
|
5465
|
-
console.log(
|
|
5628
|
+
console.log(chalk8.yellow("\n Claude Code session ended. Continuing workflow."));
|
|
5466
5629
|
}
|
|
5467
5630
|
}
|
|
5468
5631
|
}
|
|
@@ -5475,10 +5638,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5475
5638
|
const pending = tasks.filter((t) => t.status !== "done");
|
|
5476
5639
|
const doneCount = tasks.length - pending.length;
|
|
5477
5640
|
if (options.resume && doneCount > 0) {
|
|
5478
|
-
console.log(
|
|
5641
|
+
console.log(chalk8.cyan(`
|
|
5479
5642
|
Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
|
|
5480
5643
|
} else {
|
|
5481
|
-
console.log(
|
|
5644
|
+
console.log(chalk8.cyan(`
|
|
5482
5645
|
Incremental mode: ${tasks.length} tasks`));
|
|
5483
5646
|
}
|
|
5484
5647
|
let completed = doneCount;
|
|
@@ -5506,32 +5669,32 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
5506
5669
|
completed++;
|
|
5507
5670
|
} catch {
|
|
5508
5671
|
taskStatus = "failed";
|
|
5509
|
-
console.log(
|
|
5672
|
+
console.log(chalk8.yellow(`
|
|
5510
5673
|
\u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
|
|
5511
5674
|
}
|
|
5512
5675
|
await updateTaskStatus(specFilePath, task.id, taskStatus);
|
|
5513
5676
|
}
|
|
5514
5677
|
const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
|
|
5515
5678
|
console.log(
|
|
5516
|
-
|
|
5679
|
+
chalk8.bold(
|
|
5517
5680
|
`
|
|
5518
|
-
${successCount === tasks.length ?
|
|
5681
|
+
${successCount === tasks.length ? chalk8.green("\u2714") : chalk8.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
|
|
5519
5682
|
)
|
|
5520
5683
|
);
|
|
5521
5684
|
}
|
|
5522
5685
|
// ── Mode: api ─────────────────────────────────────────────────────────────
|
|
5523
5686
|
async runApiMode(specFilePath, workingDir, context, options = {}) {
|
|
5524
5687
|
console.log(
|
|
5525
|
-
|
|
5688
|
+
chalk8.blue(
|
|
5526
5689
|
`
|
|
5527
5690
|
\u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
|
|
5528
5691
|
)
|
|
5529
5692
|
);
|
|
5530
5693
|
const systemPrompt = getCodeGenSystemPrompt(options.repoType);
|
|
5531
5694
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
5532
|
-
console.log(
|
|
5695
|
+
console.log(chalk8.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
5533
5696
|
}
|
|
5534
|
-
const spec = await
|
|
5697
|
+
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
5535
5698
|
const constitutionSection = context?.constitution ? `
|
|
5536
5699
|
=== Project Constitution (MUST follow) ===
|
|
5537
5700
|
${context.constitution}
|
|
@@ -5547,7 +5710,7 @@ ${buildDslContextSection(dsl)}
|
|
|
5547
5710
|
if (dsl) {
|
|
5548
5711
|
const cmpCount = dsl.components?.length ?? 0;
|
|
5549
5712
|
const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
|
|
5550
|
-
console.log(
|
|
5713
|
+
console.log(chalk8.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
|
|
5551
5714
|
}
|
|
5552
5715
|
const isFrontend = isFrontendDeps(context?.dependencies ?? []);
|
|
5553
5716
|
let frontendSection = "";
|
|
@@ -5556,13 +5719,13 @@ ${buildDslContextSection(dsl)}
|
|
|
5556
5719
|
frontendSection = `
|
|
5557
5720
|
${buildFrontendContextSection(fctx)}
|
|
5558
5721
|
`;
|
|
5559
|
-
console.log(
|
|
5722
|
+
console.log(chalk8.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
5560
5723
|
}
|
|
5561
5724
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5562
5725
|
if (tasks && tasks.length > 0) {
|
|
5563
5726
|
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
5564
5727
|
}
|
|
5565
|
-
console.log(
|
|
5728
|
+
console.log(chalk8.gray(" [1/2] Planning implementation files..."));
|
|
5566
5729
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
5567
5730
|
|
|
5568
5731
|
IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
|
|
@@ -5585,18 +5748,18 @@ Output ONLY a valid JSON array:
|
|
|
5585
5748
|
const planResponse = await this.provider.generate(planPrompt, systemPrompt);
|
|
5586
5749
|
filePlan = parseJsonArray(planResponse);
|
|
5587
5750
|
} catch (err) {
|
|
5588
|
-
console.error(
|
|
5751
|
+
console.error(chalk8.red(" Failed to generate file plan:"), err);
|
|
5589
5752
|
}
|
|
5590
5753
|
if (filePlan.length === 0) {
|
|
5591
|
-
console.log(
|
|
5754
|
+
console.log(chalk8.yellow(" Could not determine file plan. Falling back to plan mode."));
|
|
5592
5755
|
await this.runPlanMode(specFilePath);
|
|
5593
5756
|
return [];
|
|
5594
5757
|
}
|
|
5595
|
-
console.log(
|
|
5758
|
+
console.log(chalk8.cyan(`
|
|
5596
5759
|
Plan: ${filePlan.length} file(s) to process`));
|
|
5597
5760
|
filePlan.forEach((item) => {
|
|
5598
|
-
const icon = item.action === "create" ?
|
|
5599
|
-
console.log(` ${icon} ${item.file}: ${
|
|
5761
|
+
const icon = item.action === "create" ? chalk8.green("+") : chalk8.yellow("~");
|
|
5762
|
+
console.log(` ${icon} ${item.file}: ${chalk8.gray(item.description)}`);
|
|
5600
5763
|
});
|
|
5601
5764
|
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
5602
5765
|
return files;
|
|
@@ -5605,13 +5768,13 @@ Output ONLY a valid JSON array:
|
|
|
5605
5768
|
const pendingTasks = tasks.filter((t) => t.status !== "done");
|
|
5606
5769
|
const doneCount = tasks.length - pendingTasks.length;
|
|
5607
5770
|
if (options.resume && doneCount > 0) {
|
|
5608
|
-
console.log(
|
|
5609
|
-
Task-based generation (resume): ${tasks.length} tasks (${
|
|
5771
|
+
console.log(chalk8.cyan(`
|
|
5772
|
+
Task-based generation (resume): ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, skipping)`));
|
|
5610
5773
|
} else if (doneCount > 0) {
|
|
5611
|
-
console.log(
|
|
5612
|
-
Task-based generation: ${tasks.length} tasks (${
|
|
5774
|
+
console.log(chalk8.cyan(`
|
|
5775
|
+
Task-based generation: ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, resuming from checkpoint)`));
|
|
5613
5776
|
} else {
|
|
5614
|
-
console.log(
|
|
5777
|
+
console.log(chalk8.cyan(`
|
|
5615
5778
|
Task-based generation: ${tasks.length} tasks`));
|
|
5616
5779
|
}
|
|
5617
5780
|
const sharedConfigPaths = new Set(
|
|
@@ -5643,9 +5806,9 @@ Output ONLY a valid JSON array:
|
|
|
5643
5806
|
const pct = Math.round(completedTasks / tasks.length * 100);
|
|
5644
5807
|
const barWidth = 20;
|
|
5645
5808
|
const filled = Math.round(pct / 100 * barWidth);
|
|
5646
|
-
const bar =
|
|
5809
|
+
const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
|
|
5647
5810
|
console.log(
|
|
5648
|
-
|
|
5811
|
+
chalk8.bold(`
|
|
5649
5812
|
[${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
|
|
5650
5813
|
);
|
|
5651
5814
|
} else {
|
|
@@ -5653,12 +5816,12 @@ Output ONLY a valid JSON array:
|
|
|
5653
5816
|
}
|
|
5654
5817
|
const executeTask = async (task, batchIsParallel) => {
|
|
5655
5818
|
if (task.filesToTouch.length === 0) {
|
|
5656
|
-
if (!batchIsParallel) console.log(
|
|
5819
|
+
if (!batchIsParallel) console.log(chalk8.gray(" No files specified, skipping."));
|
|
5657
5820
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
5658
5821
|
}
|
|
5659
5822
|
const filePlan = await Promise.all(
|
|
5660
5823
|
task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
|
|
5661
|
-
const exists = await
|
|
5824
|
+
const exists = await fs10.pathExists(path6.join(workingDir, f));
|
|
5662
5825
|
return {
|
|
5663
5826
|
file: f,
|
|
5664
5827
|
action: exists ? "modify" : "create",
|
|
@@ -5698,7 +5861,7 @@ ${taskContext}`,
|
|
|
5698
5861
|
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
|
|
5699
5862
|
if (isCodeFile || isViewFile) {
|
|
5700
5863
|
try {
|
|
5701
|
-
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await
|
|
5864
|
+
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path6.join(workingDir, writtenFile), "utf-8");
|
|
5702
5865
|
generatedFileCache.set(writtenFile, content);
|
|
5703
5866
|
} catch {
|
|
5704
5867
|
}
|
|
@@ -5710,7 +5873,12 @@ ${taskContext}`,
|
|
|
5710
5873
|
const layerResults = [];
|
|
5711
5874
|
for (const batch of taskBatches) {
|
|
5712
5875
|
const batchIsParallel = batch.length > 1;
|
|
5713
|
-
const batchResultPromises = batch.map(
|
|
5876
|
+
const batchResultPromises = batch.map(
|
|
5877
|
+
(task) => executeTask(task, batchIsParallel).catch((err) => {
|
|
5878
|
+
console.log(chalk8.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
|
|
5879
|
+
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
5880
|
+
})
|
|
5881
|
+
);
|
|
5714
5882
|
const batchResults = await Promise.all(batchResultPromises);
|
|
5715
5883
|
layerResults.push(...batchResults);
|
|
5716
5884
|
await updateCacheFromBatch(batchResults);
|
|
@@ -5723,14 +5891,14 @@ ${taskContext}`,
|
|
|
5723
5891
|
totalFiles += result.total;
|
|
5724
5892
|
allGeneratedFiles.push(...result.files);
|
|
5725
5893
|
if (isParallel) {
|
|
5726
|
-
const icon = result.success === result.total ?
|
|
5894
|
+
const icon = result.success === result.total ? chalk8.green("\u2714") : chalk8.yellow("!");
|
|
5727
5895
|
const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
|
|
5728
5896
|
console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
|
|
5729
5897
|
}
|
|
5730
5898
|
const taskStatus = result.success === result.total ? "done" : "failed";
|
|
5731
5899
|
await updateTaskStatus(specFilePath, result.task.id, taskStatus);
|
|
5732
5900
|
if (taskStatus === "failed") {
|
|
5733
|
-
console.log(
|
|
5901
|
+
console.log(chalk8.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
|
|
5734
5902
|
}
|
|
5735
5903
|
}
|
|
5736
5904
|
completedTasks += layerTasks.length;
|
|
@@ -5745,7 +5913,7 @@ ${taskContext}`,
|
|
|
5745
5913
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
5746
5914
|
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.`;
|
|
5747
5915
|
}
|
|
5748
|
-
console.log(
|
|
5916
|
+
console.log(chalk8.gray(`
|
|
5749
5917
|
+ updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
|
|
5750
5918
|
const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
5751
5919
|
await this.generateFiles(
|
|
@@ -5763,17 +5931,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
5763
5931
|
}
|
|
5764
5932
|
}
|
|
5765
5933
|
console.log(
|
|
5766
|
-
|
|
5934
|
+
chalk8.bold(
|
|
5767
5935
|
`
|
|
5768
|
-
${totalSuccess === totalFiles ?
|
|
5936
|
+
${totalSuccess === totalFiles ? chalk8.green("\u2714") : chalk8.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
|
|
5769
5937
|
)
|
|
5770
5938
|
);
|
|
5771
5939
|
return allGeneratedFiles;
|
|
5772
5940
|
}
|
|
5773
5941
|
async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
|
|
5774
|
-
const prefix = taskLabel ? ` [${
|
|
5942
|
+
const prefix = taskLabel ? ` [${chalk8.cyan(taskLabel)}] ` : " ";
|
|
5775
5943
|
if (!taskLabel) {
|
|
5776
|
-
console.log(
|
|
5944
|
+
console.log(chalk8.gray(`
|
|
5777
5945
|
Generating ${filePlan.length} file(s)...`));
|
|
5778
5946
|
}
|
|
5779
5947
|
let successCount = 0;
|
|
@@ -5781,8 +5949,8 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
5781
5949
|
for (const item of filePlan) {
|
|
5782
5950
|
const fullPath = path6.join(workingDir, item.file);
|
|
5783
5951
|
let existingContent = "";
|
|
5784
|
-
if (await
|
|
5785
|
-
existingContent = await
|
|
5952
|
+
if (await fs10.pathExists(fullPath)) {
|
|
5953
|
+
existingContent = await fs10.readFile(fullPath, "utf-8");
|
|
5786
5954
|
}
|
|
5787
5955
|
const codePrompt = `Implement this file.
|
|
5788
5956
|
|
|
@@ -5797,19 +5965,21 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
5797
5965
|
try {
|
|
5798
5966
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
5799
5967
|
const fileContent = stripCodeFences(raw);
|
|
5800
|
-
await
|
|
5801
|
-
await
|
|
5802
|
-
|
|
5968
|
+
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
5969
|
+
await fs10.ensureDir(path6.dirname(fullPath));
|
|
5970
|
+
await fs10.writeFile(fullPath, fileContent, "utf-8");
|
|
5971
|
+
getActiveLogger()?.fileWritten(item.file);
|
|
5972
|
+
console.log(`${prefix}${existingContent ? chalk8.yellow("~") : chalk8.green("+")} ${chalk8.bold(item.file)} ${chalk8.green("\u2714")}`);
|
|
5803
5973
|
successCount++;
|
|
5804
5974
|
writtenFiles.push(item.file);
|
|
5805
5975
|
} catch (err) {
|
|
5806
|
-
console.log(`${prefix}${
|
|
5976
|
+
console.log(`${prefix}${chalk8.red("\u2718")} ${chalk8.bold(item.file)} \u2014 ${chalk8.red(err.message)}`);
|
|
5807
5977
|
}
|
|
5808
5978
|
}
|
|
5809
5979
|
if (!taskLabel) {
|
|
5810
5980
|
console.log(
|
|
5811
|
-
|
|
5812
|
-
` ${successCount === filePlan.length ?
|
|
5981
|
+
chalk8.bold(
|
|
5982
|
+
` ${successCount === filePlan.length ? chalk8.green("\u2714") : chalk8.yellow("!")} ${successCount}/${filePlan.length} files written.`
|
|
5813
5983
|
)
|
|
5814
5984
|
);
|
|
5815
5985
|
}
|
|
@@ -5817,8 +5987,8 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
5817
5987
|
}
|
|
5818
5988
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
5819
5989
|
async runPlanMode(specFilePath) {
|
|
5820
|
-
console.log(
|
|
5821
|
-
const spec = await
|
|
5990
|
+
console.log(chalk8.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"));
|
|
5991
|
+
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
5822
5992
|
const plan = await this.provider.generate(
|
|
5823
5993
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
5824
5994
|
Be specific about:
|
|
@@ -5830,7 +6000,7 @@ Be specific about:
|
|
|
5830
6000
|
${spec}`,
|
|
5831
6001
|
"You are a senior developer creating an actionable implementation guide."
|
|
5832
6002
|
);
|
|
5833
|
-
console.log(
|
|
6003
|
+
console.log(chalk8.cyan("\n") + plan);
|
|
5834
6004
|
}
|
|
5835
6005
|
};
|
|
5836
6006
|
function topoSortLayerTasks(tasks) {
|
|
@@ -5881,32 +6051,32 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
5881
6051
|
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
5882
6052
|
const barWidth = 20;
|
|
5883
6053
|
const filled = Math.round(pct / 100 * barWidth);
|
|
5884
|
-
const bar =
|
|
6054
|
+
const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
|
|
5885
6055
|
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
5886
6056
|
if (mode === "skip") {
|
|
5887
6057
|
console.log(
|
|
5888
|
-
|
|
6058
|
+
chalk8.gray(`
|
|
5889
6059
|
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
5890
6060
|
);
|
|
5891
6061
|
} else {
|
|
5892
6062
|
console.log(
|
|
5893
|
-
|
|
6063
|
+
chalk8.bold(`
|
|
5894
6064
|
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
5895
6065
|
);
|
|
5896
6066
|
}
|
|
5897
6067
|
}
|
|
5898
6068
|
|
|
5899
6069
|
// core/reviewer.ts
|
|
5900
|
-
import
|
|
6070
|
+
import chalk9 from "chalk";
|
|
5901
6071
|
import { execSync as execSync2 } from "child_process";
|
|
5902
6072
|
import * as path7 from "path";
|
|
5903
|
-
import * as
|
|
6073
|
+
import * as fs11 from "fs-extra";
|
|
5904
6074
|
var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5905
6075
|
async function loadReviewHistory(projectRoot) {
|
|
5906
6076
|
const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
5907
6077
|
try {
|
|
5908
|
-
if (await
|
|
5909
|
-
return await
|
|
6078
|
+
if (await fs11.pathExists(historyPath)) {
|
|
6079
|
+
return await fs11.readJson(historyPath);
|
|
5910
6080
|
}
|
|
5911
6081
|
} catch {
|
|
5912
6082
|
}
|
|
@@ -5917,7 +6087,7 @@ async function appendReviewHistory(projectRoot, entry) {
|
|
|
5917
6087
|
const existing = await loadReviewHistory(projectRoot);
|
|
5918
6088
|
const updated = [...existing, entry].slice(-20);
|
|
5919
6089
|
try {
|
|
5920
|
-
await
|
|
6090
|
+
await fs11.writeJson(historyPath, updated, { spaces: 2 });
|
|
5921
6091
|
} catch {
|
|
5922
6092
|
}
|
|
5923
6093
|
}
|
|
@@ -5925,6 +6095,14 @@ function extractScore(reviewText) {
|
|
|
5925
6095
|
const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
5926
6096
|
return match ? parseFloat(match[1]) : 0;
|
|
5927
6097
|
}
|
|
6098
|
+
function extractImpactLevel(reviewText) {
|
|
6099
|
+
const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
|
|
6100
|
+
return match ? match[1] : void 0;
|
|
6101
|
+
}
|
|
6102
|
+
function extractComplexityLevel(reviewText) {
|
|
6103
|
+
const match = reviewText.match(/复杂度等级[::]\s*(低|中|高)/);
|
|
6104
|
+
return match ? match[1] : void 0;
|
|
6105
|
+
}
|
|
5928
6106
|
function extractTopIssues(reviewText) {
|
|
5929
6107
|
const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
|
|
5930
6108
|
return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
|
|
@@ -5970,15 +6148,14 @@ var CodeReviewer = class {
|
|
|
5970
6148
|
};
|
|
5971
6149
|
}
|
|
5972
6150
|
/**
|
|
5973
|
-
*
|
|
6151
|
+
* Three-pass review:
|
|
5974
6152
|
* Pass 1 — architecture (spec compliance, layer separation, auth)
|
|
5975
6153
|
* Pass 2 — implementation details (validation, error handling, edge cases)
|
|
5976
6154
|
* + historical issue recurrence check
|
|
5977
|
-
*
|
|
5978
|
-
* Falls back to single-pass if the two-pass flag is not set.
|
|
6155
|
+
* Pass 3 — impact assessment + code complexity
|
|
5979
6156
|
*/
|
|
5980
|
-
async
|
|
5981
|
-
console.log(
|
|
6157
|
+
async runThreePassReview(specContent, codeContext, specFile) {
|
|
6158
|
+
console.log(chalk9.gray(" Pass 1/3: Architecture review..."));
|
|
5982
6159
|
const archPrompt = `Review the architecture of this change.
|
|
5983
6160
|
|
|
5984
6161
|
=== Feature Spec ===
|
|
@@ -5987,7 +6164,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
5987
6164
|
=== Code ===
|
|
5988
6165
|
${codeContext}`;
|
|
5989
6166
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
5990
|
-
console.log(
|
|
6167
|
+
console.log(chalk9.gray(" Pass 2/3: Implementation review..."));
|
|
5991
6168
|
const history = await loadReviewHistory(this.projectRoot);
|
|
5992
6169
|
const historyContext = buildHistoryContext(history);
|
|
5993
6170
|
const implPrompt = `Review the implementation details of this change.
|
|
@@ -6002,61 +6179,85 @@ ${codeContext}
|
|
|
6002
6179
|
${archReview}
|
|
6003
6180
|
${historyContext}`;
|
|
6004
6181
|
const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
|
|
6005
|
-
|
|
6182
|
+
console.log(chalk9.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6183
|
+
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6006
6184
|
|
|
6007
|
-
|
|
6185
|
+
=== Feature Spec ===
|
|
6186
|
+
${specContent || "(No spec \u2014 review for general code quality)"}
|
|
6008
6187
|
|
|
6188
|
+
=== Code ===
|
|
6189
|
+
${codeContext}
|
|
6190
|
+
|
|
6191
|
+
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
6192
|
+
${archReview}
|
|
6193
|
+
|
|
6194
|
+
=== Implementation Review (Pass 2 \u2014 do NOT repeat) ===
|
|
6009
6195
|
${implReview}`;
|
|
6196
|
+
const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
|
|
6197
|
+
const sep = "\u2500".repeat(52);
|
|
6198
|
+
const combined = `${archReview}
|
|
6199
|
+
|
|
6200
|
+
${sep}
|
|
6201
|
+
|
|
6202
|
+
${implReview}
|
|
6203
|
+
|
|
6204
|
+
${sep}
|
|
6205
|
+
|
|
6206
|
+
${impactReview}`;
|
|
6010
6207
|
const score = extractScore(implReview) || extractScore(archReview);
|
|
6011
6208
|
const topIssues = extractTopIssues(implReview);
|
|
6209
|
+
const impactLevel = extractImpactLevel(impactReview);
|
|
6210
|
+
const complexityLevel = extractComplexityLevel(impactReview);
|
|
6012
6211
|
if (score > 0 && specFile) {
|
|
6013
6212
|
await appendReviewHistory(this.projectRoot, {
|
|
6014
6213
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
6015
6214
|
specFile: path7.relative(this.projectRoot, specFile),
|
|
6016
6215
|
score,
|
|
6017
|
-
topIssues
|
|
6216
|
+
topIssues,
|
|
6217
|
+
...impactLevel ? { impactLevel } : {},
|
|
6218
|
+
...complexityLevel ? { complexityLevel } : {}
|
|
6018
6219
|
});
|
|
6019
6220
|
}
|
|
6020
6221
|
return combined;
|
|
6021
6222
|
}
|
|
6022
6223
|
async reviewCode(specContent, specFile) {
|
|
6023
|
-
console.log(
|
|
6224
|
+
console.log(chalk9.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"));
|
|
6024
6225
|
const diff = this.getGitDiff();
|
|
6025
6226
|
if (!diff.trim()) {
|
|
6026
6227
|
console.log(
|
|
6027
|
-
|
|
6228
|
+
chalk9.yellow(" No git diff found. Stage or commit changes first, then run review.")
|
|
6028
6229
|
);
|
|
6029
|
-
console.log(
|
|
6230
|
+
console.log(chalk9.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
|
|
6030
6231
|
return "No changes";
|
|
6031
6232
|
}
|
|
6032
6233
|
const { files, added, removed } = this.getDiffStats(diff);
|
|
6033
6234
|
console.log(
|
|
6034
|
-
|
|
6235
|
+
chalk9.gray(` Diff: ${files} file(s), ${chalk9.green("+" + added)} ${chalk9.red("-" + removed)}`)
|
|
6035
6236
|
);
|
|
6036
6237
|
console.log(
|
|
6037
|
-
|
|
6238
|
+
chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6038
6239
|
);
|
|
6039
6240
|
const codeContext = diff.slice(0, 1e4);
|
|
6040
|
-
const reviewResult = await this.
|
|
6041
|
-
console.log(
|
|
6241
|
+
const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
|
|
6242
|
+
console.log(chalk9.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"));
|
|
6042
6243
|
console.log(reviewResult);
|
|
6043
|
-
console.log(
|
|
6244
|
+
console.log(chalk9.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"));
|
|
6044
6245
|
return reviewResult;
|
|
6045
6246
|
}
|
|
6046
6247
|
/**
|
|
6047
6248
|
* Review directly from generated file contents (for api mode where git diff is empty).
|
|
6048
6249
|
*/
|
|
6049
6250
|
async reviewFiles(specContent, filePaths, workingDir, specFile) {
|
|
6050
|
-
console.log(
|
|
6051
|
-
console.log(
|
|
6251
|
+
console.log(chalk9.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6252
|
+
console.log(chalk9.gray(` Reviewing ${filePaths.length} generated file(s)...`));
|
|
6052
6253
|
console.log(
|
|
6053
|
-
|
|
6254
|
+
chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6054
6255
|
);
|
|
6055
6256
|
let filesSection = "";
|
|
6056
6257
|
for (const filePath of filePaths) {
|
|
6057
6258
|
const fullPath = path7.join(workingDir, filePath);
|
|
6058
6259
|
try {
|
|
6059
|
-
const content = await
|
|
6260
|
+
const content = await fs11.readFile(fullPath, "utf-8");
|
|
6060
6261
|
filesSection += `
|
|
6061
6262
|
|
|
6062
6263
|
=== ${filePath} ===
|
|
@@ -6070,33 +6271,33 @@ ${content.slice(0, 3e3)}`;
|
|
|
6070
6271
|
(file not found)`;
|
|
6071
6272
|
}
|
|
6072
6273
|
}
|
|
6073
|
-
const reviewResult = await this.
|
|
6074
|
-
console.log(
|
|
6274
|
+
const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
|
|
6275
|
+
console.log(chalk9.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"));
|
|
6075
6276
|
console.log(reviewResult);
|
|
6076
|
-
console.log(
|
|
6277
|
+
console.log(chalk9.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"));
|
|
6077
6278
|
return reviewResult;
|
|
6078
6279
|
}
|
|
6079
6280
|
/** Print score trend from history (last N reviews) */
|
|
6080
6281
|
async printScoreTrend(limit = 5) {
|
|
6081
6282
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6082
6283
|
if (history.length === 0) {
|
|
6083
|
-
console.log(
|
|
6284
|
+
console.log(chalk9.gray(" No review history yet."));
|
|
6084
6285
|
return;
|
|
6085
6286
|
}
|
|
6086
6287
|
const recent = history.slice(-limit);
|
|
6087
|
-
console.log(
|
|
6288
|
+
console.log(chalk9.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"));
|
|
6088
6289
|
for (const entry of recent) {
|
|
6089
6290
|
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
6090
|
-
const color = entry.score >= 8 ?
|
|
6291
|
+
const color = entry.score >= 8 ? chalk9.green : entry.score >= 6 ? chalk9.yellow : chalk9.red;
|
|
6091
6292
|
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path7.basename(entry.specFile)}`);
|
|
6092
6293
|
}
|
|
6093
|
-
console.log(
|
|
6294
|
+
console.log(chalk9.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"));
|
|
6094
6295
|
}
|
|
6095
6296
|
};
|
|
6096
6297
|
|
|
6097
6298
|
// core/constitution-generator.ts
|
|
6098
|
-
import
|
|
6099
|
-
import * as
|
|
6299
|
+
import chalk10 from "chalk";
|
|
6300
|
+
import * as fs12 from "fs-extra";
|
|
6100
6301
|
import * as path8 from "path";
|
|
6101
6302
|
|
|
6102
6303
|
// prompts/constitution.prompt.ts
|
|
@@ -6178,7 +6379,7 @@ var ConstitutionGenerator = class {
|
|
|
6178
6379
|
}
|
|
6179
6380
|
async saveConstitution(projectRoot, content) {
|
|
6180
6381
|
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6181
|
-
await
|
|
6382
|
+
await fs12.writeFile(filePath, content, "utf-8");
|
|
6182
6383
|
return filePath;
|
|
6183
6384
|
}
|
|
6184
6385
|
};
|
|
@@ -6237,15 +6438,15 @@ ${sections.join("\n")}
|
|
|
6237
6438
|
}
|
|
6238
6439
|
async function loadConstitution(projectRoot) {
|
|
6239
6440
|
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6240
|
-
if (await
|
|
6241
|
-
return
|
|
6441
|
+
if (await fs12.pathExists(filePath)) {
|
|
6442
|
+
return fs12.readFile(filePath, "utf-8");
|
|
6242
6443
|
}
|
|
6243
6444
|
return void 0;
|
|
6244
6445
|
}
|
|
6245
6446
|
function printConstitutionHint(exists) {
|
|
6246
6447
|
if (!exists) {
|
|
6247
6448
|
console.log(
|
|
6248
|
-
|
|
6449
|
+
chalk10.yellow(
|
|
6249
6450
|
" \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
|
|
6250
6451
|
)
|
|
6251
6452
|
);
|
|
@@ -6290,8 +6491,8 @@ function parseSpecAndTasks(raw) {
|
|
|
6290
6491
|
// git/worktree.ts
|
|
6291
6492
|
import { execSync as execSync3 } from "child_process";
|
|
6292
6493
|
import * as path9 from "path";
|
|
6293
|
-
import * as
|
|
6294
|
-
import
|
|
6494
|
+
import * as fs13 from "fs-extra";
|
|
6495
|
+
import chalk11 from "chalk";
|
|
6295
6496
|
var GitWorktreeManager = class {
|
|
6296
6497
|
constructor(baseDir) {
|
|
6297
6498
|
this.baseDir = baseDir;
|
|
@@ -6318,30 +6519,30 @@ var GitWorktreeManager = class {
|
|
|
6318
6519
|
for (const dir of candidates) {
|
|
6319
6520
|
const src = path9.join(this.baseDir, dir);
|
|
6320
6521
|
const dest = path9.join(worktreePath, dir);
|
|
6321
|
-
if (!await
|
|
6322
|
-
if (await
|
|
6522
|
+
if (!await fs13.pathExists(src)) continue;
|
|
6523
|
+
if (await fs13.pathExists(dest)) continue;
|
|
6323
6524
|
try {
|
|
6324
|
-
await
|
|
6325
|
-
console.log(
|
|
6525
|
+
await fs13.ensureSymlink(src, dest, "dir");
|
|
6526
|
+
console.log(chalk11.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
6326
6527
|
} catch (err) {
|
|
6327
|
-
console.log(
|
|
6328
|
-
console.log(
|
|
6528
|
+
console.log(chalk11.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
6529
|
+
console.log(chalk11.yellow(` Run \`npm install\` inside the worktree manually.`));
|
|
6329
6530
|
}
|
|
6330
6531
|
}
|
|
6331
6532
|
}
|
|
6332
6533
|
async createWorktree(idea) {
|
|
6333
6534
|
if (!this.isGitRepo()) {
|
|
6334
|
-
console.log(
|
|
6535
|
+
console.log(chalk11.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
|
|
6335
6536
|
return null;
|
|
6336
6537
|
}
|
|
6337
6538
|
const featureName = this.sanitizeFeatureName(idea);
|
|
6338
6539
|
const branchName = `feature/${featureName}`;
|
|
6339
6540
|
const repoName = path9.basename(this.baseDir);
|
|
6340
6541
|
const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
6341
|
-
console.log(
|
|
6542
|
+
console.log(chalk11.cyan(`
|
|
6342
6543
|
--- Setting up Git Worktree ---`));
|
|
6343
|
-
if (await
|
|
6344
|
-
console.log(
|
|
6544
|
+
if (await fs13.pathExists(worktreePath)) {
|
|
6545
|
+
console.log(chalk11.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
6345
6546
|
await this.linkDependencies(worktreePath);
|
|
6346
6547
|
return worktreePath;
|
|
6347
6548
|
}
|
|
@@ -6355,7 +6556,7 @@ var GitWorktreeManager = class {
|
|
|
6355
6556
|
branchExists = true;
|
|
6356
6557
|
} catch {
|
|
6357
6558
|
}
|
|
6358
|
-
console.log(
|
|
6559
|
+
console.log(chalk11.gray(`Creating worktree at: ${worktreePath}`));
|
|
6359
6560
|
if (branchExists) {
|
|
6360
6561
|
execSync3(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6361
6562
|
cwd: this.baseDir,
|
|
@@ -6368,12 +6569,12 @@ var GitWorktreeManager = class {
|
|
|
6368
6569
|
});
|
|
6369
6570
|
}
|
|
6370
6571
|
console.log(
|
|
6371
|
-
|
|
6572
|
+
chalk11.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
|
|
6372
6573
|
);
|
|
6373
6574
|
await this.linkDependencies(worktreePath);
|
|
6374
6575
|
return worktreePath;
|
|
6375
6576
|
} catch (error) {
|
|
6376
|
-
console.error(
|
|
6577
|
+
console.error(chalk11.red("Failed to create git worktree:"), error);
|
|
6377
6578
|
return null;
|
|
6378
6579
|
}
|
|
6379
6580
|
}
|