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.js
CHANGED
|
@@ -4173,8 +4173,8 @@ ${currentSpec}`,
|
|
|
4173
4173
|
};
|
|
4174
4174
|
|
|
4175
4175
|
// core/code-generator.ts
|
|
4176
|
-
var
|
|
4177
|
-
var
|
|
4176
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
4177
|
+
var import_child_process2 = require("child_process");
|
|
4178
4178
|
var path6 = __toESM(require("path"));
|
|
4179
4179
|
var fs10 = __toESM(require("fs-extra"));
|
|
4180
4180
|
|
|
@@ -4751,7 +4751,7 @@ async function updateTaskStatus(specFilePath, taskId, status) {
|
|
|
4751
4751
|
}
|
|
4752
4752
|
|
|
4753
4753
|
// core/dsl-extractor.ts
|
|
4754
|
-
var
|
|
4754
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
4755
4755
|
var fs6 = __toESM(require("fs-extra"));
|
|
4756
4756
|
var path4 = __toESM(require("path"));
|
|
4757
4757
|
var import_prompts2 = require("@inquirer/prompts");
|
|
@@ -4840,6 +4840,7 @@ function validateDsl(raw) {
|
|
|
4840
4840
|
}
|
|
4841
4841
|
}
|
|
4842
4842
|
}
|
|
4843
|
+
crossReferenceChecks(obj, errors);
|
|
4843
4844
|
if (errors.length > 0) {
|
|
4844
4845
|
return { valid: false, errors };
|
|
4845
4846
|
}
|
|
@@ -5071,6 +5072,64 @@ function validateComponent(raw, path10, errors) {
|
|
|
5071
5072
|
}
|
|
5072
5073
|
}
|
|
5073
5074
|
}
|
|
5075
|
+
function crossReferenceChecks(obj, errors) {
|
|
5076
|
+
const models = Array.isArray(obj["models"]) ? obj["models"] : [];
|
|
5077
|
+
const endpoints = Array.isArray(obj["endpoints"]) ? obj["endpoints"] : [];
|
|
5078
|
+
const components = Array.isArray(obj["components"]) ? obj["components"] : [];
|
|
5079
|
+
const seenRoutes = /* @__PURE__ */ new Map();
|
|
5080
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
5081
|
+
const ep = endpoints[i];
|
|
5082
|
+
if (typeof ep?.["method"] === "string" && typeof ep?.["path"] === "string") {
|
|
5083
|
+
const route = `${ep["method"].toUpperCase()} ${ep["path"]}`;
|
|
5084
|
+
if (seenRoutes.has(route)) {
|
|
5085
|
+
errors.push({
|
|
5086
|
+
path: `endpoints[${i}]`,
|
|
5087
|
+
message: `Duplicate route "${route}" \u2014 also defined at endpoints[${seenRoutes.get(route)}]`
|
|
5088
|
+
});
|
|
5089
|
+
} else {
|
|
5090
|
+
seenRoutes.set(route, i);
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
const modelNames = new Set(
|
|
5095
|
+
models.filter((m) => typeof m?.["name"] === "string").map((m) => m["name"])
|
|
5096
|
+
);
|
|
5097
|
+
for (let i = 0; i < models.length; i++) {
|
|
5098
|
+
const m = models[i];
|
|
5099
|
+
if (!Array.isArray(m?.["relations"])) continue;
|
|
5100
|
+
for (const rel of m["relations"]) {
|
|
5101
|
+
if (typeof rel !== "string") continue;
|
|
5102
|
+
const refMatch = rel.match(/(?:hasMany|hasOne|belongsTo|manyToMany)\s+(\w+)/i);
|
|
5103
|
+
if (refMatch) {
|
|
5104
|
+
const refName = refMatch[1];
|
|
5105
|
+
if (!modelNames.has(refName)) {
|
|
5106
|
+
errors.push({
|
|
5107
|
+
path: `models[${i}].relations`,
|
|
5108
|
+
message: `Relation references model "${refName}" which is not defined in models[]`
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
const endpointIds = new Set(
|
|
5115
|
+
endpoints.filter((e) => typeof e?.["id"] === "string").map((e) => e["id"])
|
|
5116
|
+
);
|
|
5117
|
+
if (endpointIds.size > 0) {
|
|
5118
|
+
for (let i = 0; i < components.length; i++) {
|
|
5119
|
+
const c = components[i];
|
|
5120
|
+
if (!Array.isArray(c?.["apiCalls"])) continue;
|
|
5121
|
+
for (const call of c["apiCalls"]) {
|
|
5122
|
+
if (typeof call !== "string") continue;
|
|
5123
|
+
if (!endpointIds.has(call)) {
|
|
5124
|
+
errors.push({
|
|
5125
|
+
path: `components[${i}].apiCalls`,
|
|
5126
|
+
message: `References endpoint "${call}" which is not defined in endpoints[]`
|
|
5127
|
+
});
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5074
5133
|
function requireNonEmptyString(v2, path10, errors) {
|
|
5075
5134
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5076
5135
|
errors.push({
|
|
@@ -5085,6 +5144,26 @@ function typeLabel(v2) {
|
|
|
5085
5144
|
return typeof v2;
|
|
5086
5145
|
}
|
|
5087
5146
|
|
|
5147
|
+
// core/token-budget.ts
|
|
5148
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
5149
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5150
|
+
function estimateTokens(text) {
|
|
5151
|
+
if (!text) return 0;
|
|
5152
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5153
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5154
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5155
|
+
}
|
|
5156
|
+
var DEFAULT_TOKEN_BUDGETS = {
|
|
5157
|
+
gemini: 9e5,
|
|
5158
|
+
claude: 18e4,
|
|
5159
|
+
openai: 12e4,
|
|
5160
|
+
deepseek: 6e4,
|
|
5161
|
+
default: 1e5
|
|
5162
|
+
};
|
|
5163
|
+
function getDefaultBudget(providerName) {
|
|
5164
|
+
return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5088
5167
|
// core/dsl-extractor.ts
|
|
5089
5168
|
function dslFilePath(specFilePath) {
|
|
5090
5169
|
const dir = path4.dirname(specFilePath);
|
|
@@ -5243,7 +5322,9 @@ var ROUTING_LIBS = [
|
|
|
5243
5322
|
["@tanstack/react-router", "tanstack-router"],
|
|
5244
5323
|
["react-navigation", "react-navigation"],
|
|
5245
5324
|
["expo-router", "expo-router"],
|
|
5246
|
-
["vue-router", "vue-router"]
|
|
5325
|
+
["vue-router", "vue-router"],
|
|
5326
|
+
["@solidjs/router", "solid-router"],
|
|
5327
|
+
["@builder.io/qwik-city", "qwik-city"]
|
|
5247
5328
|
];
|
|
5248
5329
|
async function loadFrontendContext(projectRoot) {
|
|
5249
5330
|
const ctx = {
|
|
@@ -5275,6 +5356,18 @@ async function loadFrontendContext(projectRoot) {
|
|
|
5275
5356
|
const has = (name) => depKeys.includes(name);
|
|
5276
5357
|
if (has("react-native") || has("expo")) {
|
|
5277
5358
|
ctx.framework = "react-native";
|
|
5359
|
+
} else if (has("@sveltejs/kit")) {
|
|
5360
|
+
ctx.framework = "sveltekit";
|
|
5361
|
+
} else if (has("svelte")) {
|
|
5362
|
+
ctx.framework = "svelte";
|
|
5363
|
+
} else if (has("@builder.io/qwik")) {
|
|
5364
|
+
ctx.framework = "qwik";
|
|
5365
|
+
} else if (has("@remix-run/react") || has("@remix-run/node")) {
|
|
5366
|
+
ctx.framework = "remix";
|
|
5367
|
+
} else if (has("astro")) {
|
|
5368
|
+
ctx.framework = "astro";
|
|
5369
|
+
} else if (has("solid-js")) {
|
|
5370
|
+
ctx.framework = "solid";
|
|
5278
5371
|
} else if (has("next")) {
|
|
5279
5372
|
ctx.framework = "next";
|
|
5280
5373
|
} else if (has("react")) {
|
|
@@ -5301,6 +5394,12 @@ async function loadFrontendContext(projectRoot) {
|
|
|
5301
5394
|
if (ctx.framework === "next") {
|
|
5302
5395
|
const hasAppDir = await fs7.pathExists(path5.join(projectRoot, "app"));
|
|
5303
5396
|
ctx.routingPattern = hasAppDir ? "next-app-router" : "next-pages-router";
|
|
5397
|
+
} else if (ctx.framework === "sveltekit") {
|
|
5398
|
+
ctx.routingPattern = "sveltekit-file-router";
|
|
5399
|
+
} else if (ctx.framework === "remix") {
|
|
5400
|
+
ctx.routingPattern = "remix-file-router";
|
|
5401
|
+
} else if (ctx.framework === "astro") {
|
|
5402
|
+
ctx.routingPattern = "astro-file-router";
|
|
5304
5403
|
} else {
|
|
5305
5404
|
for (const [lib, label] of ROUTING_LIBS) {
|
|
5306
5405
|
if (has(lib)) {
|
|
@@ -5686,13 +5785,14 @@ function getActiveSnapshot() {
|
|
|
5686
5785
|
|
|
5687
5786
|
// core/run-logger.ts
|
|
5688
5787
|
var fs9 = __toESM(require("fs-extra"));
|
|
5689
|
-
var
|
|
5788
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
5690
5789
|
var _activeLogger = null;
|
|
5691
5790
|
function getActiveLogger() {
|
|
5692
5791
|
return _activeLogger;
|
|
5693
5792
|
}
|
|
5694
5793
|
|
|
5695
|
-
// core/
|
|
5794
|
+
// core/codegen/helpers.ts
|
|
5795
|
+
var import_child_process = require("child_process");
|
|
5696
5796
|
function buildSharedConfigSection(context) {
|
|
5697
5797
|
if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
|
|
5698
5798
|
const lines = [
|
|
@@ -5824,7 +5924,7 @@ function buildGeneratedFilesSection(cache) {
|
|
|
5824
5924
|
}
|
|
5825
5925
|
function isRtkAvailable() {
|
|
5826
5926
|
try {
|
|
5827
|
-
(0, import_child_process.execSync)("rtk --version", { stdio: "ignore" });
|
|
5927
|
+
(0, import_child_process.execSync)("rtk --version", { stdio: "ignore", timeout: 1e4 });
|
|
5828
5928
|
return true;
|
|
5829
5929
|
} catch {
|
|
5830
5930
|
return false;
|
|
@@ -5848,6 +5948,73 @@ function parseJsonArray(text) {
|
|
|
5848
5948
|
}
|
|
5849
5949
|
return [];
|
|
5850
5950
|
}
|
|
5951
|
+
|
|
5952
|
+
// core/codegen/topo-sort.ts
|
|
5953
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
5954
|
+
function topoSortLayerTasks(tasks) {
|
|
5955
|
+
if (tasks.length <= 1) return [tasks];
|
|
5956
|
+
const idSet = new Set(tasks.map((t) => t.id));
|
|
5957
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
5958
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
5959
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
5960
|
+
for (const task of tasks) {
|
|
5961
|
+
inDegree.set(task.id, 0);
|
|
5962
|
+
dependents.set(task.id, []);
|
|
5963
|
+
}
|
|
5964
|
+
for (const task of tasks) {
|
|
5965
|
+
const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
|
|
5966
|
+
inDegree.set(task.id, intraDeps.length);
|
|
5967
|
+
for (const dep of intraDeps) {
|
|
5968
|
+
dependents.get(dep).push(task.id);
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
const batches = [];
|
|
5972
|
+
const remaining = new Set(tasks.map((t) => t.id));
|
|
5973
|
+
while (remaining.size > 0) {
|
|
5974
|
+
const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
|
|
5975
|
+
if (batch.length === 0) {
|
|
5976
|
+
batches.push([...remaining].map((id) => taskById.get(id)));
|
|
5977
|
+
break;
|
|
5978
|
+
}
|
|
5979
|
+
batches.push(batch);
|
|
5980
|
+
for (const task of batch) {
|
|
5981
|
+
remaining.delete(task.id);
|
|
5982
|
+
for (const dependent of dependents.get(task.id)) {
|
|
5983
|
+
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
return batches;
|
|
5988
|
+
}
|
|
5989
|
+
var LAYER_ICONS = {
|
|
5990
|
+
data: "\u{1F4BE}",
|
|
5991
|
+
infra: "\u2699\uFE0F ",
|
|
5992
|
+
service: "\u{1F527}",
|
|
5993
|
+
api: "\u{1F310}",
|
|
5994
|
+
view: "\u{1F5A5}\uFE0F ",
|
|
5995
|
+
route: "\u{1F5FA}\uFE0F ",
|
|
5996
|
+
test: "\u{1F9EA}"
|
|
5997
|
+
};
|
|
5998
|
+
function printTaskProgress(completed, total, task, mode) {
|
|
5999
|
+
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
6000
|
+
const barWidth = 20;
|
|
6001
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
6002
|
+
const bar = import_chalk9.default.green("\u2588".repeat(filled)) + import_chalk9.default.gray("\u2591".repeat(barWidth - filled));
|
|
6003
|
+
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
6004
|
+
if (mode === "skip") {
|
|
6005
|
+
console.log(
|
|
6006
|
+
import_chalk9.default.gray(`
|
|
6007
|
+
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
6008
|
+
);
|
|
6009
|
+
} else {
|
|
6010
|
+
console.log(
|
|
6011
|
+
import_chalk9.default.bold(`
|
|
6012
|
+
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
6013
|
+
);
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
// core/code-generator.ts
|
|
5851
6018
|
var CodeGenerator = class {
|
|
5852
6019
|
constructor(provider, mode = "claude-code") {
|
|
5853
6020
|
this.provider = provider;
|
|
@@ -5858,13 +6025,13 @@ var CodeGenerator = class {
|
|
|
5858
6025
|
let effectiveMode = this.mode;
|
|
5859
6026
|
if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
|
|
5860
6027
|
console.log(
|
|
5861
|
-
|
|
6028
|
+
import_chalk10.default.yellow(
|
|
5862
6029
|
`
|
|
5863
6030
|
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
5864
6031
|
)
|
|
5865
6032
|
);
|
|
5866
|
-
console.log(
|
|
5867
|
-
console.log(
|
|
6033
|
+
console.log(import_chalk10.default.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
6034
|
+
console.log(import_chalk10.default.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
5868
6035
|
`));
|
|
5869
6036
|
effectiveMode = "api";
|
|
5870
6037
|
}
|
|
@@ -5882,23 +6049,23 @@ var CodeGenerator = class {
|
|
|
5882
6049
|
// ── Mode: claude-code ──────────────────────────────────────────────────────
|
|
5883
6050
|
isClaudeCLIAvailable() {
|
|
5884
6051
|
try {
|
|
5885
|
-
(0,
|
|
6052
|
+
(0, import_child_process2.execSync)("claude --version", { stdio: "ignore", timeout: 1e4 });
|
|
5886
6053
|
return true;
|
|
5887
6054
|
} catch {
|
|
5888
6055
|
return false;
|
|
5889
6056
|
}
|
|
5890
6057
|
}
|
|
5891
6058
|
async runClaudeCode(specFilePath, workingDir, options = {}) {
|
|
5892
|
-
console.log(
|
|
6059
|
+
console.log(import_chalk10.default.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5893
6060
|
if (!this.isClaudeCLIAvailable()) {
|
|
5894
|
-
console.log(
|
|
5895
|
-
console.log(
|
|
6061
|
+
console.log(import_chalk10.default.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
|
|
6062
|
+
console.log(import_chalk10.default.gray(" Install: npm install -g @anthropic-ai/claude-code"));
|
|
5896
6063
|
return this.runPlanMode(specFilePath);
|
|
5897
6064
|
}
|
|
5898
6065
|
const rtkAvailable = isRtkAvailable();
|
|
5899
6066
|
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
5900
6067
|
if (rtkAvailable) {
|
|
5901
|
-
console.log(
|
|
6068
|
+
console.log(import_chalk10.default.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
5902
6069
|
}
|
|
5903
6070
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5904
6071
|
if (options.auto && tasks && tasks.length > 0) {
|
|
@@ -5914,28 +6081,28 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5914
6081
|
const promptFile = path6.join(workingDir, ".claude-prompt.txt");
|
|
5915
6082
|
await fs10.writeFile(promptFile, promptContent, "utf-8");
|
|
5916
6083
|
if (options.auto) {
|
|
5917
|
-
console.log(
|
|
5918
|
-
console.log(
|
|
6084
|
+
console.log(import_chalk10.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6085
|
+
console.log(import_chalk10.default.gray(` Spec: ${specFilePath}`));
|
|
5919
6086
|
try {
|
|
5920
|
-
(0,
|
|
6087
|
+
(0, import_child_process2.spawnSync)(claudeCmd, ["-p", promptContent], {
|
|
5921
6088
|
cwd: workingDir,
|
|
5922
6089
|
stdio: "inherit",
|
|
5923
6090
|
shell: false
|
|
5924
6091
|
});
|
|
5925
|
-
console.log(
|
|
6092
|
+
console.log(import_chalk10.default.green("\n \u2714 Claude Code completed."));
|
|
5926
6093
|
} catch {
|
|
5927
|
-
console.log(
|
|
6094
|
+
console.log(import_chalk10.default.yellow("\n Claude Code exited. Check output above."));
|
|
5928
6095
|
}
|
|
5929
6096
|
} else {
|
|
5930
|
-
console.log(
|
|
5931
|
-
console.log(
|
|
5932
|
-
if (tasks) console.log(
|
|
5933
|
-
console.log(
|
|
6097
|
+
console.log(import_chalk10.default.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
|
|
6098
|
+
console.log(import_chalk10.default.gray(` Spec: ${specFilePath}`));
|
|
6099
|
+
if (tasks) console.log(import_chalk10.default.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
|
|
6100
|
+
console.log(import_chalk10.default.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
|
|
5934
6101
|
try {
|
|
5935
|
-
(0,
|
|
5936
|
-
console.log(
|
|
6102
|
+
(0, import_child_process2.execSync)(claudeCmd, { cwd: workingDir, stdio: "inherit" });
|
|
6103
|
+
console.log(import_chalk10.default.green("\n \u2714 Claude Code session completed."));
|
|
5937
6104
|
} catch {
|
|
5938
|
-
console.log(
|
|
6105
|
+
console.log(import_chalk10.default.yellow("\n Claude Code session ended. Continuing workflow."));
|
|
5939
6106
|
}
|
|
5940
6107
|
}
|
|
5941
6108
|
}
|
|
@@ -5948,10 +6115,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5948
6115
|
const pending = tasks.filter((t) => t.status !== "done");
|
|
5949
6116
|
const doneCount = tasks.length - pending.length;
|
|
5950
6117
|
if (options.resume && doneCount > 0) {
|
|
5951
|
-
console.log(
|
|
6118
|
+
console.log(import_chalk10.default.cyan(`
|
|
5952
6119
|
Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
|
|
5953
6120
|
} else {
|
|
5954
|
-
console.log(
|
|
6121
|
+
console.log(import_chalk10.default.cyan(`
|
|
5955
6122
|
Incremental mode: ${tasks.length} tasks`));
|
|
5956
6123
|
}
|
|
5957
6124
|
let completed = doneCount;
|
|
@@ -5972,7 +6139,7 @@ Full spec is at: ${specFilePath}
|
|
|
5972
6139
|
Implement ONLY this task. Do not implement other tasks.`;
|
|
5973
6140
|
let taskStatus = "done";
|
|
5974
6141
|
try {
|
|
5975
|
-
(0,
|
|
6142
|
+
(0, import_child_process2.spawnSync)(claudeCmd, ["-p", taskPrompt], {
|
|
5976
6143
|
cwd: workingDir,
|
|
5977
6144
|
stdio: "inherit",
|
|
5978
6145
|
shell: false
|
|
@@ -5980,33 +6147,33 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
5980
6147
|
completed++;
|
|
5981
6148
|
} catch {
|
|
5982
6149
|
taskStatus = "failed";
|
|
5983
|
-
console.log(
|
|
6150
|
+
console.log(import_chalk10.default.yellow(`
|
|
5984
6151
|
\u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
|
|
5985
6152
|
}
|
|
5986
6153
|
await updateTaskStatus(specFilePath, task.id, taskStatus);
|
|
5987
6154
|
}
|
|
5988
6155
|
const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
|
|
5989
6156
|
console.log(
|
|
5990
|
-
|
|
6157
|
+
import_chalk10.default.bold(
|
|
5991
6158
|
`
|
|
5992
|
-
${successCount === tasks.length ?
|
|
6159
|
+
${successCount === tasks.length ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
|
|
5993
6160
|
)
|
|
5994
6161
|
);
|
|
5995
6162
|
}
|
|
5996
6163
|
// ── Mode: api ─────────────────────────────────────────────────────────────
|
|
5997
6164
|
async runApiMode(specFilePath, workingDir, context, options = {}) {
|
|
5998
6165
|
console.log(
|
|
5999
|
-
|
|
6166
|
+
import_chalk10.default.blue(
|
|
6000
6167
|
`
|
|
6001
6168
|
\u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
|
|
6002
6169
|
)
|
|
6003
6170
|
);
|
|
6004
6171
|
const systemPrompt = getCodeGenSystemPrompt(options.repoType);
|
|
6005
6172
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
6006
|
-
console.log(
|
|
6173
|
+
console.log(import_chalk10.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
6007
6174
|
}
|
|
6008
6175
|
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
6009
|
-
|
|
6176
|
+
let constitutionSection = context?.constitution ? `
|
|
6010
6177
|
=== Project Constitution (MUST follow) ===
|
|
6011
6178
|
${context.constitution}
|
|
6012
6179
|
` : "";
|
|
@@ -6021,7 +6188,7 @@ ${buildDslContextSection(dsl)}
|
|
|
6021
6188
|
if (dsl) {
|
|
6022
6189
|
const cmpCount = dsl.components?.length ?? 0;
|
|
6023
6190
|
const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
|
|
6024
|
-
console.log(
|
|
6191
|
+
console.log(import_chalk10.default.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
|
|
6025
6192
|
}
|
|
6026
6193
|
const isFrontend = isFrontendDeps(context?.dependencies ?? []);
|
|
6027
6194
|
let frontendSection = "";
|
|
@@ -6030,13 +6197,30 @@ ${buildDslContextSection(dsl)}
|
|
|
6030
6197
|
frontendSection = `
|
|
6031
6198
|
${buildFrontendContextSection(fctx)}
|
|
6032
6199
|
`;
|
|
6033
|
-
console.log(
|
|
6200
|
+
console.log(import_chalk10.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
6201
|
+
}
|
|
6202
|
+
const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
|
|
6203
|
+
const estimatedTokenCount = estimateTokens(allContextText);
|
|
6204
|
+
const budget = getDefaultBudget(this.provider.providerName);
|
|
6205
|
+
if (estimatedTokenCount > budget * 0.7) {
|
|
6206
|
+
console.log(
|
|
6207
|
+
import_chalk10.default.yellow(
|
|
6208
|
+
` \u26A0 Context size: ~${Math.round(estimatedTokenCount / 1e3)}K tokens (budget: ${Math.round(budget / 1e3)}K for ${this.provider.providerName})`
|
|
6209
|
+
)
|
|
6210
|
+
);
|
|
6211
|
+
if (constitutionSection.length > 4e3) {
|
|
6212
|
+
const s9Start = constitutionSection.indexOf("## 9.");
|
|
6213
|
+
if (s9Start > 0) {
|
|
6214
|
+
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";
|
|
6215
|
+
console.log(import_chalk10.default.gray(" \u2192 \xA79 trimmed from constitution to save tokens."));
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6034
6218
|
}
|
|
6035
6219
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
6036
6220
|
if (tasks && tasks.length > 0) {
|
|
6037
6221
|
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6038
6222
|
}
|
|
6039
|
-
console.log(
|
|
6223
|
+
console.log(import_chalk10.default.gray(" [1/2] Planning implementation files..."));
|
|
6040
6224
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
6041
6225
|
|
|
6042
6226
|
IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
|
|
@@ -6059,18 +6243,18 @@ Output ONLY a valid JSON array:
|
|
|
6059
6243
|
const planResponse = await this.provider.generate(planPrompt, systemPrompt);
|
|
6060
6244
|
filePlan = parseJsonArray(planResponse);
|
|
6061
6245
|
} catch (err) {
|
|
6062
|
-
console.error(
|
|
6246
|
+
console.error(import_chalk10.default.red(" Failed to generate file plan:"), err);
|
|
6063
6247
|
}
|
|
6064
6248
|
if (filePlan.length === 0) {
|
|
6065
|
-
console.log(
|
|
6249
|
+
console.log(import_chalk10.default.yellow(" Could not determine file plan. Falling back to plan mode."));
|
|
6066
6250
|
await this.runPlanMode(specFilePath);
|
|
6067
6251
|
return [];
|
|
6068
6252
|
}
|
|
6069
|
-
console.log(
|
|
6253
|
+
console.log(import_chalk10.default.cyan(`
|
|
6070
6254
|
Plan: ${filePlan.length} file(s) to process`));
|
|
6071
6255
|
filePlan.forEach((item) => {
|
|
6072
|
-
const icon = item.action === "create" ?
|
|
6073
|
-
console.log(` ${icon} ${item.file}: ${
|
|
6256
|
+
const icon = item.action === "create" ? import_chalk10.default.green("+") : import_chalk10.default.yellow("~");
|
|
6257
|
+
console.log(` ${icon} ${item.file}: ${import_chalk10.default.gray(item.description)}`);
|
|
6074
6258
|
});
|
|
6075
6259
|
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
6076
6260
|
return files;
|
|
@@ -6079,13 +6263,13 @@ Output ONLY a valid JSON array:
|
|
|
6079
6263
|
const pendingTasks = tasks.filter((t) => t.status !== "done");
|
|
6080
6264
|
const doneCount = tasks.length - pendingTasks.length;
|
|
6081
6265
|
if (options.resume && doneCount > 0) {
|
|
6082
|
-
console.log(
|
|
6083
|
-
Task-based generation (resume): ${tasks.length} tasks (${
|
|
6266
|
+
console.log(import_chalk10.default.cyan(`
|
|
6267
|
+
Task-based generation (resume): ${tasks.length} tasks (${import_chalk10.default.green(doneCount + " already done")}, skipping)`));
|
|
6084
6268
|
} else if (doneCount > 0) {
|
|
6085
|
-
console.log(
|
|
6086
|
-
Task-based generation: ${tasks.length} tasks (${
|
|
6269
|
+
console.log(import_chalk10.default.cyan(`
|
|
6270
|
+
Task-based generation: ${tasks.length} tasks (${import_chalk10.default.green(doneCount + " already done")}, resuming from checkpoint)`));
|
|
6087
6271
|
} else {
|
|
6088
|
-
console.log(
|
|
6272
|
+
console.log(import_chalk10.default.cyan(`
|
|
6089
6273
|
Task-based generation: ${tasks.length} tasks`));
|
|
6090
6274
|
}
|
|
6091
6275
|
const sharedConfigPaths = new Set(
|
|
@@ -6117,9 +6301,9 @@ Output ONLY a valid JSON array:
|
|
|
6117
6301
|
const pct = Math.round(completedTasks / tasks.length * 100);
|
|
6118
6302
|
const barWidth = 20;
|
|
6119
6303
|
const filled = Math.round(pct / 100 * barWidth);
|
|
6120
|
-
const bar =
|
|
6304
|
+
const bar = import_chalk10.default.green("\u2588".repeat(filled)) + import_chalk10.default.gray("\u2591".repeat(barWidth - filled));
|
|
6121
6305
|
console.log(
|
|
6122
|
-
|
|
6306
|
+
import_chalk10.default.bold(`
|
|
6123
6307
|
[${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
|
|
6124
6308
|
);
|
|
6125
6309
|
} else {
|
|
@@ -6127,7 +6311,7 @@ Output ONLY a valid JSON array:
|
|
|
6127
6311
|
}
|
|
6128
6312
|
const executeTask = async (task, batchIsParallel) => {
|
|
6129
6313
|
if (task.filesToTouch.length === 0) {
|
|
6130
|
-
if (!batchIsParallel) console.log(
|
|
6314
|
+
if (!batchIsParallel) console.log(import_chalk10.default.gray(" No files specified, skipping."));
|
|
6131
6315
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
6132
6316
|
}
|
|
6133
6317
|
const filePlan = await Promise.all(
|
|
@@ -6184,13 +6368,19 @@ ${taskContext}`,
|
|
|
6184
6368
|
const layerResults = [];
|
|
6185
6369
|
for (const batch of taskBatches) {
|
|
6186
6370
|
const batchIsParallel = batch.length > 1;
|
|
6187
|
-
const batchResultPromises = batch.map(
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6371
|
+
const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
|
|
6372
|
+
const settled = await Promise.allSettled(batchResultPromises);
|
|
6373
|
+
const batchResults = [];
|
|
6374
|
+
for (let i = 0; i < settled.length; i++) {
|
|
6375
|
+
const outcome = settled[i];
|
|
6376
|
+
if (outcome.status === "fulfilled") {
|
|
6377
|
+
batchResults.push(outcome.value);
|
|
6378
|
+
} else {
|
|
6379
|
+
const task = batch[i];
|
|
6380
|
+
console.log(import_chalk10.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
|
|
6381
|
+
batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6194
6384
|
layerResults.push(...batchResults);
|
|
6195
6385
|
await updateCacheFromBatch(batchResults);
|
|
6196
6386
|
}
|
|
@@ -6202,14 +6392,14 @@ ${taskContext}`,
|
|
|
6202
6392
|
totalFiles += result.total;
|
|
6203
6393
|
allGeneratedFiles.push(...result.files);
|
|
6204
6394
|
if (isParallel) {
|
|
6205
|
-
const icon = result.success === result.total ?
|
|
6395
|
+
const icon = result.success === result.total ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!");
|
|
6206
6396
|
const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
|
|
6207
6397
|
console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
|
|
6208
6398
|
}
|
|
6209
6399
|
const taskStatus = result.success === result.total ? "done" : "failed";
|
|
6210
6400
|
await updateTaskStatus(specFilePath, result.task.id, taskStatus);
|
|
6211
6401
|
if (taskStatus === "failed") {
|
|
6212
|
-
console.log(
|
|
6402
|
+
console.log(import_chalk10.default.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
|
|
6213
6403
|
}
|
|
6214
6404
|
}
|
|
6215
6405
|
completedTasks += layerTasks.length;
|
|
@@ -6224,7 +6414,7 @@ ${taskContext}`,
|
|
|
6224
6414
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
6225
6415
|
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.`;
|
|
6226
6416
|
}
|
|
6227
|
-
console.log(
|
|
6417
|
+
console.log(import_chalk10.default.gray(`
|
|
6228
6418
|
+ updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
|
|
6229
6419
|
const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
6230
6420
|
await this.generateFiles(
|
|
@@ -6242,17 +6432,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
6242
6432
|
}
|
|
6243
6433
|
}
|
|
6244
6434
|
console.log(
|
|
6245
|
-
|
|
6435
|
+
import_chalk10.default.bold(
|
|
6246
6436
|
`
|
|
6247
|
-
${totalSuccess === totalFiles ?
|
|
6437
|
+
${totalSuccess === totalFiles ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
|
|
6248
6438
|
)
|
|
6249
6439
|
);
|
|
6250
6440
|
return allGeneratedFiles;
|
|
6251
6441
|
}
|
|
6252
6442
|
async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
|
|
6253
|
-
const prefix = taskLabel ? ` [${
|
|
6443
|
+
const prefix = taskLabel ? ` [${import_chalk10.default.cyan(taskLabel)}] ` : " ";
|
|
6254
6444
|
if (!taskLabel) {
|
|
6255
|
-
console.log(
|
|
6445
|
+
console.log(import_chalk10.default.gray(`
|
|
6256
6446
|
Generating ${filePlan.length} file(s)...`));
|
|
6257
6447
|
}
|
|
6258
6448
|
let successCount = 0;
|
|
@@ -6280,17 +6470,17 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6280
6470
|
await fs10.ensureDir(path6.dirname(fullPath));
|
|
6281
6471
|
await fs10.writeFile(fullPath, fileContent, "utf-8");
|
|
6282
6472
|
getActiveLogger()?.fileWritten(item.file);
|
|
6283
|
-
console.log(`${prefix}${existingContent ?
|
|
6473
|
+
console.log(`${prefix}${existingContent ? import_chalk10.default.yellow("~") : import_chalk10.default.green("+")} ${import_chalk10.default.bold(item.file)} ${import_chalk10.default.green("\u2714")}`);
|
|
6284
6474
|
successCount++;
|
|
6285
6475
|
writtenFiles.push(item.file);
|
|
6286
6476
|
} catch (err) {
|
|
6287
|
-
console.log(`${prefix}${
|
|
6477
|
+
console.log(`${prefix}${import_chalk10.default.red("\u2718")} ${import_chalk10.default.bold(item.file)} \u2014 ${import_chalk10.default.red(err.message)}`);
|
|
6288
6478
|
}
|
|
6289
6479
|
}
|
|
6290
6480
|
if (!taskLabel) {
|
|
6291
6481
|
console.log(
|
|
6292
|
-
|
|
6293
|
-
` ${successCount === filePlan.length ?
|
|
6482
|
+
import_chalk10.default.bold(
|
|
6483
|
+
` ${successCount === filePlan.length ? import_chalk10.default.green("\u2714") : import_chalk10.default.yellow("!")} ${successCount}/${filePlan.length} files written.`
|
|
6294
6484
|
)
|
|
6295
6485
|
);
|
|
6296
6486
|
}
|
|
@@ -6298,7 +6488,7 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6298
6488
|
}
|
|
6299
6489
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
6300
6490
|
async runPlanMode(specFilePath) {
|
|
6301
|
-
console.log(
|
|
6491
|
+
console.log(import_chalk10.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6302
6492
|
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
6303
6493
|
const plan = await this.provider.generate(
|
|
6304
6494
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
@@ -6311,80 +6501,18 @@ Be specific about:
|
|
|
6311
6501
|
${spec}`,
|
|
6312
6502
|
"You are a senior developer creating an actionable implementation guide."
|
|
6313
6503
|
);
|
|
6314
|
-
console.log(
|
|
6504
|
+
console.log(import_chalk10.default.cyan("\n") + plan);
|
|
6315
6505
|
}
|
|
6316
6506
|
};
|
|
6317
|
-
function topoSortLayerTasks(tasks) {
|
|
6318
|
-
if (tasks.length <= 1) return [tasks];
|
|
6319
|
-
const idSet = new Set(tasks.map((t) => t.id));
|
|
6320
|
-
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
6321
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
6322
|
-
const dependents = /* @__PURE__ */ new Map();
|
|
6323
|
-
for (const task of tasks) {
|
|
6324
|
-
inDegree.set(task.id, 0);
|
|
6325
|
-
dependents.set(task.id, []);
|
|
6326
|
-
}
|
|
6327
|
-
for (const task of tasks) {
|
|
6328
|
-
const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
|
|
6329
|
-
inDegree.set(task.id, intraDeps.length);
|
|
6330
|
-
for (const dep of intraDeps) {
|
|
6331
|
-
dependents.get(dep).push(task.id);
|
|
6332
|
-
}
|
|
6333
|
-
}
|
|
6334
|
-
const batches = [];
|
|
6335
|
-
const remaining = new Set(tasks.map((t) => t.id));
|
|
6336
|
-
while (remaining.size > 0) {
|
|
6337
|
-
const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
|
|
6338
|
-
if (batch.length === 0) {
|
|
6339
|
-
batches.push([...remaining].map((id) => taskById.get(id)));
|
|
6340
|
-
break;
|
|
6341
|
-
}
|
|
6342
|
-
batches.push(batch);
|
|
6343
|
-
for (const task of batch) {
|
|
6344
|
-
remaining.delete(task.id);
|
|
6345
|
-
for (const dependent of dependents.get(task.id)) {
|
|
6346
|
-
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
6347
|
-
}
|
|
6348
|
-
}
|
|
6349
|
-
}
|
|
6350
|
-
return batches;
|
|
6351
|
-
}
|
|
6352
|
-
var LAYER_ICONS = {
|
|
6353
|
-
data: "\u{1F4BE}",
|
|
6354
|
-
infra: "\u2699\uFE0F ",
|
|
6355
|
-
service: "\u{1F527}",
|
|
6356
|
-
api: "\u{1F310}",
|
|
6357
|
-
view: "\u{1F5A5}\uFE0F ",
|
|
6358
|
-
route: "\u{1F5FA}\uFE0F ",
|
|
6359
|
-
test: "\u{1F9EA}"
|
|
6360
|
-
};
|
|
6361
|
-
function printTaskProgress(completed, total, task, mode) {
|
|
6362
|
-
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
6363
|
-
const barWidth = 20;
|
|
6364
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
6365
|
-
const bar = import_chalk8.default.green("\u2588".repeat(filled)) + import_chalk8.default.gray("\u2591".repeat(barWidth - filled));
|
|
6366
|
-
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
6367
|
-
if (mode === "skip") {
|
|
6368
|
-
console.log(
|
|
6369
|
-
import_chalk8.default.gray(`
|
|
6370
|
-
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
6371
|
-
);
|
|
6372
|
-
} else {
|
|
6373
|
-
console.log(
|
|
6374
|
-
import_chalk8.default.bold(`
|
|
6375
|
-
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
6376
|
-
);
|
|
6377
|
-
}
|
|
6378
|
-
}
|
|
6379
6507
|
|
|
6380
6508
|
// core/reviewer.ts
|
|
6381
|
-
var
|
|
6382
|
-
var
|
|
6509
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
6510
|
+
var import_child_process3 = require("child_process");
|
|
6383
6511
|
var path8 = __toESM(require("path"));
|
|
6384
6512
|
var fs12 = __toESM(require("fs-extra"));
|
|
6385
6513
|
|
|
6386
6514
|
// core/constitution-generator.ts
|
|
6387
|
-
var
|
|
6515
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
6388
6516
|
var fs11 = __toESM(require("fs-extra"));
|
|
6389
6517
|
var path7 = __toESM(require("path"));
|
|
6390
6518
|
|
|
@@ -6534,7 +6662,7 @@ async function loadConstitution(projectRoot) {
|
|
|
6534
6662
|
function printConstitutionHint(exists) {
|
|
6535
6663
|
if (!exists) {
|
|
6536
6664
|
console.log(
|
|
6537
|
-
|
|
6665
|
+
import_chalk11.default.yellow(
|
|
6538
6666
|
" \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
|
|
6539
6667
|
)
|
|
6540
6668
|
);
|
|
@@ -6618,16 +6746,16 @@ var CodeReviewer = class {
|
|
|
6618
6746
|
this.projectRoot = projectRoot;
|
|
6619
6747
|
}
|
|
6620
6748
|
getGitDiff() {
|
|
6621
|
-
const silent = { encoding: "utf-8", stdio: "pipe" };
|
|
6749
|
+
const silent = { encoding: "utf-8", stdio: "pipe", cwd: this.projectRoot, timeout: 3e4 };
|
|
6622
6750
|
try {
|
|
6623
|
-
(0,
|
|
6751
|
+
(0, import_child_process3.execSync)("git rev-parse --is-inside-work-tree", silent);
|
|
6624
6752
|
} catch {
|
|
6625
6753
|
return "";
|
|
6626
6754
|
}
|
|
6627
6755
|
try {
|
|
6628
|
-
let diff = (0,
|
|
6629
|
-
if (!diff.trim()) diff = (0,
|
|
6630
|
-
if (!diff.trim()) diff = (0,
|
|
6756
|
+
let diff = (0, import_child_process3.execSync)("git diff --cached", silent);
|
|
6757
|
+
if (!diff.trim()) diff = (0, import_child_process3.execSync)("git diff HEAD", silent);
|
|
6758
|
+
if (!diff.trim()) diff = (0, import_child_process3.execSync)("git diff", silent);
|
|
6631
6759
|
return diff;
|
|
6632
6760
|
} catch {
|
|
6633
6761
|
return "";
|
|
@@ -6652,7 +6780,7 @@ var CodeReviewer = class {
|
|
|
6652
6780
|
async runThreePassReview(specContent, codeContext, specFile) {
|
|
6653
6781
|
let complianceReview = "";
|
|
6654
6782
|
if (specContent && specContent.trim() && specContent !== "(No spec \u2014 review for general code quality)") {
|
|
6655
|
-
console.log(
|
|
6783
|
+
console.log(import_chalk12.default.gray(" Pass 0/3: Spec compliance check..."));
|
|
6656
6784
|
const compliancePrompt = `Check whether the implementation covers every requirement in the spec.
|
|
6657
6785
|
|
|
6658
6786
|
=== Feature Spec ===
|
|
@@ -6664,13 +6792,13 @@ ${codeContext}`;
|
|
|
6664
6792
|
const complianceScore2 = extractComplianceScore(complianceReview);
|
|
6665
6793
|
const missingCount = extractMissingCount(complianceReview);
|
|
6666
6794
|
if (complianceScore2 > 0) {
|
|
6667
|
-
const scoreColor = complianceScore2 >= 8 ?
|
|
6795
|
+
const scoreColor = complianceScore2 >= 8 ? import_chalk12.default.green : complianceScore2 >= 6 ? import_chalk12.default.yellow : import_chalk12.default.red;
|
|
6668
6796
|
console.log(
|
|
6669
|
-
|
|
6797
|
+
import_chalk12.default.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? import_chalk12.default.red(` \xB7 ${missingCount} missing requirement(s)`) : import_chalk12.default.green(" \xB7 all requirements covered"))
|
|
6670
6798
|
);
|
|
6671
6799
|
}
|
|
6672
6800
|
}
|
|
6673
|
-
console.log(
|
|
6801
|
+
console.log(import_chalk12.default.gray(` Pass 1/3: Architecture review...`));
|
|
6674
6802
|
const accumulatedLessons = await loadAccumulatedLessons(this.projectRoot);
|
|
6675
6803
|
const archPrompt = `Review the architecture of this change.
|
|
6676
6804
|
${complianceReview ? `
|
|
@@ -6687,7 +6815,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
6687
6815
|
=== Code ===
|
|
6688
6816
|
${codeContext}`;
|
|
6689
6817
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
6690
|
-
console.log(
|
|
6818
|
+
console.log(import_chalk12.default.gray(" Pass 2/3: Implementation review..."));
|
|
6691
6819
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6692
6820
|
const historyContext = buildHistoryContext(history);
|
|
6693
6821
|
const implPrompt = `Review the implementation details of this change.
|
|
@@ -6702,7 +6830,7 @@ ${codeContext}
|
|
|
6702
6830
|
${archReview}
|
|
6703
6831
|
${historyContext}`;
|
|
6704
6832
|
const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
|
|
6705
|
-
console.log(
|
|
6833
|
+
console.log(import_chalk12.default.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6706
6834
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6707
6835
|
|
|
6708
6836
|
=== Feature Spec ===
|
|
@@ -6743,37 +6871,37 @@ ${sep}
|
|
|
6743
6871
|
return combined;
|
|
6744
6872
|
}
|
|
6745
6873
|
async reviewCode(specContent, specFile) {
|
|
6746
|
-
console.log(
|
|
6874
|
+
console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6747
6875
|
const diff = this.getGitDiff();
|
|
6748
6876
|
if (!diff.trim()) {
|
|
6749
6877
|
console.log(
|
|
6750
|
-
|
|
6878
|
+
import_chalk12.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
|
|
6751
6879
|
);
|
|
6752
|
-
console.log(
|
|
6880
|
+
console.log(import_chalk12.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
|
|
6753
6881
|
return "No changes";
|
|
6754
6882
|
}
|
|
6755
6883
|
const { files, added, removed } = this.getDiffStats(diff);
|
|
6756
6884
|
console.log(
|
|
6757
|
-
|
|
6885
|
+
import_chalk12.default.gray(` Diff: ${files} file(s), ${import_chalk12.default.green("+" + added)} ${import_chalk12.default.red("-" + removed)}`)
|
|
6758
6886
|
);
|
|
6759
6887
|
console.log(
|
|
6760
|
-
|
|
6888
|
+
import_chalk12.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6761
6889
|
);
|
|
6762
6890
|
const codeContext = diff.slice(0, 1e4);
|
|
6763
6891
|
const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
|
|
6764
|
-
console.log(
|
|
6892
|
+
console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6765
6893
|
console.log(reviewResult);
|
|
6766
|
-
console.log(
|
|
6894
|
+
console.log(import_chalk12.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6767
6895
|
return reviewResult;
|
|
6768
6896
|
}
|
|
6769
6897
|
/**
|
|
6770
6898
|
* Review directly from generated file contents (for api mode where git diff is empty).
|
|
6771
6899
|
*/
|
|
6772
6900
|
async reviewFiles(specContent, filePaths, workingDir, specFile) {
|
|
6773
|
-
console.log(
|
|
6774
|
-
console.log(
|
|
6901
|
+
console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6902
|
+
console.log(import_chalk12.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
|
|
6775
6903
|
console.log(
|
|
6776
|
-
|
|
6904
|
+
import_chalk12.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6777
6905
|
);
|
|
6778
6906
|
let filesSection = "";
|
|
6779
6907
|
for (const filePath of filePaths) {
|
|
@@ -6794,28 +6922,28 @@ ${content.slice(0, 3e3)}`;
|
|
|
6794
6922
|
}
|
|
6795
6923
|
}
|
|
6796
6924
|
const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
|
|
6797
|
-
console.log(
|
|
6925
|
+
console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6798
6926
|
console.log(reviewResult);
|
|
6799
|
-
console.log(
|
|
6927
|
+
console.log(import_chalk12.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6800
6928
|
return reviewResult;
|
|
6801
6929
|
}
|
|
6802
6930
|
/** Print score trend from history (last N reviews) */
|
|
6803
6931
|
async printScoreTrend(limit = 5) {
|
|
6804
6932
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6805
6933
|
if (history.length === 0) {
|
|
6806
|
-
console.log(
|
|
6934
|
+
console.log(import_chalk12.default.gray(" No review history yet."));
|
|
6807
6935
|
return;
|
|
6808
6936
|
}
|
|
6809
6937
|
const recent = history.slice(-limit);
|
|
6810
|
-
console.log(
|
|
6938
|
+
console.log(import_chalk12.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6811
6939
|
for (const entry of recent) {
|
|
6812
6940
|
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
6813
|
-
const color = entry.score >= 8 ?
|
|
6814
|
-
const impactTag = entry.impactLevel ?
|
|
6815
|
-
const complexityTag = entry.complexityLevel ?
|
|
6941
|
+
const color = entry.score >= 8 ? import_chalk12.default.green : entry.score >= 6 ? import_chalk12.default.yellow : import_chalk12.default.red;
|
|
6942
|
+
const impactTag = entry.impactLevel ? import_chalk12.default.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? import_chalk12.default.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? import_chalk12.default.yellow(entry.impactLevel) : import_chalk12.default.green(entry.impactLevel)}`) : "";
|
|
6943
|
+
const complexityTag = entry.complexityLevel ? import_chalk12.default.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? import_chalk12.default.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? import_chalk12.default.yellow(entry.complexityLevel) : import_chalk12.default.green(entry.complexityLevel)}`) : "";
|
|
6816
6944
|
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
|
|
6817
6945
|
}
|
|
6818
|
-
console.log(
|
|
6946
|
+
console.log(import_chalk12.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6819
6947
|
}
|
|
6820
6948
|
};
|
|
6821
6949
|
|
|
@@ -6865,17 +6993,17 @@ function parseSpecAndTasks(raw) {
|
|
|
6865
6993
|
}
|
|
6866
6994
|
|
|
6867
6995
|
// git/worktree.ts
|
|
6868
|
-
var
|
|
6996
|
+
var import_child_process4 = require("child_process");
|
|
6869
6997
|
var path9 = __toESM(require("path"));
|
|
6870
6998
|
var fs13 = __toESM(require("fs-extra"));
|
|
6871
|
-
var
|
|
6999
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
6872
7000
|
var GitWorktreeManager = class {
|
|
6873
7001
|
constructor(baseDir) {
|
|
6874
7002
|
this.baseDir = baseDir;
|
|
6875
7003
|
}
|
|
6876
7004
|
isGitRepo() {
|
|
6877
7005
|
try {
|
|
6878
|
-
(0,
|
|
7006
|
+
(0, import_child_process4.execSync)("git rev-parse --is-inside-work-tree", { cwd: this.baseDir, stdio: "ignore" });
|
|
6879
7007
|
return true;
|
|
6880
7008
|
} catch {
|
|
6881
7009
|
return false;
|
|
@@ -6899,58 +7027,58 @@ var GitWorktreeManager = class {
|
|
|
6899
7027
|
if (await fs13.pathExists(dest)) continue;
|
|
6900
7028
|
try {
|
|
6901
7029
|
await fs13.ensureSymlink(src, dest, "dir");
|
|
6902
|
-
console.log(
|
|
7030
|
+
console.log(import_chalk13.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
6903
7031
|
} catch (err) {
|
|
6904
|
-
console.log(
|
|
6905
|
-
console.log(
|
|
7032
|
+
console.log(import_chalk13.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
7033
|
+
console.log(import_chalk13.default.yellow(` Run \`npm install\` inside the worktree manually.`));
|
|
6906
7034
|
}
|
|
6907
7035
|
}
|
|
6908
7036
|
}
|
|
6909
7037
|
async createWorktree(idea) {
|
|
6910
7038
|
if (!this.isGitRepo()) {
|
|
6911
|
-
console.log(
|
|
7039
|
+
console.log(import_chalk13.default.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
|
|
6912
7040
|
return null;
|
|
6913
7041
|
}
|
|
6914
7042
|
const featureName = this.sanitizeFeatureName(idea);
|
|
6915
7043
|
const branchName = `feature/${featureName}`;
|
|
6916
7044
|
const repoName = path9.basename(this.baseDir);
|
|
6917
7045
|
const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
6918
|
-
console.log(
|
|
7046
|
+
console.log(import_chalk13.default.cyan(`
|
|
6919
7047
|
--- Setting up Git Worktree ---`));
|
|
6920
7048
|
if (await fs13.pathExists(worktreePath)) {
|
|
6921
|
-
console.log(
|
|
7049
|
+
console.log(import_chalk13.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
6922
7050
|
await this.linkDependencies(worktreePath);
|
|
6923
7051
|
return worktreePath;
|
|
6924
7052
|
}
|
|
6925
7053
|
try {
|
|
6926
7054
|
let branchExists = false;
|
|
6927
7055
|
try {
|
|
6928
|
-
(0,
|
|
7056
|
+
(0, import_child_process4.execSync)(`git show-ref --verify refs/heads/${branchName}`, {
|
|
6929
7057
|
cwd: this.baseDir,
|
|
6930
7058
|
stdio: "ignore"
|
|
6931
7059
|
});
|
|
6932
7060
|
branchExists = true;
|
|
6933
7061
|
} catch {
|
|
6934
7062
|
}
|
|
6935
|
-
console.log(
|
|
7063
|
+
console.log(import_chalk13.default.gray(`Creating worktree at: ${worktreePath}`));
|
|
6936
7064
|
if (branchExists) {
|
|
6937
|
-
(0,
|
|
7065
|
+
(0, import_child_process4.execSync)(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6938
7066
|
cwd: this.baseDir,
|
|
6939
7067
|
stdio: "inherit"
|
|
6940
7068
|
});
|
|
6941
7069
|
} else {
|
|
6942
|
-
(0,
|
|
7070
|
+
(0, import_child_process4.execSync)(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
6943
7071
|
cwd: this.baseDir,
|
|
6944
7072
|
stdio: "inherit"
|
|
6945
7073
|
});
|
|
6946
7074
|
}
|
|
6947
7075
|
console.log(
|
|
6948
|
-
|
|
7076
|
+
import_chalk13.default.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
|
|
6949
7077
|
);
|
|
6950
7078
|
await this.linkDependencies(worktreePath);
|
|
6951
7079
|
return worktreePath;
|
|
6952
7080
|
} catch (error) {
|
|
6953
|
-
console.error(
|
|
7081
|
+
console.error(import_chalk13.default.red("Failed to create git worktree:"), error);
|
|
6954
7082
|
return null;
|
|
6955
7083
|
}
|
|
6956
7084
|
}
|