ai-spec-dev 0.17.0 → 0.24.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/.claude/settings.local.json +1 -6
- package/README.md +79 -17
- package/RELEASE_LOG.md +426 -0
- package/cli/index.ts +200 -42
- package/core/code-generator.ts +266 -46
- package/core/error-feedback.ts +14 -6
- package/core/knowledge-memory.ts +54 -0
- package/core/mock-server-generator.ts +210 -0
- package/core/task-generator.ts +6 -2
- package/dist/cli/index.js +553 -57
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +553 -57
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +214 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +214 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +11 -3
- package/prompts/consolidate.prompt.ts +3 -1
- package/prompts/tasks.prompt.ts +28 -5
- package/purpose.md +174 -101
package/dist/cli/index.mjs
CHANGED
|
@@ -96,9 +96,17 @@ CRITICAL \u2014 Route/Store index registration (MUST follow):
|
|
|
96
96
|
|
|
97
97
|
CRITICAL \u2014 Cross-file function name consistency (MUST follow):
|
|
98
98
|
17. When you see an "=== Files Already Generated in This Run ===" section, those file contents are the AUTHORITATIVE source
|
|
99
|
-
of truth for exported function/variable names.
|
|
100
|
-
NEVER rename, guess, or substitute alternative names.
|
|
101
|
-
|
|
99
|
+
of truth for exported function/variable/action names.
|
|
100
|
+
NEVER rename, guess, or substitute alternative names. Copy-paste the exact identifier.
|
|
101
|
+
Common hallucination patterns to AVOID:
|
|
102
|
+
- Adding suffixes: fetchTasks \u2192 fetchTaskList, fetchTaskData, fetchTaskAll \u2190 ALL WRONG
|
|
103
|
+
- Changing verb: fetchTasks \u2192 getTasks, loadTasks, queryTasks \u2190 WRONG unless that's in the cache
|
|
104
|
+
- Changing number: createTask \u2192 createTasks \u2190 WRONG
|
|
105
|
+
For Pinia stores specifically: the "// public API (return object):" section or the full store content
|
|
106
|
+
shows EVERY available action name. If it shows "fetchTasks", that is the ONLY valid name.
|
|
107
|
+
If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.
|
|
108
|
+
ALSO applies to file paths: if you see "// exists: src/views/task-management/TaskManagement.vue",
|
|
109
|
+
the router import MUST use that exact path \u2014 NOT "@/views/task-management/index.vue" or any other guess.`;
|
|
102
110
|
codeGenGoSystemPrompt = `You are a Senior Go Developer implementing features based on provided specifications.
|
|
103
111
|
|
|
104
112
|
Rules:
|
|
@@ -4068,7 +4076,7 @@ ${content.slice(0, 1500)}`);
|
|
|
4068
4076
|
try {
|
|
4069
4077
|
const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
|
|
4070
4078
|
parts.push(`// ${f}
|
|
4071
|
-
${content.slice(0,
|
|
4079
|
+
${content.slice(0, 2e3)}`);
|
|
4072
4080
|
} catch {
|
|
4073
4081
|
}
|
|
4074
4082
|
}
|
|
@@ -4473,7 +4481,7 @@ Each task object must have these exact fields:
|
|
|
4473
4481
|
"id": "TASK-001", // sequential, zero-padded
|
|
4474
4482
|
"title": "...", // short action phrase, e.g. "Add UserFavorite Prisma model"
|
|
4475
4483
|
"description": "...", // 1-2 sentences, specific and actionable
|
|
4476
|
-
"layer": "data|service|api|test|infra", // implementation layer
|
|
4484
|
+
"layer": "data|service|api|view|route|test|infra", // implementation layer
|
|
4477
4485
|
"filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
|
|
4478
4486
|
"acceptanceCriteria": ["..."], // verifiable completion conditions
|
|
4479
4487
|
"dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
|
|
@@ -4481,11 +4489,34 @@ Each task object must have these exact fields:
|
|
|
4481
4489
|
}
|
|
4482
4490
|
|
|
4483
4491
|
Layer ordering guidance (implement in this order):
|
|
4484
|
-
1. "data" \u2014 DB schema changes, migrations, seed data
|
|
4492
|
+
1. "data" \u2014 DB schema changes, migrations, seed data; TypeScript type/interface definition files
|
|
4485
4493
|
2. "infra" \u2014 config, env vars, external service setup
|
|
4486
|
-
3. "service" \u2014 business logic, service classes
|
|
4487
|
-
4. "api" \u2014 controllers, routes, middleware, validators
|
|
4488
|
-
5. "
|
|
4494
|
+
3. "service" \u2014 business logic, service classes; for frontend: HTTP API call files ONLY (src/api/ or src/apis/)
|
|
4495
|
+
4. "api" \u2014 controllers, routes, middleware, validators; for frontend: state stores ONLY (src/stores/, Pinia/Vuex/Zustand/Redux)
|
|
4496
|
+
5. "view" \u2014 FRONTEND ONLY: page/view components (src/views/, src/pages/) \u2014 generated AFTER stores
|
|
4497
|
+
6. "route" \u2014 FRONTEND ONLY: router module files (src/router/routes/) \u2014 generated AFTER view components
|
|
4498
|
+
7. "test" \u2014 unit tests, integration tests
|
|
4499
|
+
|
|
4500
|
+
CRITICAL \u2014 Frontend four-layer dependency rule (prevents BOTH naming AND filename hallucinations):
|
|
4501
|
+
For Vue/React frontend projects, STRICTLY follow this assignment:
|
|
4502
|
+
"service" \u2192 src/api/* or src/apis/* files (HTTP functions: getTaskList, createTask)
|
|
4503
|
+
"api" \u2192 src/stores/* files (stores call service layer \u2014 see exact function names)
|
|
4504
|
+
"view" \u2192 src/views/* or src/pages/* files (pages use stores \u2014 see exact action names)
|
|
4505
|
+
"route" \u2192 src/router/routes/* files (router imports views \u2014 sees EXACT component filenames)
|
|
4506
|
+
|
|
4507
|
+
WHY "route" must come after "view":
|
|
4508
|
+
The router file imports the view component by filename, e.g.:
|
|
4509
|
+
import('@/views/task-management/TaskManagement.vue')
|
|
4510
|
+
If the router is generated BEFORE the view file exists in cache, the AI will guess a generic name
|
|
4511
|
+
like "index.vue" (the most common fallback) instead of the real filename "TaskManagement.vue".
|
|
4512
|
+
By generating the router AFTER the view, the cache contains "// exists: src/views/task-management/TaskManagement.vue"
|
|
4513
|
+
and the AI uses the EXACT path.
|
|
4514
|
+
|
|
4515
|
+
EXAMPLE (correct, four-layer):
|
|
4516
|
+
TASK-001 layer:"service" src/apis/taskManagement.ts (exports getTaskList, createTask)
|
|
4517
|
+
TASK-002 layer:"api" src/stores/taskStore.ts (calls getTaskList \u2014 visible in cache \u2713)
|
|
4518
|
+
TASK-003 layer:"view" src/views/task-management/TaskManagement.vue (uses taskStore \u2014 visible in cache \u2713)
|
|
4519
|
+
TASK-004 layer:"route" src/router/routes/taskManagement.ts (imports TaskManagement.vue \u2014 filename visible in cache \u2713)
|
|
4489
4520
|
|
|
4490
4521
|
CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
|
|
4491
4522
|
- ONLY use paths that appear in the "Verified File Inventory" section of the prompt.
|
|
@@ -4535,7 +4566,7 @@ function buildTaskPrompt(spec, context) {
|
|
|
4535
4566
|
if (context.constitution) {
|
|
4536
4567
|
parts.push(`
|
|
4537
4568
|
=== Project Constitution (rules to follow) ===
|
|
4538
|
-
${context.constitution
|
|
4569
|
+
${context.constitution}`);
|
|
4539
4570
|
}
|
|
4540
4571
|
if (context.techStack.length > 0) {
|
|
4541
4572
|
parts.push(`
|
|
@@ -4550,7 +4581,9 @@ var LAYER_ORDER = {
|
|
|
4550
4581
|
infra: 1,
|
|
4551
4582
|
service: 2,
|
|
4552
4583
|
api: 3,
|
|
4553
|
-
|
|
4584
|
+
view: 4,
|
|
4585
|
+
route: 5,
|
|
4586
|
+
test: 6
|
|
4554
4587
|
};
|
|
4555
4588
|
var TaskGenerator = class {
|
|
4556
4589
|
constructor(provider) {
|
|
@@ -4592,6 +4625,8 @@ function printTasks(tasks) {
|
|
|
4592
4625
|
infra: chalk3.gray,
|
|
4593
4626
|
service: chalk3.blue,
|
|
4594
4627
|
api: chalk3.cyan,
|
|
4628
|
+
view: chalk3.yellow,
|
|
4629
|
+
route: chalk3.white,
|
|
4595
4630
|
test: chalk3.green
|
|
4596
4631
|
};
|
|
4597
4632
|
console.log(chalk3.bold(`
|
|
@@ -5811,16 +5846,111 @@ function buildInstalledPackagesSection(context) {
|
|
|
5811
5846
|
${context.dependencies.join(", ")}
|
|
5812
5847
|
`;
|
|
5813
5848
|
}
|
|
5849
|
+
function extractBehavioralContract(content) {
|
|
5850
|
+
const lines = content.split("\n");
|
|
5851
|
+
const contractLines = [];
|
|
5852
|
+
const throwLines = [];
|
|
5853
|
+
let i = 0;
|
|
5854
|
+
while (i < lines.length) {
|
|
5855
|
+
const line = lines[i];
|
|
5856
|
+
const trimmed = line.trim();
|
|
5857
|
+
if (/^export\s+(interface|type|class|abstract\s+class|enum)\s/.test(trimmed)) {
|
|
5858
|
+
contractLines.push(line.trimEnd());
|
|
5859
|
+
if (trimmed.includes("{")) {
|
|
5860
|
+
let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
|
|
5861
|
+
i++;
|
|
5862
|
+
while (i < lines.length && depth > 0) {
|
|
5863
|
+
const inner = lines[i];
|
|
5864
|
+
contractLines.push(inner.trimEnd());
|
|
5865
|
+
depth += (inner.match(/\{/g) ?? []).length;
|
|
5866
|
+
depth -= (inner.match(/\}/g) ?? []).length;
|
|
5867
|
+
i++;
|
|
5868
|
+
}
|
|
5869
|
+
} else {
|
|
5870
|
+
i++;
|
|
5871
|
+
}
|
|
5872
|
+
continue;
|
|
5873
|
+
}
|
|
5874
|
+
if (/^export\s+const\s+\w+\s*=\s*(defineStore|createStore|createSlice)\s*\(/.test(trimmed)) {
|
|
5875
|
+
contractLines.push(line.trimEnd());
|
|
5876
|
+
let depth = (trimmed.match(/\(/g) ?? []).length - (trimmed.match(/\)/g) ?? []).length;
|
|
5877
|
+
i++;
|
|
5878
|
+
while (i < lines.length && depth > 0) {
|
|
5879
|
+
const inner = lines[i];
|
|
5880
|
+
contractLines.push(inner.trimEnd());
|
|
5881
|
+
depth += (inner.match(/\(/g) ?? []).length;
|
|
5882
|
+
depth -= (inner.match(/\)/g) ?? []).length;
|
|
5883
|
+
i++;
|
|
5884
|
+
}
|
|
5885
|
+
continue;
|
|
5886
|
+
}
|
|
5887
|
+
if (/^return\s*\{/.test(trimmed)) {
|
|
5888
|
+
contractLines.push("// public API (return object):");
|
|
5889
|
+
contractLines.push(line.trimEnd());
|
|
5890
|
+
let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
|
|
5891
|
+
i++;
|
|
5892
|
+
while (i < lines.length && depth > 0) {
|
|
5893
|
+
const inner = lines[i];
|
|
5894
|
+
contractLines.push(inner.trimEnd());
|
|
5895
|
+
depth += (inner.match(/\{/g) ?? []).length;
|
|
5896
|
+
depth -= (inner.match(/\}/g) ?? []).length;
|
|
5897
|
+
i++;
|
|
5898
|
+
}
|
|
5899
|
+
continue;
|
|
5900
|
+
}
|
|
5901
|
+
if (/^export\s+default\s+(async\s+)?(function|class)\b/.test(trimmed)) {
|
|
5902
|
+
contractLines.push(line.trimEnd());
|
|
5903
|
+
if (trimmed.includes("{")) {
|
|
5904
|
+
let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
|
|
5905
|
+
i++;
|
|
5906
|
+
while (i < lines.length && depth > 0) {
|
|
5907
|
+
const inner = lines[i];
|
|
5908
|
+
contractLines.push(inner.trimEnd());
|
|
5909
|
+
depth += (inner.match(/\{/g) ?? []).length;
|
|
5910
|
+
depth -= (inner.match(/\}/g) ?? []).length;
|
|
5911
|
+
i++;
|
|
5912
|
+
}
|
|
5913
|
+
} else {
|
|
5914
|
+
i++;
|
|
5915
|
+
}
|
|
5916
|
+
continue;
|
|
5917
|
+
}
|
|
5918
|
+
if (/^export\s/.test(trimmed)) {
|
|
5919
|
+
contractLines.push(line.trimEnd());
|
|
5920
|
+
}
|
|
5921
|
+
if (/throw\s+(new\s+)?\w*[Ee]rror\b|throw\s+create[A-Z]\w*|@throws/.test(line) && throwLines.length < 20) {
|
|
5922
|
+
throwLines.push(" // " + trimmed);
|
|
5923
|
+
}
|
|
5924
|
+
i++;
|
|
5925
|
+
}
|
|
5926
|
+
if (contractLines.length === 0 && throwLines.length === 0) {
|
|
5927
|
+
return content.slice(0, 3e3);
|
|
5928
|
+
}
|
|
5929
|
+
const parts = [...contractLines];
|
|
5930
|
+
if (throwLines.length > 0) {
|
|
5931
|
+
parts.push("", "// Error contracts (throws / validation):", ...throwLines);
|
|
5932
|
+
}
|
|
5933
|
+
return parts.join("\n");
|
|
5934
|
+
}
|
|
5814
5935
|
function buildGeneratedFilesSection(cache) {
|
|
5815
5936
|
if (cache.size === 0) return "";
|
|
5816
5937
|
const lines = [
|
|
5817
|
-
"\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ==="
|
|
5938
|
+
"\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ===",
|
|
5939
|
+
"// CRITICAL: function/action names and file paths below are ground truth. Copy them EXACTLY.",
|
|
5940
|
+
"// Do NOT add suffixes (List, Data, All, Info) or change casing.",
|
|
5941
|
+
"// For '// exists:' entries: use the EXACT filename shown \u2014 do NOT substitute index.vue or other defaults."
|
|
5818
5942
|
];
|
|
5819
5943
|
for (const [filePath, content] of cache) {
|
|
5944
|
+
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(filePath);
|
|
5945
|
+
if (isViewFile) {
|
|
5946
|
+
lines.push(`
|
|
5947
|
+
// exists: ${filePath}`);
|
|
5948
|
+
continue;
|
|
5949
|
+
}
|
|
5820
5950
|
lines.push(`
|
|
5821
5951
|
--- ${filePath} ---`);
|
|
5822
|
-
|
|
5823
|
-
|
|
5952
|
+
const isStoreOrComposable = /src[\\/](stores?|composables?)[\\/]/i.test(filePath);
|
|
5953
|
+
lines.push(isStoreOrComposable ? content : extractBehavioralContract(content));
|
|
5824
5954
|
}
|
|
5825
5955
|
return lines.join("\n") + "\n";
|
|
5826
5956
|
}
|
|
@@ -6008,7 +6138,7 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
6008
6138
|
const spec = await fs8.readFile(specFilePath, "utf-8");
|
|
6009
6139
|
const constitutionSection = context?.constitution ? `
|
|
6010
6140
|
=== Project Constitution (MUST follow) ===
|
|
6011
|
-
${context.constitution
|
|
6141
|
+
${context.constitution}
|
|
6012
6142
|
` : "";
|
|
6013
6143
|
const contextSummary = context ? `Tech Stack: ${context.techStack.join(", ")}
|
|
6014
6144
|
Existing files: ${context.fileStructure.slice(0, 20).join(", ")}` : "";
|
|
@@ -6102,7 +6232,7 @@ Output ONLY a valid JSON array:
|
|
|
6102
6232
|
printTaskProgress(completedTasks++, tasks.length, task, "skip");
|
|
6103
6233
|
}
|
|
6104
6234
|
}
|
|
6105
|
-
const LAYER_ORDER2 = ["data", "infra", "service", "api", "test"];
|
|
6235
|
+
const LAYER_ORDER2 = ["data", "infra", "service", "api", "view", "route", "test"];
|
|
6106
6236
|
const layerGroups = [];
|
|
6107
6237
|
for (const layer of LAYER_ORDER2) {
|
|
6108
6238
|
const group = pendingTasks.filter((t) => t.layer === layer);
|
|
@@ -6125,10 +6255,9 @@ Output ONLY a valid JSON array:
|
|
|
6125
6255
|
} else {
|
|
6126
6256
|
printTaskProgress(completedTasks, tasks.length, layerTasks[0], "run");
|
|
6127
6257
|
}
|
|
6128
|
-
const
|
|
6129
|
-
const taskResultPromises = layerTasks.map(async (task) => {
|
|
6258
|
+
const executeTask = async (task, batchIsParallel) => {
|
|
6130
6259
|
if (task.filesToTouch.length === 0) {
|
|
6131
|
-
if (!
|
|
6260
|
+
if (!batchIsParallel) console.log(chalk6.gray(" No files specified, skipping."));
|
|
6132
6261
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
6133
6262
|
}
|
|
6134
6263
|
const filePlan = await Promise.all(
|
|
@@ -6143,10 +6272,11 @@ Output ONLY a valid JSON array:
|
|
|
6143
6272
|
);
|
|
6144
6273
|
const createsNewFiles = filePlan.some((f) => f.action === "create");
|
|
6145
6274
|
const taskText = `${task.title} ${task.description}`.toLowerCase();
|
|
6146
|
-
const impliesRegistration = createsNewFiles && (taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
|
|
6275
|
+
const impliesRegistration = createsNewFiles && (task.layer === "route" || task.layer === "view" || task.layer === "api" || taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
|
|
6147
6276
|
if (filePlan.length === 0) {
|
|
6148
6277
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration };
|
|
6149
6278
|
}
|
|
6279
|
+
const currentGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
6150
6280
|
const taskContext = `Task: ${task.id} \u2014 ${task.title}
|
|
6151
6281
|
${task.description}
|
|
6152
6282
|
Acceptance: ${task.acceptanceCriteria.join("; ")}`;
|
|
@@ -6157,15 +6287,38 @@ Acceptance: ${task.acceptanceCriteria.join("; ")}`;
|
|
|
6157
6287
|
=== Current Task ===
|
|
6158
6288
|
${taskContext}`,
|
|
6159
6289
|
workingDir,
|
|
6160
|
-
constitutionSection + frontendSection + sharedConfigSection +
|
|
6290
|
+
constitutionSection + frontendSection + sharedConfigSection + currentGeneratedFilesSection,
|
|
6161
6291
|
systemPrompt,
|
|
6162
|
-
|
|
6292
|
+
batchIsParallel ? task.id : void 0
|
|
6163
6293
|
// prefix output lines with task ID in parallel mode
|
|
6164
6294
|
);
|
|
6165
6295
|
const createdFiles = filePlan.filter((fp) => fp.action === "create").map((fp) => fp.file);
|
|
6166
6296
|
return { task, files, createdFiles, success, total, impliesRegistration };
|
|
6167
|
-
}
|
|
6168
|
-
const
|
|
6297
|
+
};
|
|
6298
|
+
const updateCacheFromBatch = async (results) => {
|
|
6299
|
+
for (const result of results) {
|
|
6300
|
+
for (const writtenFile of result.files) {
|
|
6301
|
+
const isCodeFile = /src[\\/](api[s]?|services?|stores?|composables?)[\\/]/i.test(writtenFile);
|
|
6302
|
+
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
|
|
6303
|
+
if (isCodeFile || isViewFile) {
|
|
6304
|
+
try {
|
|
6305
|
+
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs8.readFile(path7.join(workingDir, writtenFile), "utf-8");
|
|
6306
|
+
generatedFileCache.set(writtenFile, content);
|
|
6307
|
+
} catch {
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
6312
|
+
};
|
|
6313
|
+
const taskBatches = topoSortLayerTasks(layerTasks);
|
|
6314
|
+
const layerResults = [];
|
|
6315
|
+
for (const batch of taskBatches) {
|
|
6316
|
+
const batchIsParallel = batch.length > 1;
|
|
6317
|
+
const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
|
|
6318
|
+
const batchResults = await Promise.all(batchResultPromises);
|
|
6319
|
+
layerResults.push(...batchResults);
|
|
6320
|
+
await updateCacheFromBatch(batchResults);
|
|
6321
|
+
}
|
|
6169
6322
|
if (isParallel) {
|
|
6170
6323
|
console.log("");
|
|
6171
6324
|
}
|
|
@@ -6185,17 +6338,6 @@ ${taskContext}`,
|
|
|
6185
6338
|
}
|
|
6186
6339
|
}
|
|
6187
6340
|
completedTasks += layerTasks.length;
|
|
6188
|
-
for (const result of layerResults) {
|
|
6189
|
-
for (const writtenFile of result.files) {
|
|
6190
|
-
if (/src[\\/](api[s]?|services?|stores?|composables?)[\\/]/.test(writtenFile)) {
|
|
6191
|
-
try {
|
|
6192
|
-
const content = await fs8.readFile(path7.join(workingDir, writtenFile), "utf-8");
|
|
6193
|
-
generatedFileCache.set(writtenFile, content);
|
|
6194
|
-
} catch {
|
|
6195
|
-
}
|
|
6196
|
-
}
|
|
6197
|
-
}
|
|
6198
|
-
}
|
|
6199
6341
|
const anyImpliesRegistration = layerResults.some((r) => r.impliesRegistration);
|
|
6200
6342
|
if (anyImpliesRegistration && sharedConfigPaths.size > 0 && context?.sharedConfigFiles) {
|
|
6201
6343
|
const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
|
|
@@ -6295,11 +6437,48 @@ ${spec}`,
|
|
|
6295
6437
|
console.log(chalk6.cyan("\n") + plan);
|
|
6296
6438
|
}
|
|
6297
6439
|
};
|
|
6440
|
+
function topoSortLayerTasks(tasks) {
|
|
6441
|
+
if (tasks.length <= 1) return [tasks];
|
|
6442
|
+
const idSet = new Set(tasks.map((t) => t.id));
|
|
6443
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
6444
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
6445
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
6446
|
+
for (const task of tasks) {
|
|
6447
|
+
inDegree.set(task.id, 0);
|
|
6448
|
+
dependents.set(task.id, []);
|
|
6449
|
+
}
|
|
6450
|
+
for (const task of tasks) {
|
|
6451
|
+
const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
|
|
6452
|
+
inDegree.set(task.id, intraDeps.length);
|
|
6453
|
+
for (const dep of intraDeps) {
|
|
6454
|
+
dependents.get(dep).push(task.id);
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
const batches = [];
|
|
6458
|
+
const remaining = new Set(tasks.map((t) => t.id));
|
|
6459
|
+
while (remaining.size > 0) {
|
|
6460
|
+
const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
|
|
6461
|
+
if (batch.length === 0) {
|
|
6462
|
+
batches.push([...remaining].map((id) => taskById.get(id)));
|
|
6463
|
+
break;
|
|
6464
|
+
}
|
|
6465
|
+
batches.push(batch);
|
|
6466
|
+
for (const task of batch) {
|
|
6467
|
+
remaining.delete(task.id);
|
|
6468
|
+
for (const dependent of dependents.get(task.id)) {
|
|
6469
|
+
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
return batches;
|
|
6474
|
+
}
|
|
6298
6475
|
var LAYER_ICONS = {
|
|
6299
6476
|
data: "\u{1F4BE}",
|
|
6300
6477
|
infra: "\u2699\uFE0F ",
|
|
6301
6478
|
service: "\u{1F527}",
|
|
6302
6479
|
api: "\u{1F310}",
|
|
6480
|
+
view: "\u{1F5A5}\uFE0F ",
|
|
6481
|
+
route: "\u{1F5FA}\uFE0F ",
|
|
6303
6482
|
test: "\u{1F9EA}"
|
|
6304
6483
|
};
|
|
6305
6484
|
function printTaskProgress(completed, total, task, mode) {
|
|
@@ -6808,7 +6987,7 @@ function parseConstitutionStats(content) {
|
|
|
6808
6987
|
for (let i = section9Start + 1; i < lines.length; i++) {
|
|
6809
6988
|
if (lines[i].match(/^## \d/) && i > section9Start) break;
|
|
6810
6989
|
section9Lines++;
|
|
6811
|
-
if (lines[i].trim()
|
|
6990
|
+
if (/^-\s+.*\*\*\[\d{4}-\d{2}-\d{2}\]\*\*/.test(lines[i].trim())) lessonCount++;
|
|
6812
6991
|
}
|
|
6813
6992
|
return { totalLines, section9Lines, lessonCount };
|
|
6814
6993
|
}
|
|
@@ -7297,7 +7476,7 @@ function detectLintCommand(workingDir) {
|
|
|
7297
7476
|
function parseErrors(output, source) {
|
|
7298
7477
|
const errors = [];
|
|
7299
7478
|
if (!output.trim()) return errors;
|
|
7300
|
-
const lines = output.split("\n")
|
|
7479
|
+
const lines = output.split("\n");
|
|
7301
7480
|
for (const line of lines) {
|
|
7302
7481
|
const trimmed = line.trim();
|
|
7303
7482
|
if (!trimmed) continue;
|
|
@@ -7306,13 +7485,15 @@ function parseErrors(output, source) {
|
|
|
7306
7485
|
if (trimmed.startsWith("at ")) continue;
|
|
7307
7486
|
if (trimmed.startsWith("Node.js ")) continue;
|
|
7308
7487
|
const fileMatch = trimmed.match(/^([^:]+\.(?:ts|js|tsx|jsx|go|py|java|rs|php)):\d+/);
|
|
7488
|
+
if (!fileMatch) continue;
|
|
7309
7489
|
errors.push({
|
|
7310
7490
|
source,
|
|
7311
|
-
message: trimmed.slice(0,
|
|
7312
|
-
file: fileMatch
|
|
7491
|
+
message: trimmed.slice(0, 400),
|
|
7492
|
+
file: fileMatch[1]
|
|
7313
7493
|
});
|
|
7494
|
+
if (errors.length >= 20) break;
|
|
7314
7495
|
}
|
|
7315
|
-
return errors
|
|
7496
|
+
return errors;
|
|
7316
7497
|
}
|
|
7317
7498
|
async function attemptFix(provider, errors, workingDir, dsl) {
|
|
7318
7499
|
const results = [];
|
|
@@ -7503,7 +7684,7 @@ async function assessSpec(provider, spec, constitution) {
|
|
|
7503
7684
|
const prompt = `Assess the following feature specification.
|
|
7504
7685
|
${constitution ? `
|
|
7505
7686
|
=== Project Constitution (check consistency against this) ===
|
|
7506
|
-
${constitution
|
|
7687
|
+
${constitution}
|
|
7507
7688
|
` : ""}
|
|
7508
7689
|
=== Feature Spec ===
|
|
7509
7690
|
${spec}`;
|
|
@@ -7622,6 +7803,42 @@ async function appendLessonsToConstitution(projectRoot, issues) {
|
|
|
7622
7803
|
);
|
|
7623
7804
|
}
|
|
7624
7805
|
}
|
|
7806
|
+
async function appendDirectLesson(projectRoot, lessonText) {
|
|
7807
|
+
const constitutionPath = path14.join(projectRoot, CONSTITUTION_FILE);
|
|
7808
|
+
let content = "";
|
|
7809
|
+
try {
|
|
7810
|
+
content = await fs15.readFile(constitutionPath, "utf-8");
|
|
7811
|
+
} catch {
|
|
7812
|
+
return { appended: false, reason: "No constitution file found. Run `ai-spec init` first." };
|
|
7813
|
+
}
|
|
7814
|
+
const normalized = lessonText.toLowerCase().slice(0, 60);
|
|
7815
|
+
if (content.toLowerCase().includes(normalized)) {
|
|
7816
|
+
return { appended: false, reason: "Similar lesson already exists in the constitution." };
|
|
7817
|
+
}
|
|
7818
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7819
|
+
const entry = `- \u{1F4DD} **[${date}]** ${lessonText.trim()}`;
|
|
7820
|
+
const hasMemorySection = content.includes(MEMORY_SECTION_MARKER);
|
|
7821
|
+
let updatedContent;
|
|
7822
|
+
if (hasMemorySection) {
|
|
7823
|
+
const sectionStart = content.indexOf(MEMORY_SECTION_MARKER);
|
|
7824
|
+
const afterHeader = sectionStart + MEMORY_SECTION_HEADER.length;
|
|
7825
|
+
const nextSectionMatch = content.slice(afterHeader).match(/\n## \d/);
|
|
7826
|
+
const insertPos = nextSectionMatch ? afterHeader + nextSectionMatch.index : content.length;
|
|
7827
|
+
updatedContent = content.slice(0, insertPos) + entry + "\n" + content.slice(insertPos);
|
|
7828
|
+
} else {
|
|
7829
|
+
updatedContent = content + MEMORY_SECTION_HEADER + entry + "\n";
|
|
7830
|
+
}
|
|
7831
|
+
await fs15.writeFile(constitutionPath, updatedContent, "utf-8");
|
|
7832
|
+
const stats = parseConstitutionStats(updatedContent);
|
|
7833
|
+
if (stats.lessonCount >= 8) {
|
|
7834
|
+
console.log(
|
|
7835
|
+
chalk14.yellow(
|
|
7836
|
+
` \u26A0 \xA79 now has ${stats.lessonCount} lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
|
|
7837
|
+
)
|
|
7838
|
+
);
|
|
7839
|
+
}
|
|
7840
|
+
return { appended: true };
|
|
7841
|
+
}
|
|
7625
7842
|
async function accumulateReviewKnowledge(provider, projectRoot, reviewText) {
|
|
7626
7843
|
console.log(chalk14.blue("\n\u2500\u2500\u2500 Knowledge Memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7627
7844
|
const issues = extractIssuesFromReview(reviewText);
|
|
@@ -8290,6 +8507,7 @@ Existing API/service files:`);
|
|
|
8290
8507
|
// core/mock-server-generator.ts
|
|
8291
8508
|
import * as path18 from "path";
|
|
8292
8509
|
import * as fs19 from "fs-extra";
|
|
8510
|
+
import { spawn } from "child_process";
|
|
8293
8511
|
function typeToFixture(fieldName, typeDesc) {
|
|
8294
8512
|
const t = typeDesc.toLowerCase();
|
|
8295
8513
|
if (t.includes("boolean") || t === "bool") return true;
|
|
@@ -8616,6 +8834,151 @@ import { handlers } from './handlers';
|
|
|
8616
8834
|
export const worker = setupWorker(...handlers);
|
|
8617
8835
|
`;
|
|
8618
8836
|
}
|
|
8837
|
+
var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
|
|
8838
|
+
function findViteConfigFile(projectDir) {
|
|
8839
|
+
for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
8840
|
+
if (fs19.existsSync(path18.join(projectDir, f))) return f;
|
|
8841
|
+
}
|
|
8842
|
+
return null;
|
|
8843
|
+
}
|
|
8844
|
+
function buildViteProxyEntries(endpoints, mockPort) {
|
|
8845
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
8846
|
+
for (const ep of endpoints) {
|
|
8847
|
+
const parts = ep.path.split("/").filter(Boolean);
|
|
8848
|
+
if (parts.length > 0) prefixes.add(`/${parts[0]}`);
|
|
8849
|
+
}
|
|
8850
|
+
if (prefixes.size === 0) prefixes.add("/api");
|
|
8851
|
+
const target = `http://localhost:${mockPort}`;
|
|
8852
|
+
return Array.from(prefixes).map((p) => ` '${p}': { target: '${target}', changeOrigin: true },`).join("\n");
|
|
8853
|
+
}
|
|
8854
|
+
function generateViteMockConfigTs(baseConfigFile, mockPort, endpoints) {
|
|
8855
|
+
const importPath = `./${baseConfigFile.replace(/\.(ts|mts|js|mjs)$/, "")}`;
|
|
8856
|
+
const proxyEntries = buildViteProxyEntries(endpoints, mockPort);
|
|
8857
|
+
return `// Auto-generated by ai-spec mock --serve
|
|
8858
|
+
// LOCAL DEVELOPMENT ONLY \u2014 do not commit this file
|
|
8859
|
+
// Remove with: ai-spec mock --restore
|
|
8860
|
+
import { defineConfig, mergeConfig } from 'vite';
|
|
8861
|
+
|
|
8862
|
+
export default defineConfig(async (env) => {
|
|
8863
|
+
const mod = await import('${importPath}');
|
|
8864
|
+
const baseConfigOrFn = mod.default;
|
|
8865
|
+
const baseConfig =
|
|
8866
|
+
typeof baseConfigOrFn === 'function'
|
|
8867
|
+
? await baseConfigOrFn(env)
|
|
8868
|
+
: baseConfigOrFn;
|
|
8869
|
+
|
|
8870
|
+
return mergeConfig(baseConfig ?? {}, {
|
|
8871
|
+
server: {
|
|
8872
|
+
proxy: {
|
|
8873
|
+
${proxyEntries}
|
|
8874
|
+
},
|
|
8875
|
+
},
|
|
8876
|
+
});
|
|
8877
|
+
});
|
|
8878
|
+
`;
|
|
8879
|
+
}
|
|
8880
|
+
async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
|
|
8881
|
+
const framework = detectFrontendFramework(frontendDir);
|
|
8882
|
+
const actions = [];
|
|
8883
|
+
if (framework === "vite") {
|
|
8884
|
+
const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
|
|
8885
|
+
const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
|
|
8886
|
+
const mockConfigPath = path18.join(frontendDir, "vite.config.ai-spec-mock.ts");
|
|
8887
|
+
await fs19.writeFile(mockConfigPath, mockConfigContent, "utf-8");
|
|
8888
|
+
actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
|
|
8889
|
+
const pkgPath = path18.join(frontendDir, "package.json");
|
|
8890
|
+
if (await fs19.pathExists(pkgPath)) {
|
|
8891
|
+
const pkg = await fs19.readJson(pkgPath);
|
|
8892
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
8893
|
+
const originalValue = pkg.scripts["dev:mock"] ?? null;
|
|
8894
|
+
pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
|
|
8895
|
+
await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8896
|
+
actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
|
|
8897
|
+
}
|
|
8898
|
+
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
8899
|
+
await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
8900
|
+
return { framework, applied: true, devCommand: "npm run dev:mock" };
|
|
8901
|
+
}
|
|
8902
|
+
if (framework === "cra") {
|
|
8903
|
+
const pkgPath = path18.join(frontendDir, "package.json");
|
|
8904
|
+
if (await fs19.pathExists(pkgPath)) {
|
|
8905
|
+
const pkg = await fs19.readJson(pkgPath);
|
|
8906
|
+
const originalProxy = pkg.proxy ?? null;
|
|
8907
|
+
pkg.proxy = `http://localhost:${mockPort}`;
|
|
8908
|
+
await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8909
|
+
actions.push({ type: "patched-pkg-proxy", originalProxy });
|
|
8910
|
+
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
8911
|
+
await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
8912
|
+
return { framework, applied: true, devCommand: "npm start" };
|
|
8913
|
+
}
|
|
8914
|
+
return { framework, applied: false, devCommand: null, note: "No package.json found." };
|
|
8915
|
+
}
|
|
8916
|
+
const lock = { framework, mockPort, frontendDir, actions };
|
|
8917
|
+
await fs19.writeJson(path18.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
8918
|
+
const manualNote = framework === "next" ? `Add rewrites in next.config.js to proxy API calls to http://localhost:${mockPort}` : `Add proxy in webpack.config.js devServer to target http://localhost:${mockPort}`;
|
|
8919
|
+
return { framework, applied: false, devCommand: null, note: manualNote };
|
|
8920
|
+
}
|
|
8921
|
+
async function restoreMockProxy(frontendDir) {
|
|
8922
|
+
const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
|
|
8923
|
+
if (!await fs19.pathExists(lockPath)) {
|
|
8924
|
+
return { restored: false, note: "No lock file found \u2014 nothing to restore." };
|
|
8925
|
+
}
|
|
8926
|
+
const lock = await fs19.readJson(lockPath);
|
|
8927
|
+
for (const action of lock.actions) {
|
|
8928
|
+
if (action.type === "wrote-file") {
|
|
8929
|
+
const fp = path18.join(frontendDir, action.filePath);
|
|
8930
|
+
if (await fs19.pathExists(fp)) await fs19.remove(fp);
|
|
8931
|
+
} else if (action.type === "added-pkg-script") {
|
|
8932
|
+
const pkgPath = path18.join(frontendDir, "package.json");
|
|
8933
|
+
if (await fs19.pathExists(pkgPath)) {
|
|
8934
|
+
const pkg = await fs19.readJson(pkgPath);
|
|
8935
|
+
if (action.originalValue == null) {
|
|
8936
|
+
delete pkg.scripts?.[action.key];
|
|
8937
|
+
} else {
|
|
8938
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
8939
|
+
pkg.scripts[action.key] = action.originalValue;
|
|
8940
|
+
}
|
|
8941
|
+
await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8942
|
+
}
|
|
8943
|
+
} else if (action.type === "patched-pkg-proxy") {
|
|
8944
|
+
const pkgPath = path18.join(frontendDir, "package.json");
|
|
8945
|
+
if (await fs19.pathExists(pkgPath)) {
|
|
8946
|
+
const pkg = await fs19.readJson(pkgPath);
|
|
8947
|
+
if (action.originalProxy == null) {
|
|
8948
|
+
delete pkg.proxy;
|
|
8949
|
+
} else {
|
|
8950
|
+
pkg.proxy = action.originalProxy;
|
|
8951
|
+
}
|
|
8952
|
+
await fs19.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8953
|
+
}
|
|
8954
|
+
}
|
|
8955
|
+
}
|
|
8956
|
+
if (lock.mockServerPid) {
|
|
8957
|
+
try {
|
|
8958
|
+
process.kill(lock.mockServerPid, "SIGTERM");
|
|
8959
|
+
} catch {
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
await fs19.remove(lockPath);
|
|
8963
|
+
return { restored: true };
|
|
8964
|
+
}
|
|
8965
|
+
function startMockServerBackground(serverJsPath, port) {
|
|
8966
|
+
const child = spawn("node", [serverJsPath], {
|
|
8967
|
+
detached: true,
|
|
8968
|
+
stdio: "ignore",
|
|
8969
|
+
env: { ...process.env, MOCK_PORT: String(port) }
|
|
8970
|
+
});
|
|
8971
|
+
child.unref();
|
|
8972
|
+
return child.pid;
|
|
8973
|
+
}
|
|
8974
|
+
async function saveMockServerPid(frontendDir, pid) {
|
|
8975
|
+
const lockPath = path18.join(frontendDir, MOCK_LOCK_FILE);
|
|
8976
|
+
if (await fs19.pathExists(lockPath)) {
|
|
8977
|
+
const lock = await fs19.readJson(lockPath);
|
|
8978
|
+
lock.mockServerPid = pid;
|
|
8979
|
+
await fs19.writeJson(lockPath, lock, { spaces: 2 });
|
|
8980
|
+
}
|
|
8981
|
+
}
|
|
8619
8982
|
async function generateMockAssets(dsl, projectDir, opts = {}) {
|
|
8620
8983
|
const port = opts.port ?? 3001;
|
|
8621
8984
|
const outputDir = path18.join(projectDir, opts.outputDir ?? "mock");
|
|
@@ -8752,7 +9115,7 @@ Rules:
|
|
|
8752
9115
|
function buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context) {
|
|
8753
9116
|
const constitutionSection = context?.constitution ? `
|
|
8754
9117
|
=== Project Constitution (all changes must comply) ===
|
|
8755
|
-
${context.constitution
|
|
9118
|
+
${context.constitution}
|
|
8756
9119
|
` : "";
|
|
8757
9120
|
const dslSummary = existingDsl ? `
|
|
8758
9121
|
=== Current DSL Summary (for reference) ===
|
|
@@ -9264,7 +9627,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9264
9627
|
).option(
|
|
9265
9628
|
"--codegen-provider <name>",
|
|
9266
9629
|
"AI provider for code generation (defaults to --provider)"
|
|
9267
|
-
).option("--codegen-model <name>", "Model for code generation").option("--codegen-key <key>", "API key for code generation (if different)").option("--skip-worktree", "Skip git worktree creation (auto-set for frontend projects)").option("--worktree", "Force git worktree creation even for frontend projects").option("--skip-review", "Skip automated code review").option("--skip-tasks", "Skip task generation (just generate spec)").option("--auto", "Run claude non-interactively via -p flag (saves tokens)").option("--fast", "Skip interactive spec refinement, proceed immediately with initial spec").option("--resume", "Resume an interrupted run \u2014 skip tasks already marked as done").option("--skip-dsl", "Skip DSL extraction step").option("--skip-tests", "Skip test skeleton generation").option("--skip-error-feedback", "Skip error feedback loop (test/lint auto-fix)").option("--tdd", "TDD mode: generate failing tests first, then generate implementation to pass them").option("--skip-assessment", "Skip spec quality pre-assessment before the Approval Gate").action(async (idea, opts) => {
|
|
9630
|
+
).option("--codegen-model <name>", "Model for code generation").option("--codegen-key <key>", "API key for code generation (if different)").option("--skip-worktree", "Skip git worktree creation (auto-set for frontend projects)").option("--worktree", "Force git worktree creation even for frontend projects").option("--skip-review", "Skip automated code review").option("--skip-tasks", "Skip task generation (just generate spec)").option("--auto", "Run claude non-interactively via -p flag (saves tokens)").option("--fast", "Skip interactive spec refinement, proceed immediately with initial spec").option("--resume", "Resume an interrupted run \u2014 skip tasks already marked as done").option("--skip-dsl", "Skip DSL extraction step").option("--skip-tests", "Skip test skeleton generation").option("--skip-error-feedback", "Skip error feedback loop (test/lint auto-fix)").option("--tdd", "TDD mode: generate failing tests first, then generate implementation to pass them").option("--skip-assessment", "Skip spec quality pre-assessment before the Approval Gate").option("--force", "Bypass the spec quality score gate even if score is below minSpecScore").option("--serve", "After workspace pipeline completes, auto-start mock server + patch frontend proxy").action(async (idea, opts) => {
|
|
9268
9631
|
const currentDir = process.cwd();
|
|
9269
9632
|
const config2 = await loadConfig(currentDir);
|
|
9270
9633
|
if (!idea) {
|
|
@@ -9279,7 +9642,42 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9279
9642
|
console.log(chalk17.cyan(`
|
|
9280
9643
|
[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
9281
9644
|
console.log(chalk17.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
9282
|
-
await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
9645
|
+
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
9646
|
+
if (opts.serve) {
|
|
9647
|
+
console.log(chalk17.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
9648
|
+
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
9649
|
+
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
9650
|
+
if (!backendResult) {
|
|
9651
|
+
console.log(chalk17.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
|
|
9652
|
+
} else {
|
|
9653
|
+
const mockPort = 3001;
|
|
9654
|
+
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
9655
|
+
const serverJsPath = path21.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
9656
|
+
console.log(chalk17.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
9657
|
+
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
9658
|
+
console.log(chalk17.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
9659
|
+
if (frontendResult) {
|
|
9660
|
+
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
|
|
9661
|
+
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
9662
|
+
if (proxyResult.applied) {
|
|
9663
|
+
console.log(chalk17.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
9664
|
+
console.log(chalk17.bold.cyan(`
|
|
9665
|
+
Ready! Run your frontend dev server:`));
|
|
9666
|
+
console.log(chalk17.white(` cd ${frontendResult.repoAbsPath}`));
|
|
9667
|
+
console.log(chalk17.white(` ${proxyResult.devCommand}`));
|
|
9668
|
+
console.log(chalk17.gray(`
|
|
9669
|
+
When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
9670
|
+
} else {
|
|
9671
|
+
console.log(chalk17.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
9672
|
+
if (proxyResult.note) console.log(chalk17.gray(` ${proxyResult.note}`));
|
|
9673
|
+
console.log(chalk17.gray(` Mock server: http://localhost:${mockPort}`));
|
|
9674
|
+
}
|
|
9675
|
+
} else {
|
|
9676
|
+
console.log(chalk17.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
|
|
9677
|
+
console.log(chalk17.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
}
|
|
9283
9681
|
return;
|
|
9284
9682
|
}
|
|
9285
9683
|
const specProviderName = opts.provider || config2.provider || "gemini";
|
|
@@ -9308,6 +9706,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9308
9706
|
}
|
|
9309
9707
|
if (context.constitution) {
|
|
9310
9708
|
console.log(chalk17.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
9709
|
+
if (context.constitution.length > 6e3) {
|
|
9710
|
+
console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
9711
|
+
}
|
|
9311
9712
|
} else {
|
|
9312
9713
|
console.log(chalk17.yellow(" Constitution : not found \u2014 auto-generating..."));
|
|
9313
9714
|
try {
|
|
@@ -9357,12 +9758,32 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9357
9758
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
9358
9759
|
}
|
|
9359
9760
|
const featureSlug = slugify(idea);
|
|
9360
|
-
|
|
9361
|
-
|
|
9761
|
+
const minScore = config2.minSpecScore ?? 0;
|
|
9762
|
+
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
9763
|
+
if (shouldRunAssessment) {
|
|
9764
|
+
if (!opts.auto) {
|
|
9765
|
+
console.log(chalk17.blue("\n[3.4/6] Spec quality assessment..."));
|
|
9766
|
+
}
|
|
9362
9767
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
9363
9768
|
if (assessment) {
|
|
9364
|
-
printSpecAssessment(assessment);
|
|
9365
|
-
|
|
9769
|
+
if (!opts.auto) printSpecAssessment(assessment);
|
|
9770
|
+
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
9771
|
+
if (opts.force) {
|
|
9772
|
+
console.log(chalk17.yellow(`
|
|
9773
|
+
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
9774
|
+
} else {
|
|
9775
|
+
console.log(chalk17.red(`
|
|
9776
|
+
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
9777
|
+
if (!opts.auto) {
|
|
9778
|
+
console.log(chalk17.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
9779
|
+
} else {
|
|
9780
|
+
console.log(chalk17.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
9781
|
+
}
|
|
9782
|
+
console.log(chalk17.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
|
|
9783
|
+
process.exit(1);
|
|
9784
|
+
}
|
|
9785
|
+
}
|
|
9786
|
+
} else if (!opts.auto) {
|
|
9366
9787
|
console.log(chalk17.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
9367
9788
|
}
|
|
9368
9789
|
}
|
|
@@ -9696,7 +10117,7 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
9696
10117
|
program.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option(
|
|
9697
10118
|
"--codegen <mode>",
|
|
9698
10119
|
"Default code generation mode (claude-code|api|plan)"
|
|
9699
|
-
).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
10120
|
+
).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
9700
10121
|
const currentDir = process.cwd();
|
|
9701
10122
|
const configPath = path21.join(currentDir, CONFIG_FILE);
|
|
9702
10123
|
if (opts.clearKeys) {
|
|
@@ -9746,6 +10167,14 @@ File: ${KEY_STORE_FILE}`));
|
|
|
9746
10167
|
if (opts.codegen) updated.codegen = opts.codegen;
|
|
9747
10168
|
if (opts.codegenProvider) updated.codegenProvider = opts.codegenProvider;
|
|
9748
10169
|
if (opts.codegenModel) updated.codegenModel = opts.codegenModel;
|
|
10170
|
+
if (opts.minSpecScore !== void 0) {
|
|
10171
|
+
const score = parseInt(opts.minSpecScore, 10);
|
|
10172
|
+
if (isNaN(score) || score < 0 || score > 10) {
|
|
10173
|
+
console.error(chalk17.red(" --min-spec-score must be a number between 0 and 10"));
|
|
10174
|
+
process.exit(1);
|
|
10175
|
+
}
|
|
10176
|
+
updated.minSpecScore = score;
|
|
10177
|
+
}
|
|
9749
10178
|
await fs22.writeJson(configPath, updated, { spaces: 2 });
|
|
9750
10179
|
console.log(chalk17.green(`\u2714 Config saved to ${configPath}`));
|
|
9751
10180
|
console.log(JSON.stringify(updated, null, 2));
|
|
@@ -9889,6 +10318,9 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
9889
10318
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
9890
10319
|
console.log(chalk17.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
9891
10320
|
console.log(chalk17.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10321
|
+
if (context.constitution && context.constitution.length > 6e3) {
|
|
10322
|
+
console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10323
|
+
}
|
|
9892
10324
|
if (!context.constitution) {
|
|
9893
10325
|
console.log(chalk17.yellow(` Constitution: not found \u2014 auto-generating...`));
|
|
9894
10326
|
try {
|
|
@@ -10109,7 +10541,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10109
10541
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
10110
10542
|
if (!repoConfig) {
|
|
10111
10543
|
console.log(chalk17.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
|
|
10112
|
-
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null });
|
|
10544
|
+
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
10113
10545
|
continue;
|
|
10114
10546
|
}
|
|
10115
10547
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
|
|
@@ -10160,11 +10592,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10160
10592
|
contractDsls.set(repoReq.repoName, dsl);
|
|
10161
10593
|
console.log(chalk17.green(` Contract stored for downstream repos.`));
|
|
10162
10594
|
}
|
|
10163
|
-
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl });
|
|
10595
|
+
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
10164
10596
|
console.log(chalk17.green(` \u2714 ${repoReq.repoName} complete`));
|
|
10165
10597
|
} catch (err) {
|
|
10166
10598
|
console.error(chalk17.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
|
|
10167
|
-
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null });
|
|
10599
|
+
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
10168
10600
|
}
|
|
10169
10601
|
}
|
|
10170
10602
|
console.log(chalk17.bold.green("\n\u2714 Multi-repo pipeline complete!"));
|
|
@@ -10176,6 +10608,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10176
10608
|
const specInfo = r.specFile ? chalk17.gray(` \u2192 ${r.specFile}`) : "";
|
|
10177
10609
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
10178
10610
|
}
|
|
10611
|
+
return results;
|
|
10179
10612
|
}
|
|
10180
10613
|
var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
|
|
10181
10614
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
@@ -10373,6 +10806,9 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
10373
10806
|
console.log(chalk17.gray(" Loading project context..."));
|
|
10374
10807
|
const loader = new ContextLoader(currentDir);
|
|
10375
10808
|
const context = await loader.loadProjectContext();
|
|
10809
|
+
if (context.constitution && context.constitution.length > 6e3) {
|
|
10810
|
+
console.log(chalk17.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10811
|
+
}
|
|
10376
10812
|
const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
|
|
10377
10813
|
const { type: repoType } = await _detectRepoType(currentDir);
|
|
10378
10814
|
const updater = new SpecUpdater(provider);
|
|
@@ -10408,7 +10844,7 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
10408
10844
|
const specContent = await fs22.readFile(result.newSpecPath, "utf-8");
|
|
10409
10845
|
const constitutionSection = context.constitution ? `
|
|
10410
10846
|
=== Project Constitution (MUST follow) ===
|
|
10411
|
-
${context.constitution
|
|
10847
|
+
${context.constitution}
|
|
10412
10848
|
` : "";
|
|
10413
10849
|
const dslSection = result.updatedDsl ? `
|
|
10414
10850
|
=== DSL Context ===
|
|
@@ -10493,13 +10929,20 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
10493
10929
|
process.exit(1);
|
|
10494
10930
|
}
|
|
10495
10931
|
});
|
|
10496
|
-
program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option(
|
|
10497
|
-
"--workspace",
|
|
10498
|
-
"Generate mock assets for all backend repos in the workspace"
|
|
10499
|
-
).action(async (opts) => {
|
|
10932
|
+
program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option("--workspace", "Generate mock assets for all backend repos in the workspace").option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)").option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)").option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)").action(async (opts) => {
|
|
10500
10933
|
const currentDir = process.cwd();
|
|
10501
10934
|
const port = parseInt(opts.port, 10) || 3001;
|
|
10502
10935
|
console.log(chalk17.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10936
|
+
if (opts.restore) {
|
|
10937
|
+
const frontendDir = opts.frontend ? path21.resolve(opts.frontend) : currentDir;
|
|
10938
|
+
const r = await restoreMockProxy(frontendDir);
|
|
10939
|
+
if (r.restored) {
|
|
10940
|
+
console.log(chalk17.green(" \u2714 Proxy restored and mock server stopped."));
|
|
10941
|
+
} else {
|
|
10942
|
+
console.log(chalk17.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
10943
|
+
}
|
|
10944
|
+
return;
|
|
10945
|
+
}
|
|
10503
10946
|
if (opts.workspace) {
|
|
10504
10947
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10505
10948
|
const workspaceConfig = await workspaceLoader.load();
|
|
@@ -10565,11 +11008,43 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
10565
11008
|
console.log(chalk17.green(` ${f.path}`));
|
|
10566
11009
|
console.log(chalk17.gray(` ${f.description}`));
|
|
10567
11010
|
}
|
|
11011
|
+
if (opts.serve) {
|
|
11012
|
+
const serverJsPath = path21.join(currentDir, "mock", "server.js");
|
|
11013
|
+
if (!await fs22.pathExists(serverJsPath)) {
|
|
11014
|
+
console.error(chalk17.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11015
|
+
process.exit(1);
|
|
11016
|
+
}
|
|
11017
|
+
const pid = startMockServerBackground(serverJsPath, port);
|
|
11018
|
+
console.log(chalk17.green(`
|
|
11019
|
+
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11020
|
+
if (opts.frontend) {
|
|
11021
|
+
const frontendDir = path21.resolve(opts.frontend);
|
|
11022
|
+
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11023
|
+
await saveMockServerPid(frontendDir, pid);
|
|
11024
|
+
if (proxyResult.applied) {
|
|
11025
|
+
console.log(chalk17.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
11026
|
+
console.log(chalk17.bold.cyan(`
|
|
11027
|
+
Ready! Open a new terminal and run:`));
|
|
11028
|
+
console.log(chalk17.white(` cd ${frontendDir}`));
|
|
11029
|
+
console.log(chalk17.white(` ${proxyResult.devCommand}`));
|
|
11030
|
+
console.log(chalk17.gray(`
|
|
11031
|
+
When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
11032
|
+
} else {
|
|
11033
|
+
console.log(chalk17.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
11034
|
+
if (proxyResult.note) console.log(chalk17.gray(` ${proxyResult.note}`));
|
|
11035
|
+
}
|
|
11036
|
+
} else {
|
|
11037
|
+
console.log(chalk17.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
11038
|
+
console.log(chalk17.gray(` Mock server: http://localhost:${port}`));
|
|
11039
|
+
}
|
|
11040
|
+
return;
|
|
11041
|
+
}
|
|
10568
11042
|
console.log(chalk17.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10569
11043
|
console.log(chalk17.white(` 1. Install express (if not already):`));
|
|
10570
11044
|
console.log(chalk17.gray(` npm install --save-dev express`));
|
|
10571
11045
|
console.log(chalk17.white(` 2. Start mock server:`));
|
|
10572
11046
|
console.log(chalk17.gray(` node mock/server.js`));
|
|
11047
|
+
console.log(chalk17.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
10573
11048
|
console.log(chalk17.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
10574
11049
|
console.log(chalk17.gray(` http://localhost:${port}`));
|
|
10575
11050
|
if (opts.proxy) {
|
|
@@ -10581,6 +11056,27 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
10581
11056
|
console.log(chalk17.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
10582
11057
|
}
|
|
10583
11058
|
});
|
|
11059
|
+
program.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
|
|
11060
|
+
const currentDir = process.cwd();
|
|
11061
|
+
if (!lesson) {
|
|
11062
|
+
const { default: inquirerInput } = await import("@inquirer/prompts").then(
|
|
11063
|
+
(m) => ({ default: m.input })
|
|
11064
|
+
);
|
|
11065
|
+
lesson = await inquirerInput({
|
|
11066
|
+
message: "What lesson or engineering decision should be recorded?",
|
|
11067
|
+
validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
|
|
11068
|
+
});
|
|
11069
|
+
}
|
|
11070
|
+
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
11071
|
+
if (result.appended) {
|
|
11072
|
+
console.log(chalk17.green(`
|
|
11073
|
+
\u2714 Lesson appended to constitution \xA79`));
|
|
11074
|
+
console.log(chalk17.gray(` File: .ai-spec-constitution.md`));
|
|
11075
|
+
} else {
|
|
11076
|
+
console.log(chalk17.yellow(`
|
|
11077
|
+
\u26A0 Not appended: ${result.reason}`));
|
|
11078
|
+
}
|
|
11079
|
+
});
|
|
10584
11080
|
if (process.argv.length <= 2) {
|
|
10585
11081
|
(async () => {
|
|
10586
11082
|
const currentDir = process.cwd();
|