ai-spec-dev 0.37.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +381 -1796
- package/RELEASE_LOG.md +231 -0
- package/cli/commands/create.ts +9 -1176
- package/cli/commands/dashboard.ts +1 -1
- package/cli/pipeline/helpers.ts +34 -0
- package/cli/pipeline/multi-repo.ts +483 -0
- package/cli/pipeline/single-repo.ts +755 -0
- package/cli/utils.ts +2 -0
- package/core/code-generator.ts +52 -341
- package/core/codegen/helpers.ts +219 -0
- package/core/codegen/topo-sort.ts +98 -0
- package/core/constitution-consolidator.ts +2 -2
- package/core/dsl-coverage-checker.ts +298 -0
- package/core/dsl-extractor.ts +19 -46
- package/core/dsl-feedback.ts +1 -1
- package/core/dsl-validator.ts +74 -0
- package/core/error-feedback.ts +95 -11
- package/core/frontend-context-loader.ts +27 -5
- package/core/knowledge-memory.ts +52 -0
- package/core/mock/fixtures.ts +89 -0
- package/core/mock/proxy.ts +380 -0
- package/core/mock-server-generator.ts +12 -460
- package/core/requirement-decomposer.ts +4 -28
- package/core/reviewer.ts +1 -1
- package/core/safe-json.ts +76 -0
- package/core/spec-updater.ts +5 -21
- package/core/token-budget.ts +124 -0
- package/core/vcr.ts +20 -1
- package/dist/cli/index.js +4110 -3534
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +4237 -3661
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +18 -16
- package/dist/index.d.ts +18 -16
- package/dist/index.js +310 -182
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +308 -180
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/purpose.md +173 -33
- package/tests/auto-consolidation.test.ts +109 -0
- package/tests/combined-generator.test.ts +81 -0
- package/tests/constitution-consolidator.test.ts +161 -0
- package/tests/constitution-generator.test.ts +94 -0
- package/tests/contract-bridge.test.ts +201 -0
- package/tests/design-dialogue.test.ts +108 -0
- package/tests/dsl-coverage-checker.test.ts +230 -0
- package/tests/dsl-feedback.test.ts +45 -0
- package/tests/dsl-validator-xref.test.ts +99 -0
- package/tests/error-feedback-repair.test.ts +319 -0
- package/tests/error-feedback-validation.test.ts +91 -0
- package/tests/frontend-context-loader.test.ts +609 -0
- package/tests/global-constitution.test.ts +110 -0
- package/tests/key-store.test.ts +73 -0
- package/tests/knowledge-memory.test.ts +327 -0
- package/tests/project-index.test.ts +206 -0
- package/tests/prompt-hasher.test.ts +19 -0
- package/tests/requirement-decomposer.test.ts +171 -0
- package/tests/reviewer.test.ts +4 -1
- package/tests/run-logger.test.ts +289 -0
- package/tests/run-snapshot.test.ts +113 -0
- package/tests/safe-json.test.ts +63 -0
- package/tests/spec-updater.test.ts +161 -0
- package/tests/test-generator.test.ts +146 -0
- package/tests/token-budget.test.ts +124 -0
- package/tests/vcr-hash.test.ts +101 -0
- package/tests/workspace-loader.test.ts +277 -0
package/dist/index.mjs
CHANGED
|
@@ -4107,8 +4107,8 @@ ${currentSpec}`,
|
|
|
4107
4107
|
};
|
|
4108
4108
|
|
|
4109
4109
|
// core/code-generator.ts
|
|
4110
|
-
import
|
|
4111
|
-
import { execSync, spawnSync } from "child_process";
|
|
4110
|
+
import chalk10 from "chalk";
|
|
4111
|
+
import { execSync as execSync2, spawnSync } from "child_process";
|
|
4112
4112
|
import * as path6 from "path";
|
|
4113
4113
|
import * as fs10 from "fs-extra";
|
|
4114
4114
|
|
|
@@ -4685,7 +4685,7 @@ async function updateTaskStatus(specFilePath, taskId, status) {
|
|
|
4685
4685
|
}
|
|
4686
4686
|
|
|
4687
4687
|
// core/dsl-extractor.ts
|
|
4688
|
-
import
|
|
4688
|
+
import chalk7 from "chalk";
|
|
4689
4689
|
import * as fs6 from "fs-extra";
|
|
4690
4690
|
import * as path4 from "path";
|
|
4691
4691
|
import { select as select2 } from "@inquirer/prompts";
|
|
@@ -4774,6 +4774,7 @@ function validateDsl(raw) {
|
|
|
4774
4774
|
}
|
|
4775
4775
|
}
|
|
4776
4776
|
}
|
|
4777
|
+
crossReferenceChecks(obj, errors);
|
|
4777
4778
|
if (errors.length > 0) {
|
|
4778
4779
|
return { valid: false, errors };
|
|
4779
4780
|
}
|
|
@@ -5005,6 +5006,64 @@ function validateComponent(raw, path10, errors) {
|
|
|
5005
5006
|
}
|
|
5006
5007
|
}
|
|
5007
5008
|
}
|
|
5009
|
+
function crossReferenceChecks(obj, errors) {
|
|
5010
|
+
const models = Array.isArray(obj["models"]) ? obj["models"] : [];
|
|
5011
|
+
const endpoints = Array.isArray(obj["endpoints"]) ? obj["endpoints"] : [];
|
|
5012
|
+
const components = Array.isArray(obj["components"]) ? obj["components"] : [];
|
|
5013
|
+
const seenRoutes = /* @__PURE__ */ new Map();
|
|
5014
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
5015
|
+
const ep = endpoints[i];
|
|
5016
|
+
if (typeof ep?.["method"] === "string" && typeof ep?.["path"] === "string") {
|
|
5017
|
+
const route = `${ep["method"].toUpperCase()} ${ep["path"]}`;
|
|
5018
|
+
if (seenRoutes.has(route)) {
|
|
5019
|
+
errors.push({
|
|
5020
|
+
path: `endpoints[${i}]`,
|
|
5021
|
+
message: `Duplicate route "${route}" \u2014 also defined at endpoints[${seenRoutes.get(route)}]`
|
|
5022
|
+
});
|
|
5023
|
+
} else {
|
|
5024
|
+
seenRoutes.set(route, i);
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
const modelNames = new Set(
|
|
5029
|
+
models.filter((m) => typeof m?.["name"] === "string").map((m) => m["name"])
|
|
5030
|
+
);
|
|
5031
|
+
for (let i = 0; i < models.length; i++) {
|
|
5032
|
+
const m = models[i];
|
|
5033
|
+
if (!Array.isArray(m?.["relations"])) continue;
|
|
5034
|
+
for (const rel of m["relations"]) {
|
|
5035
|
+
if (typeof rel !== "string") continue;
|
|
5036
|
+
const refMatch = rel.match(/(?:hasMany|hasOne|belongsTo|manyToMany)\s+(\w+)/i);
|
|
5037
|
+
if (refMatch) {
|
|
5038
|
+
const refName = refMatch[1];
|
|
5039
|
+
if (!modelNames.has(refName)) {
|
|
5040
|
+
errors.push({
|
|
5041
|
+
path: `models[${i}].relations`,
|
|
5042
|
+
message: `Relation references model "${refName}" which is not defined in models[]`
|
|
5043
|
+
});
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
const endpointIds = new Set(
|
|
5049
|
+
endpoints.filter((e) => typeof e?.["id"] === "string").map((e) => e["id"])
|
|
5050
|
+
);
|
|
5051
|
+
if (endpointIds.size > 0) {
|
|
5052
|
+
for (let i = 0; i < components.length; i++) {
|
|
5053
|
+
const c = components[i];
|
|
5054
|
+
if (!Array.isArray(c?.["apiCalls"])) continue;
|
|
5055
|
+
for (const call of c["apiCalls"]) {
|
|
5056
|
+
if (typeof call !== "string") continue;
|
|
5057
|
+
if (!endpointIds.has(call)) {
|
|
5058
|
+
errors.push({
|
|
5059
|
+
path: `components[${i}].apiCalls`,
|
|
5060
|
+
message: `References endpoint "${call}" which is not defined in endpoints[]`
|
|
5061
|
+
});
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5008
5067
|
function requireNonEmptyString(v2, path10, errors) {
|
|
5009
5068
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5010
5069
|
errors.push({
|
|
@@ -5019,6 +5078,26 @@ function typeLabel(v2) {
|
|
|
5019
5078
|
return typeof v2;
|
|
5020
5079
|
}
|
|
5021
5080
|
|
|
5081
|
+
// core/token-budget.ts
|
|
5082
|
+
import chalk6 from "chalk";
|
|
5083
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5084
|
+
function estimateTokens(text) {
|
|
5085
|
+
if (!text) return 0;
|
|
5086
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5087
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5088
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5089
|
+
}
|
|
5090
|
+
var DEFAULT_TOKEN_BUDGETS = {
|
|
5091
|
+
gemini: 9e5,
|
|
5092
|
+
claude: 18e4,
|
|
5093
|
+
openai: 12e4,
|
|
5094
|
+
deepseek: 6e4,
|
|
5095
|
+
default: 1e5
|
|
5096
|
+
};
|
|
5097
|
+
function getDefaultBudget(providerName) {
|
|
5098
|
+
return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5022
5101
|
// core/dsl-extractor.ts
|
|
5023
5102
|
function dslFilePath(specFilePath) {
|
|
5024
5103
|
const dir = path4.dirname(specFilePath);
|
|
@@ -5177,7 +5256,9 @@ var ROUTING_LIBS = [
|
|
|
5177
5256
|
["@tanstack/react-router", "tanstack-router"],
|
|
5178
5257
|
["react-navigation", "react-navigation"],
|
|
5179
5258
|
["expo-router", "expo-router"],
|
|
5180
|
-
["vue-router", "vue-router"]
|
|
5259
|
+
["vue-router", "vue-router"],
|
|
5260
|
+
["@solidjs/router", "solid-router"],
|
|
5261
|
+
["@builder.io/qwik-city", "qwik-city"]
|
|
5181
5262
|
];
|
|
5182
5263
|
async function loadFrontendContext(projectRoot) {
|
|
5183
5264
|
const ctx = {
|
|
@@ -5209,6 +5290,18 @@ async function loadFrontendContext(projectRoot) {
|
|
|
5209
5290
|
const has = (name) => depKeys.includes(name);
|
|
5210
5291
|
if (has("react-native") || has("expo")) {
|
|
5211
5292
|
ctx.framework = "react-native";
|
|
5293
|
+
} else if (has("@sveltejs/kit")) {
|
|
5294
|
+
ctx.framework = "sveltekit";
|
|
5295
|
+
} else if (has("svelte")) {
|
|
5296
|
+
ctx.framework = "svelte";
|
|
5297
|
+
} else if (has("@builder.io/qwik")) {
|
|
5298
|
+
ctx.framework = "qwik";
|
|
5299
|
+
} else if (has("@remix-run/react") || has("@remix-run/node")) {
|
|
5300
|
+
ctx.framework = "remix";
|
|
5301
|
+
} else if (has("astro")) {
|
|
5302
|
+
ctx.framework = "astro";
|
|
5303
|
+
} else if (has("solid-js")) {
|
|
5304
|
+
ctx.framework = "solid";
|
|
5212
5305
|
} else if (has("next")) {
|
|
5213
5306
|
ctx.framework = "next";
|
|
5214
5307
|
} else if (has("react")) {
|
|
@@ -5235,6 +5328,12 @@ async function loadFrontendContext(projectRoot) {
|
|
|
5235
5328
|
if (ctx.framework === "next") {
|
|
5236
5329
|
const hasAppDir = await fs7.pathExists(path5.join(projectRoot, "app"));
|
|
5237
5330
|
ctx.routingPattern = hasAppDir ? "next-app-router" : "next-pages-router";
|
|
5331
|
+
} else if (ctx.framework === "sveltekit") {
|
|
5332
|
+
ctx.routingPattern = "sveltekit-file-router";
|
|
5333
|
+
} else if (ctx.framework === "remix") {
|
|
5334
|
+
ctx.routingPattern = "remix-file-router";
|
|
5335
|
+
} else if (ctx.framework === "astro") {
|
|
5336
|
+
ctx.routingPattern = "astro-file-router";
|
|
5238
5337
|
} else {
|
|
5239
5338
|
for (const [lib, label] of ROUTING_LIBS) {
|
|
5240
5339
|
if (has(lib)) {
|
|
@@ -5620,13 +5719,14 @@ function getActiveSnapshot() {
|
|
|
5620
5719
|
|
|
5621
5720
|
// core/run-logger.ts
|
|
5622
5721
|
import * as fs9 from "fs-extra";
|
|
5623
|
-
import
|
|
5722
|
+
import chalk8 from "chalk";
|
|
5624
5723
|
var _activeLogger = null;
|
|
5625
5724
|
function getActiveLogger() {
|
|
5626
5725
|
return _activeLogger;
|
|
5627
5726
|
}
|
|
5628
5727
|
|
|
5629
|
-
// core/
|
|
5728
|
+
// core/codegen/helpers.ts
|
|
5729
|
+
import { execSync } from "child_process";
|
|
5630
5730
|
function buildSharedConfigSection(context) {
|
|
5631
5731
|
if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
|
|
5632
5732
|
const lines = [
|
|
@@ -5758,7 +5858,7 @@ function buildGeneratedFilesSection(cache) {
|
|
|
5758
5858
|
}
|
|
5759
5859
|
function isRtkAvailable() {
|
|
5760
5860
|
try {
|
|
5761
|
-
execSync("rtk --version", { stdio: "ignore" });
|
|
5861
|
+
execSync("rtk --version", { stdio: "ignore", timeout: 1e4 });
|
|
5762
5862
|
return true;
|
|
5763
5863
|
} catch {
|
|
5764
5864
|
return false;
|
|
@@ -5782,6 +5882,73 @@ function parseJsonArray(text) {
|
|
|
5782
5882
|
}
|
|
5783
5883
|
return [];
|
|
5784
5884
|
}
|
|
5885
|
+
|
|
5886
|
+
// core/codegen/topo-sort.ts
|
|
5887
|
+
import chalk9 from "chalk";
|
|
5888
|
+
function topoSortLayerTasks(tasks) {
|
|
5889
|
+
if (tasks.length <= 1) return [tasks];
|
|
5890
|
+
const idSet = new Set(tasks.map((t) => t.id));
|
|
5891
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
5892
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
5893
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
5894
|
+
for (const task of tasks) {
|
|
5895
|
+
inDegree.set(task.id, 0);
|
|
5896
|
+
dependents.set(task.id, []);
|
|
5897
|
+
}
|
|
5898
|
+
for (const task of tasks) {
|
|
5899
|
+
const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
|
|
5900
|
+
inDegree.set(task.id, intraDeps.length);
|
|
5901
|
+
for (const dep of intraDeps) {
|
|
5902
|
+
dependents.get(dep).push(task.id);
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
const batches = [];
|
|
5906
|
+
const remaining = new Set(tasks.map((t) => t.id));
|
|
5907
|
+
while (remaining.size > 0) {
|
|
5908
|
+
const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
|
|
5909
|
+
if (batch.length === 0) {
|
|
5910
|
+
batches.push([...remaining].map((id) => taskById.get(id)));
|
|
5911
|
+
break;
|
|
5912
|
+
}
|
|
5913
|
+
batches.push(batch);
|
|
5914
|
+
for (const task of batch) {
|
|
5915
|
+
remaining.delete(task.id);
|
|
5916
|
+
for (const dependent of dependents.get(task.id)) {
|
|
5917
|
+
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
return batches;
|
|
5922
|
+
}
|
|
5923
|
+
var LAYER_ICONS = {
|
|
5924
|
+
data: "\u{1F4BE}",
|
|
5925
|
+
infra: "\u2699\uFE0F ",
|
|
5926
|
+
service: "\u{1F527}",
|
|
5927
|
+
api: "\u{1F310}",
|
|
5928
|
+
view: "\u{1F5A5}\uFE0F ",
|
|
5929
|
+
route: "\u{1F5FA}\uFE0F ",
|
|
5930
|
+
test: "\u{1F9EA}"
|
|
5931
|
+
};
|
|
5932
|
+
function printTaskProgress(completed, total, task, mode) {
|
|
5933
|
+
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
5934
|
+
const barWidth = 20;
|
|
5935
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
5936
|
+
const bar = chalk9.green("\u2588".repeat(filled)) + chalk9.gray("\u2591".repeat(barWidth - filled));
|
|
5937
|
+
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
5938
|
+
if (mode === "skip") {
|
|
5939
|
+
console.log(
|
|
5940
|
+
chalk9.gray(`
|
|
5941
|
+
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
5942
|
+
);
|
|
5943
|
+
} else {
|
|
5944
|
+
console.log(
|
|
5945
|
+
chalk9.bold(`
|
|
5946
|
+
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
5947
|
+
);
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
|
|
5951
|
+
// core/code-generator.ts
|
|
5785
5952
|
var CodeGenerator = class {
|
|
5786
5953
|
constructor(provider, mode = "claude-code") {
|
|
5787
5954
|
this.provider = provider;
|
|
@@ -5792,13 +5959,13 @@ var CodeGenerator = class {
|
|
|
5792
5959
|
let effectiveMode = this.mode;
|
|
5793
5960
|
if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
|
|
5794
5961
|
console.log(
|
|
5795
|
-
|
|
5962
|
+
chalk10.yellow(
|
|
5796
5963
|
`
|
|
5797
5964
|
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
5798
5965
|
)
|
|
5799
5966
|
);
|
|
5800
|
-
console.log(
|
|
5801
|
-
console.log(
|
|
5967
|
+
console.log(chalk10.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
5968
|
+
console.log(chalk10.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
5802
5969
|
`));
|
|
5803
5970
|
effectiveMode = "api";
|
|
5804
5971
|
}
|
|
@@ -5816,23 +5983,23 @@ var CodeGenerator = class {
|
|
|
5816
5983
|
// ── Mode: claude-code ──────────────────────────────────────────────────────
|
|
5817
5984
|
isClaudeCLIAvailable() {
|
|
5818
5985
|
try {
|
|
5819
|
-
|
|
5986
|
+
execSync2("claude --version", { stdio: "ignore", timeout: 1e4 });
|
|
5820
5987
|
return true;
|
|
5821
5988
|
} catch {
|
|
5822
5989
|
return false;
|
|
5823
5990
|
}
|
|
5824
5991
|
}
|
|
5825
5992
|
async runClaudeCode(specFilePath, workingDir, options = {}) {
|
|
5826
|
-
console.log(
|
|
5993
|
+
console.log(chalk10.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5827
5994
|
if (!this.isClaudeCLIAvailable()) {
|
|
5828
|
-
console.log(
|
|
5829
|
-
console.log(
|
|
5995
|
+
console.log(chalk10.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
|
|
5996
|
+
console.log(chalk10.gray(" Install: npm install -g @anthropic-ai/claude-code"));
|
|
5830
5997
|
return this.runPlanMode(specFilePath);
|
|
5831
5998
|
}
|
|
5832
5999
|
const rtkAvailable = isRtkAvailable();
|
|
5833
6000
|
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
5834
6001
|
if (rtkAvailable) {
|
|
5835
|
-
console.log(
|
|
6002
|
+
console.log(chalk10.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
5836
6003
|
}
|
|
5837
6004
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5838
6005
|
if (options.auto && tasks && tasks.length > 0) {
|
|
@@ -5848,28 +6015,28 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5848
6015
|
const promptFile = path6.join(workingDir, ".claude-prompt.txt");
|
|
5849
6016
|
await fs10.writeFile(promptFile, promptContent, "utf-8");
|
|
5850
6017
|
if (options.auto) {
|
|
5851
|
-
console.log(
|
|
5852
|
-
console.log(
|
|
6018
|
+
console.log(chalk10.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6019
|
+
console.log(chalk10.gray(` Spec: ${specFilePath}`));
|
|
5853
6020
|
try {
|
|
5854
6021
|
spawnSync(claudeCmd, ["-p", promptContent], {
|
|
5855
6022
|
cwd: workingDir,
|
|
5856
6023
|
stdio: "inherit",
|
|
5857
6024
|
shell: false
|
|
5858
6025
|
});
|
|
5859
|
-
console.log(
|
|
6026
|
+
console.log(chalk10.green("\n \u2714 Claude Code completed."));
|
|
5860
6027
|
} catch {
|
|
5861
|
-
console.log(
|
|
6028
|
+
console.log(chalk10.yellow("\n Claude Code exited. Check output above."));
|
|
5862
6029
|
}
|
|
5863
6030
|
} else {
|
|
5864
|
-
console.log(
|
|
5865
|
-
console.log(
|
|
5866
|
-
if (tasks) console.log(
|
|
5867
|
-
console.log(
|
|
6031
|
+
console.log(chalk10.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
|
|
6032
|
+
console.log(chalk10.gray(` Spec: ${specFilePath}`));
|
|
6033
|
+
if (tasks) console.log(chalk10.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
|
|
6034
|
+
console.log(chalk10.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
|
|
5868
6035
|
try {
|
|
5869
|
-
|
|
5870
|
-
console.log(
|
|
6036
|
+
execSync2(claudeCmd, { cwd: workingDir, stdio: "inherit" });
|
|
6037
|
+
console.log(chalk10.green("\n \u2714 Claude Code session completed."));
|
|
5871
6038
|
} catch {
|
|
5872
|
-
console.log(
|
|
6039
|
+
console.log(chalk10.yellow("\n Claude Code session ended. Continuing workflow."));
|
|
5873
6040
|
}
|
|
5874
6041
|
}
|
|
5875
6042
|
}
|
|
@@ -5882,10 +6049,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5882
6049
|
const pending = tasks.filter((t) => t.status !== "done");
|
|
5883
6050
|
const doneCount = tasks.length - pending.length;
|
|
5884
6051
|
if (options.resume && doneCount > 0) {
|
|
5885
|
-
console.log(
|
|
6052
|
+
console.log(chalk10.cyan(`
|
|
5886
6053
|
Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
|
|
5887
6054
|
} else {
|
|
5888
|
-
console.log(
|
|
6055
|
+
console.log(chalk10.cyan(`
|
|
5889
6056
|
Incremental mode: ${tasks.length} tasks`));
|
|
5890
6057
|
}
|
|
5891
6058
|
let completed = doneCount;
|
|
@@ -5914,33 +6081,33 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
5914
6081
|
completed++;
|
|
5915
6082
|
} catch {
|
|
5916
6083
|
taskStatus = "failed";
|
|
5917
|
-
console.log(
|
|
6084
|
+
console.log(chalk10.yellow(`
|
|
5918
6085
|
\u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
|
|
5919
6086
|
}
|
|
5920
6087
|
await updateTaskStatus(specFilePath, task.id, taskStatus);
|
|
5921
6088
|
}
|
|
5922
6089
|
const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
|
|
5923
6090
|
console.log(
|
|
5924
|
-
|
|
6091
|
+
chalk10.bold(
|
|
5925
6092
|
`
|
|
5926
|
-
${successCount === tasks.length ?
|
|
6093
|
+
${successCount === tasks.length ? chalk10.green("\u2714") : chalk10.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
|
|
5927
6094
|
)
|
|
5928
6095
|
);
|
|
5929
6096
|
}
|
|
5930
6097
|
// ── Mode: api ─────────────────────────────────────────────────────────────
|
|
5931
6098
|
async runApiMode(specFilePath, workingDir, context, options = {}) {
|
|
5932
6099
|
console.log(
|
|
5933
|
-
|
|
6100
|
+
chalk10.blue(
|
|
5934
6101
|
`
|
|
5935
6102
|
\u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
|
|
5936
6103
|
)
|
|
5937
6104
|
);
|
|
5938
6105
|
const systemPrompt = getCodeGenSystemPrompt(options.repoType);
|
|
5939
6106
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
5940
|
-
console.log(
|
|
6107
|
+
console.log(chalk10.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
5941
6108
|
}
|
|
5942
6109
|
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
5943
|
-
|
|
6110
|
+
let constitutionSection = context?.constitution ? `
|
|
5944
6111
|
=== Project Constitution (MUST follow) ===
|
|
5945
6112
|
${context.constitution}
|
|
5946
6113
|
` : "";
|
|
@@ -5955,7 +6122,7 @@ ${buildDslContextSection(dsl)}
|
|
|
5955
6122
|
if (dsl) {
|
|
5956
6123
|
const cmpCount = dsl.components?.length ?? 0;
|
|
5957
6124
|
const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
|
|
5958
|
-
console.log(
|
|
6125
|
+
console.log(chalk10.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
|
|
5959
6126
|
}
|
|
5960
6127
|
const isFrontend = isFrontendDeps(context?.dependencies ?? []);
|
|
5961
6128
|
let frontendSection = "";
|
|
@@ -5964,13 +6131,30 @@ ${buildDslContextSection(dsl)}
|
|
|
5964
6131
|
frontendSection = `
|
|
5965
6132
|
${buildFrontendContextSection(fctx)}
|
|
5966
6133
|
`;
|
|
5967
|
-
console.log(
|
|
6134
|
+
console.log(chalk10.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
6135
|
+
}
|
|
6136
|
+
const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
|
|
6137
|
+
const estimatedTokenCount = estimateTokens(allContextText);
|
|
6138
|
+
const budget = getDefaultBudget(this.provider.providerName);
|
|
6139
|
+
if (estimatedTokenCount > budget * 0.7) {
|
|
6140
|
+
console.log(
|
|
6141
|
+
chalk10.yellow(
|
|
6142
|
+
` \u26A0 Context size: ~${Math.round(estimatedTokenCount / 1e3)}K tokens (budget: ${Math.round(budget / 1e3)}K for ${this.provider.providerName})`
|
|
6143
|
+
)
|
|
6144
|
+
);
|
|
6145
|
+
if (constitutionSection.length > 4e3) {
|
|
6146
|
+
const s9Start = constitutionSection.indexOf("## 9.");
|
|
6147
|
+
if (s9Start > 0) {
|
|
6148
|
+
constitutionSection = constitutionSection.slice(0, s9Start) + "## 9. \u79EF\u7D2F\u6559\u8BAD (Accumulated Lessons)\n[Trimmed for context budget \u2014 run `ai-spec init --consolidate` to prune]\n";
|
|
6149
|
+
console.log(chalk10.gray(" \u2192 \xA79 trimmed from constitution to save tokens."));
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
5968
6152
|
}
|
|
5969
6153
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5970
6154
|
if (tasks && tasks.length > 0) {
|
|
5971
6155
|
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
5972
6156
|
}
|
|
5973
|
-
console.log(
|
|
6157
|
+
console.log(chalk10.gray(" [1/2] Planning implementation files..."));
|
|
5974
6158
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
5975
6159
|
|
|
5976
6160
|
IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
|
|
@@ -5993,18 +6177,18 @@ Output ONLY a valid JSON array:
|
|
|
5993
6177
|
const planResponse = await this.provider.generate(planPrompt, systemPrompt);
|
|
5994
6178
|
filePlan = parseJsonArray(planResponse);
|
|
5995
6179
|
} catch (err) {
|
|
5996
|
-
console.error(
|
|
6180
|
+
console.error(chalk10.red(" Failed to generate file plan:"), err);
|
|
5997
6181
|
}
|
|
5998
6182
|
if (filePlan.length === 0) {
|
|
5999
|
-
console.log(
|
|
6183
|
+
console.log(chalk10.yellow(" Could not determine file plan. Falling back to plan mode."));
|
|
6000
6184
|
await this.runPlanMode(specFilePath);
|
|
6001
6185
|
return [];
|
|
6002
6186
|
}
|
|
6003
|
-
console.log(
|
|
6187
|
+
console.log(chalk10.cyan(`
|
|
6004
6188
|
Plan: ${filePlan.length} file(s) to process`));
|
|
6005
6189
|
filePlan.forEach((item) => {
|
|
6006
|
-
const icon = item.action === "create" ?
|
|
6007
|
-
console.log(` ${icon} ${item.file}: ${
|
|
6190
|
+
const icon = item.action === "create" ? chalk10.green("+") : chalk10.yellow("~");
|
|
6191
|
+
console.log(` ${icon} ${item.file}: ${chalk10.gray(item.description)}`);
|
|
6008
6192
|
});
|
|
6009
6193
|
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
6010
6194
|
return files;
|
|
@@ -6013,13 +6197,13 @@ Output ONLY a valid JSON array:
|
|
|
6013
6197
|
const pendingTasks = tasks.filter((t) => t.status !== "done");
|
|
6014
6198
|
const doneCount = tasks.length - pendingTasks.length;
|
|
6015
6199
|
if (options.resume && doneCount > 0) {
|
|
6016
|
-
console.log(
|
|
6017
|
-
Task-based generation (resume): ${tasks.length} tasks (${
|
|
6200
|
+
console.log(chalk10.cyan(`
|
|
6201
|
+
Task-based generation (resume): ${tasks.length} tasks (${chalk10.green(doneCount + " already done")}, skipping)`));
|
|
6018
6202
|
} else if (doneCount > 0) {
|
|
6019
|
-
console.log(
|
|
6020
|
-
Task-based generation: ${tasks.length} tasks (${
|
|
6203
|
+
console.log(chalk10.cyan(`
|
|
6204
|
+
Task-based generation: ${tasks.length} tasks (${chalk10.green(doneCount + " already done")}, resuming from checkpoint)`));
|
|
6021
6205
|
} else {
|
|
6022
|
-
console.log(
|
|
6206
|
+
console.log(chalk10.cyan(`
|
|
6023
6207
|
Task-based generation: ${tasks.length} tasks`));
|
|
6024
6208
|
}
|
|
6025
6209
|
const sharedConfigPaths = new Set(
|
|
@@ -6051,9 +6235,9 @@ Output ONLY a valid JSON array:
|
|
|
6051
6235
|
const pct = Math.round(completedTasks / tasks.length * 100);
|
|
6052
6236
|
const barWidth = 20;
|
|
6053
6237
|
const filled = Math.round(pct / 100 * barWidth);
|
|
6054
|
-
const bar =
|
|
6238
|
+
const bar = chalk10.green("\u2588".repeat(filled)) + chalk10.gray("\u2591".repeat(barWidth - filled));
|
|
6055
6239
|
console.log(
|
|
6056
|
-
|
|
6240
|
+
chalk10.bold(`
|
|
6057
6241
|
[${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
|
|
6058
6242
|
);
|
|
6059
6243
|
} else {
|
|
@@ -6061,7 +6245,7 @@ Output ONLY a valid JSON array:
|
|
|
6061
6245
|
}
|
|
6062
6246
|
const executeTask = async (task, batchIsParallel) => {
|
|
6063
6247
|
if (task.filesToTouch.length === 0) {
|
|
6064
|
-
if (!batchIsParallel) console.log(
|
|
6248
|
+
if (!batchIsParallel) console.log(chalk10.gray(" No files specified, skipping."));
|
|
6065
6249
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
6066
6250
|
}
|
|
6067
6251
|
const filePlan = await Promise.all(
|
|
@@ -6118,13 +6302,19 @@ ${taskContext}`,
|
|
|
6118
6302
|
const layerResults = [];
|
|
6119
6303
|
for (const batch of taskBatches) {
|
|
6120
6304
|
const batchIsParallel = batch.length > 1;
|
|
6121
|
-
const batchResultPromises = batch.map(
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6305
|
+
const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
|
|
6306
|
+
const settled = await Promise.allSettled(batchResultPromises);
|
|
6307
|
+
const batchResults = [];
|
|
6308
|
+
for (let i = 0; i < settled.length; i++) {
|
|
6309
|
+
const outcome = settled[i];
|
|
6310
|
+
if (outcome.status === "fulfilled") {
|
|
6311
|
+
batchResults.push(outcome.value);
|
|
6312
|
+
} else {
|
|
6313
|
+
const task = batch[i];
|
|
6314
|
+
console.log(chalk10.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
|
|
6315
|
+
batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6128
6318
|
layerResults.push(...batchResults);
|
|
6129
6319
|
await updateCacheFromBatch(batchResults);
|
|
6130
6320
|
}
|
|
@@ -6136,14 +6326,14 @@ ${taskContext}`,
|
|
|
6136
6326
|
totalFiles += result.total;
|
|
6137
6327
|
allGeneratedFiles.push(...result.files);
|
|
6138
6328
|
if (isParallel) {
|
|
6139
|
-
const icon = result.success === result.total ?
|
|
6329
|
+
const icon = result.success === result.total ? chalk10.green("\u2714") : chalk10.yellow("!");
|
|
6140
6330
|
const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
|
|
6141
6331
|
console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
|
|
6142
6332
|
}
|
|
6143
6333
|
const taskStatus = result.success === result.total ? "done" : "failed";
|
|
6144
6334
|
await updateTaskStatus(specFilePath, result.task.id, taskStatus);
|
|
6145
6335
|
if (taskStatus === "failed") {
|
|
6146
|
-
console.log(
|
|
6336
|
+
console.log(chalk10.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
|
|
6147
6337
|
}
|
|
6148
6338
|
}
|
|
6149
6339
|
completedTasks += layerTasks.length;
|
|
@@ -6158,7 +6348,7 @@ ${taskContext}`,
|
|
|
6158
6348
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
6159
6349
|
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.`;
|
|
6160
6350
|
}
|
|
6161
|
-
console.log(
|
|
6351
|
+
console.log(chalk10.gray(`
|
|
6162
6352
|
+ updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
|
|
6163
6353
|
const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
6164
6354
|
await this.generateFiles(
|
|
@@ -6176,17 +6366,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
6176
6366
|
}
|
|
6177
6367
|
}
|
|
6178
6368
|
console.log(
|
|
6179
|
-
|
|
6369
|
+
chalk10.bold(
|
|
6180
6370
|
`
|
|
6181
|
-
${totalSuccess === totalFiles ?
|
|
6371
|
+
${totalSuccess === totalFiles ? chalk10.green("\u2714") : chalk10.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
|
|
6182
6372
|
)
|
|
6183
6373
|
);
|
|
6184
6374
|
return allGeneratedFiles;
|
|
6185
6375
|
}
|
|
6186
6376
|
async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
|
|
6187
|
-
const prefix = taskLabel ? ` [${
|
|
6377
|
+
const prefix = taskLabel ? ` [${chalk10.cyan(taskLabel)}] ` : " ";
|
|
6188
6378
|
if (!taskLabel) {
|
|
6189
|
-
console.log(
|
|
6379
|
+
console.log(chalk10.gray(`
|
|
6190
6380
|
Generating ${filePlan.length} file(s)...`));
|
|
6191
6381
|
}
|
|
6192
6382
|
let successCount = 0;
|
|
@@ -6214,17 +6404,17 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6214
6404
|
await fs10.ensureDir(path6.dirname(fullPath));
|
|
6215
6405
|
await fs10.writeFile(fullPath, fileContent, "utf-8");
|
|
6216
6406
|
getActiveLogger()?.fileWritten(item.file);
|
|
6217
|
-
console.log(`${prefix}${existingContent ?
|
|
6407
|
+
console.log(`${prefix}${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)} ${chalk10.green("\u2714")}`);
|
|
6218
6408
|
successCount++;
|
|
6219
6409
|
writtenFiles.push(item.file);
|
|
6220
6410
|
} catch (err) {
|
|
6221
|
-
console.log(`${prefix}${
|
|
6411
|
+
console.log(`${prefix}${chalk10.red("\u2718")} ${chalk10.bold(item.file)} \u2014 ${chalk10.red(err.message)}`);
|
|
6222
6412
|
}
|
|
6223
6413
|
}
|
|
6224
6414
|
if (!taskLabel) {
|
|
6225
6415
|
console.log(
|
|
6226
|
-
|
|
6227
|
-
` ${successCount === filePlan.length ?
|
|
6416
|
+
chalk10.bold(
|
|
6417
|
+
` ${successCount === filePlan.length ? chalk10.green("\u2714") : chalk10.yellow("!")} ${successCount}/${filePlan.length} files written.`
|
|
6228
6418
|
)
|
|
6229
6419
|
);
|
|
6230
6420
|
}
|
|
@@ -6232,7 +6422,7 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6232
6422
|
}
|
|
6233
6423
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
6234
6424
|
async runPlanMode(specFilePath) {
|
|
6235
|
-
console.log(
|
|
6425
|
+
console.log(chalk10.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6236
6426
|
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
6237
6427
|
const plan = await this.provider.generate(
|
|
6238
6428
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
@@ -6245,80 +6435,18 @@ Be specific about:
|
|
|
6245
6435
|
${spec}`,
|
|
6246
6436
|
"You are a senior developer creating an actionable implementation guide."
|
|
6247
6437
|
);
|
|
6248
|
-
console.log(
|
|
6438
|
+
console.log(chalk10.cyan("\n") + plan);
|
|
6249
6439
|
}
|
|
6250
6440
|
};
|
|
6251
|
-
function topoSortLayerTasks(tasks) {
|
|
6252
|
-
if (tasks.length <= 1) return [tasks];
|
|
6253
|
-
const idSet = new Set(tasks.map((t) => t.id));
|
|
6254
|
-
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
6255
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
6256
|
-
const dependents = /* @__PURE__ */ new Map();
|
|
6257
|
-
for (const task of tasks) {
|
|
6258
|
-
inDegree.set(task.id, 0);
|
|
6259
|
-
dependents.set(task.id, []);
|
|
6260
|
-
}
|
|
6261
|
-
for (const task of tasks) {
|
|
6262
|
-
const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
|
|
6263
|
-
inDegree.set(task.id, intraDeps.length);
|
|
6264
|
-
for (const dep of intraDeps) {
|
|
6265
|
-
dependents.get(dep).push(task.id);
|
|
6266
|
-
}
|
|
6267
|
-
}
|
|
6268
|
-
const batches = [];
|
|
6269
|
-
const remaining = new Set(tasks.map((t) => t.id));
|
|
6270
|
-
while (remaining.size > 0) {
|
|
6271
|
-
const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
|
|
6272
|
-
if (batch.length === 0) {
|
|
6273
|
-
batches.push([...remaining].map((id) => taskById.get(id)));
|
|
6274
|
-
break;
|
|
6275
|
-
}
|
|
6276
|
-
batches.push(batch);
|
|
6277
|
-
for (const task of batch) {
|
|
6278
|
-
remaining.delete(task.id);
|
|
6279
|
-
for (const dependent of dependents.get(task.id)) {
|
|
6280
|
-
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
6281
|
-
}
|
|
6282
|
-
}
|
|
6283
|
-
}
|
|
6284
|
-
return batches;
|
|
6285
|
-
}
|
|
6286
|
-
var LAYER_ICONS = {
|
|
6287
|
-
data: "\u{1F4BE}",
|
|
6288
|
-
infra: "\u2699\uFE0F ",
|
|
6289
|
-
service: "\u{1F527}",
|
|
6290
|
-
api: "\u{1F310}",
|
|
6291
|
-
view: "\u{1F5A5}\uFE0F ",
|
|
6292
|
-
route: "\u{1F5FA}\uFE0F ",
|
|
6293
|
-
test: "\u{1F9EA}"
|
|
6294
|
-
};
|
|
6295
|
-
function printTaskProgress(completed, total, task, mode) {
|
|
6296
|
-
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
6297
|
-
const barWidth = 20;
|
|
6298
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
6299
|
-
const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
|
|
6300
|
-
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
6301
|
-
if (mode === "skip") {
|
|
6302
|
-
console.log(
|
|
6303
|
-
chalk8.gray(`
|
|
6304
|
-
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
6305
|
-
);
|
|
6306
|
-
} else {
|
|
6307
|
-
console.log(
|
|
6308
|
-
chalk8.bold(`
|
|
6309
|
-
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
6310
|
-
);
|
|
6311
|
-
}
|
|
6312
|
-
}
|
|
6313
6441
|
|
|
6314
6442
|
// core/reviewer.ts
|
|
6315
|
-
import
|
|
6316
|
-
import { execSync as
|
|
6443
|
+
import chalk12 from "chalk";
|
|
6444
|
+
import { execSync as execSync3 } from "child_process";
|
|
6317
6445
|
import * as path8 from "path";
|
|
6318
6446
|
import * as fs12 from "fs-extra";
|
|
6319
6447
|
|
|
6320
6448
|
// core/constitution-generator.ts
|
|
6321
|
-
import
|
|
6449
|
+
import chalk11 from "chalk";
|
|
6322
6450
|
import * as fs11 from "fs-extra";
|
|
6323
6451
|
import * as path7 from "path";
|
|
6324
6452
|
|
|
@@ -6468,7 +6596,7 @@ async function loadConstitution(projectRoot) {
|
|
|
6468
6596
|
function printConstitutionHint(exists) {
|
|
6469
6597
|
if (!exists) {
|
|
6470
6598
|
console.log(
|
|
6471
|
-
|
|
6599
|
+
chalk11.yellow(
|
|
6472
6600
|
" \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
|
|
6473
6601
|
)
|
|
6474
6602
|
);
|
|
@@ -6552,16 +6680,16 @@ var CodeReviewer = class {
|
|
|
6552
6680
|
this.projectRoot = projectRoot;
|
|
6553
6681
|
}
|
|
6554
6682
|
getGitDiff() {
|
|
6555
|
-
const silent = { encoding: "utf-8", stdio: "pipe" };
|
|
6683
|
+
const silent = { encoding: "utf-8", stdio: "pipe", cwd: this.projectRoot, timeout: 3e4 };
|
|
6556
6684
|
try {
|
|
6557
|
-
|
|
6685
|
+
execSync3("git rev-parse --is-inside-work-tree", silent);
|
|
6558
6686
|
} catch {
|
|
6559
6687
|
return "";
|
|
6560
6688
|
}
|
|
6561
6689
|
try {
|
|
6562
|
-
let diff =
|
|
6563
|
-
if (!diff.trim()) diff =
|
|
6564
|
-
if (!diff.trim()) diff =
|
|
6690
|
+
let diff = execSync3("git diff --cached", silent);
|
|
6691
|
+
if (!diff.trim()) diff = execSync3("git diff HEAD", silent);
|
|
6692
|
+
if (!diff.trim()) diff = execSync3("git diff", silent);
|
|
6565
6693
|
return diff;
|
|
6566
6694
|
} catch {
|
|
6567
6695
|
return "";
|
|
@@ -6586,7 +6714,7 @@ var CodeReviewer = class {
|
|
|
6586
6714
|
async runThreePassReview(specContent, codeContext, specFile) {
|
|
6587
6715
|
let complianceReview = "";
|
|
6588
6716
|
if (specContent && specContent.trim() && specContent !== "(No spec \u2014 review for general code quality)") {
|
|
6589
|
-
console.log(
|
|
6717
|
+
console.log(chalk12.gray(" Pass 0/3: Spec compliance check..."));
|
|
6590
6718
|
const compliancePrompt = `Check whether the implementation covers every requirement in the spec.
|
|
6591
6719
|
|
|
6592
6720
|
=== Feature Spec ===
|
|
@@ -6598,13 +6726,13 @@ ${codeContext}`;
|
|
|
6598
6726
|
const complianceScore2 = extractComplianceScore(complianceReview);
|
|
6599
6727
|
const missingCount = extractMissingCount(complianceReview);
|
|
6600
6728
|
if (complianceScore2 > 0) {
|
|
6601
|
-
const scoreColor = complianceScore2 >= 8 ?
|
|
6729
|
+
const scoreColor = complianceScore2 >= 8 ? chalk12.green : complianceScore2 >= 6 ? chalk12.yellow : chalk12.red;
|
|
6602
6730
|
console.log(
|
|
6603
|
-
|
|
6731
|
+
chalk12.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? chalk12.red(` \xB7 ${missingCount} missing requirement(s)`) : chalk12.green(" \xB7 all requirements covered"))
|
|
6604
6732
|
);
|
|
6605
6733
|
}
|
|
6606
6734
|
}
|
|
6607
|
-
console.log(
|
|
6735
|
+
console.log(chalk12.gray(` Pass 1/3: Architecture review...`));
|
|
6608
6736
|
const accumulatedLessons = await loadAccumulatedLessons(this.projectRoot);
|
|
6609
6737
|
const archPrompt = `Review the architecture of this change.
|
|
6610
6738
|
${complianceReview ? `
|
|
@@ -6621,7 +6749,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
6621
6749
|
=== Code ===
|
|
6622
6750
|
${codeContext}`;
|
|
6623
6751
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
6624
|
-
console.log(
|
|
6752
|
+
console.log(chalk12.gray(" Pass 2/3: Implementation review..."));
|
|
6625
6753
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6626
6754
|
const historyContext = buildHistoryContext(history);
|
|
6627
6755
|
const implPrompt = `Review the implementation details of this change.
|
|
@@ -6636,7 +6764,7 @@ ${codeContext}
|
|
|
6636
6764
|
${archReview}
|
|
6637
6765
|
${historyContext}`;
|
|
6638
6766
|
const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
|
|
6639
|
-
console.log(
|
|
6767
|
+
console.log(chalk12.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6640
6768
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6641
6769
|
|
|
6642
6770
|
=== Feature Spec ===
|
|
@@ -6677,37 +6805,37 @@ ${sep}
|
|
|
6677
6805
|
return combined;
|
|
6678
6806
|
}
|
|
6679
6807
|
async reviewCode(specContent, specFile) {
|
|
6680
|
-
console.log(
|
|
6808
|
+
console.log(chalk12.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"));
|
|
6681
6809
|
const diff = this.getGitDiff();
|
|
6682
6810
|
if (!diff.trim()) {
|
|
6683
6811
|
console.log(
|
|
6684
|
-
|
|
6812
|
+
chalk12.yellow(" No git diff found. Stage or commit changes first, then run review.")
|
|
6685
6813
|
);
|
|
6686
|
-
console.log(
|
|
6814
|
+
console.log(chalk12.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
|
|
6687
6815
|
return "No changes";
|
|
6688
6816
|
}
|
|
6689
6817
|
const { files, added, removed } = this.getDiffStats(diff);
|
|
6690
6818
|
console.log(
|
|
6691
|
-
|
|
6819
|
+
chalk12.gray(` Diff: ${files} file(s), ${chalk12.green("+" + added)} ${chalk12.red("-" + removed)}`)
|
|
6692
6820
|
);
|
|
6693
6821
|
console.log(
|
|
6694
|
-
|
|
6822
|
+
chalk12.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6695
6823
|
);
|
|
6696
6824
|
const codeContext = diff.slice(0, 1e4);
|
|
6697
6825
|
const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
|
|
6698
|
-
console.log(
|
|
6826
|
+
console.log(chalk12.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"));
|
|
6699
6827
|
console.log(reviewResult);
|
|
6700
|
-
console.log(
|
|
6828
|
+
console.log(chalk12.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6701
6829
|
return reviewResult;
|
|
6702
6830
|
}
|
|
6703
6831
|
/**
|
|
6704
6832
|
* Review directly from generated file contents (for api mode where git diff is empty).
|
|
6705
6833
|
*/
|
|
6706
6834
|
async reviewFiles(specContent, filePaths, workingDir, specFile) {
|
|
6707
|
-
console.log(
|
|
6708
|
-
console.log(
|
|
6835
|
+
console.log(chalk12.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6836
|
+
console.log(chalk12.gray(` Reviewing ${filePaths.length} generated file(s)...`));
|
|
6709
6837
|
console.log(
|
|
6710
|
-
|
|
6838
|
+
chalk12.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6711
6839
|
);
|
|
6712
6840
|
let filesSection = "";
|
|
6713
6841
|
for (const filePath of filePaths) {
|
|
@@ -6728,28 +6856,28 @@ ${content.slice(0, 3e3)}`;
|
|
|
6728
6856
|
}
|
|
6729
6857
|
}
|
|
6730
6858
|
const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
|
|
6731
|
-
console.log(
|
|
6859
|
+
console.log(chalk12.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"));
|
|
6732
6860
|
console.log(reviewResult);
|
|
6733
|
-
console.log(
|
|
6861
|
+
console.log(chalk12.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6734
6862
|
return reviewResult;
|
|
6735
6863
|
}
|
|
6736
6864
|
/** Print score trend from history (last N reviews) */
|
|
6737
6865
|
async printScoreTrend(limit = 5) {
|
|
6738
6866
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6739
6867
|
if (history.length === 0) {
|
|
6740
|
-
console.log(
|
|
6868
|
+
console.log(chalk12.gray(" No review history yet."));
|
|
6741
6869
|
return;
|
|
6742
6870
|
}
|
|
6743
6871
|
const recent = history.slice(-limit);
|
|
6744
|
-
console.log(
|
|
6872
|
+
console.log(chalk12.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"));
|
|
6745
6873
|
for (const entry of recent) {
|
|
6746
6874
|
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
6747
|
-
const color = entry.score >= 8 ?
|
|
6748
|
-
const impactTag = entry.impactLevel ?
|
|
6749
|
-
const complexityTag = entry.complexityLevel ?
|
|
6875
|
+
const color = entry.score >= 8 ? chalk12.green : entry.score >= 6 ? chalk12.yellow : chalk12.red;
|
|
6876
|
+
const impactTag = entry.impactLevel ? chalk12.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk12.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk12.yellow(entry.impactLevel) : chalk12.green(entry.impactLevel)}`) : "";
|
|
6877
|
+
const complexityTag = entry.complexityLevel ? chalk12.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk12.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk12.yellow(entry.complexityLevel) : chalk12.green(entry.complexityLevel)}`) : "";
|
|
6750
6878
|
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
|
|
6751
6879
|
}
|
|
6752
|
-
console.log(
|
|
6880
|
+
console.log(chalk12.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6753
6881
|
}
|
|
6754
6882
|
};
|
|
6755
6883
|
|
|
@@ -6799,17 +6927,17 @@ function parseSpecAndTasks(raw) {
|
|
|
6799
6927
|
}
|
|
6800
6928
|
|
|
6801
6929
|
// git/worktree.ts
|
|
6802
|
-
import { execSync as
|
|
6930
|
+
import { execSync as execSync4 } from "child_process";
|
|
6803
6931
|
import * as path9 from "path";
|
|
6804
6932
|
import * as fs13 from "fs-extra";
|
|
6805
|
-
import
|
|
6933
|
+
import chalk13 from "chalk";
|
|
6806
6934
|
var GitWorktreeManager = class {
|
|
6807
6935
|
constructor(baseDir) {
|
|
6808
6936
|
this.baseDir = baseDir;
|
|
6809
6937
|
}
|
|
6810
6938
|
isGitRepo() {
|
|
6811
6939
|
try {
|
|
6812
|
-
|
|
6940
|
+
execSync4("git rev-parse --is-inside-work-tree", { cwd: this.baseDir, stdio: "ignore" });
|
|
6813
6941
|
return true;
|
|
6814
6942
|
} catch {
|
|
6815
6943
|
return false;
|
|
@@ -6833,58 +6961,58 @@ var GitWorktreeManager = class {
|
|
|
6833
6961
|
if (await fs13.pathExists(dest)) continue;
|
|
6834
6962
|
try {
|
|
6835
6963
|
await fs13.ensureSymlink(src, dest, "dir");
|
|
6836
|
-
console.log(
|
|
6964
|
+
console.log(chalk13.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
6837
6965
|
} catch (err) {
|
|
6838
|
-
console.log(
|
|
6839
|
-
console.log(
|
|
6966
|
+
console.log(chalk13.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
6967
|
+
console.log(chalk13.yellow(` Run \`npm install\` inside the worktree manually.`));
|
|
6840
6968
|
}
|
|
6841
6969
|
}
|
|
6842
6970
|
}
|
|
6843
6971
|
async createWorktree(idea) {
|
|
6844
6972
|
if (!this.isGitRepo()) {
|
|
6845
|
-
console.log(
|
|
6973
|
+
console.log(chalk13.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
|
|
6846
6974
|
return null;
|
|
6847
6975
|
}
|
|
6848
6976
|
const featureName = this.sanitizeFeatureName(idea);
|
|
6849
6977
|
const branchName = `feature/${featureName}`;
|
|
6850
6978
|
const repoName = path9.basename(this.baseDir);
|
|
6851
6979
|
const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
6852
|
-
console.log(
|
|
6980
|
+
console.log(chalk13.cyan(`
|
|
6853
6981
|
--- Setting up Git Worktree ---`));
|
|
6854
6982
|
if (await fs13.pathExists(worktreePath)) {
|
|
6855
|
-
console.log(
|
|
6983
|
+
console.log(chalk13.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
6856
6984
|
await this.linkDependencies(worktreePath);
|
|
6857
6985
|
return worktreePath;
|
|
6858
6986
|
}
|
|
6859
6987
|
try {
|
|
6860
6988
|
let branchExists = false;
|
|
6861
6989
|
try {
|
|
6862
|
-
|
|
6990
|
+
execSync4(`git show-ref --verify refs/heads/${branchName}`, {
|
|
6863
6991
|
cwd: this.baseDir,
|
|
6864
6992
|
stdio: "ignore"
|
|
6865
6993
|
});
|
|
6866
6994
|
branchExists = true;
|
|
6867
6995
|
} catch {
|
|
6868
6996
|
}
|
|
6869
|
-
console.log(
|
|
6997
|
+
console.log(chalk13.gray(`Creating worktree at: ${worktreePath}`));
|
|
6870
6998
|
if (branchExists) {
|
|
6871
|
-
|
|
6999
|
+
execSync4(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6872
7000
|
cwd: this.baseDir,
|
|
6873
7001
|
stdio: "inherit"
|
|
6874
7002
|
});
|
|
6875
7003
|
} else {
|
|
6876
|
-
|
|
7004
|
+
execSync4(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
6877
7005
|
cwd: this.baseDir,
|
|
6878
7006
|
stdio: "inherit"
|
|
6879
7007
|
});
|
|
6880
7008
|
}
|
|
6881
7009
|
console.log(
|
|
6882
|
-
|
|
7010
|
+
chalk13.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
|
|
6883
7011
|
);
|
|
6884
7012
|
await this.linkDependencies(worktreePath);
|
|
6885
7013
|
return worktreePath;
|
|
6886
7014
|
} catch (error) {
|
|
6887
|
-
console.error(
|
|
7015
|
+
console.error(chalk13.red("Failed to create git worktree:"), error);
|
|
6888
7016
|
return null;
|
|
6889
7017
|
}
|
|
6890
7018
|
}
|