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
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs-extra";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import {
|
|
6
|
+
AIProvider,
|
|
7
|
+
createProvider,
|
|
8
|
+
DEFAULT_MODELS,
|
|
9
|
+
} from "../../core/spec-generator";
|
|
10
|
+
import { ContextLoader, isFrontendDeps } from "../../core/context-loader";
|
|
11
|
+
import { SpecRefiner } from "../../core/spec-refiner";
|
|
12
|
+
import { CodeGenerator, CodeGenMode } from "../../core/code-generator";
|
|
13
|
+
import { CodeReviewer, extractComplianceScore, extractMissingCount } from "../../core/reviewer";
|
|
14
|
+
import { GitWorktreeManager } from "../../git/worktree";
|
|
15
|
+
import { ConstitutionGenerator } from "../../core/constitution-generator";
|
|
16
|
+
import { TaskGenerator, printTasks } from "../../core/task-generator";
|
|
17
|
+
import { generateSpecWithTasks } from "../../core/combined-generator";
|
|
18
|
+
import {
|
|
19
|
+
slugify,
|
|
20
|
+
findLatestVersion,
|
|
21
|
+
nextVersionPath,
|
|
22
|
+
computeDiff,
|
|
23
|
+
printDiff,
|
|
24
|
+
printDiffSummary,
|
|
25
|
+
} from "../../core/spec-versioning";
|
|
26
|
+
import { DslExtractor } from "../../core/dsl-extractor";
|
|
27
|
+
import { TestGenerator } from "../../core/test-generator";
|
|
28
|
+
import { runErrorFeedback } from "../../core/error-feedback";
|
|
29
|
+
import { assessSpec, printSpecAssessment } from "../../core/spec-assessor";
|
|
30
|
+
import { accumulateReviewKnowledge, maybeAutoConsolidate } from "../../core/knowledge-memory";
|
|
31
|
+
import { detectRepoType } from "../../core/workspace-loader";
|
|
32
|
+
import { SpecDSL } from "../../core/dsl-types";
|
|
33
|
+
import { generateRunId, RunLogger, setActiveLogger } from "../../core/run-logger";
|
|
34
|
+
import { RunSnapshot, setActiveSnapshot } from "../../core/run-snapshot";
|
|
35
|
+
import { computePromptHash } from "../../core/prompt-hasher";
|
|
36
|
+
import { runSelfEval, printSelfEval } from "../../core/self-evaluator";
|
|
37
|
+
import { extractSpecRequirements, checkDslCoverage } from "../../core/dsl-coverage-checker";
|
|
38
|
+
import {
|
|
39
|
+
assessDslRichness,
|
|
40
|
+
buildDslGapRefinementPrompt,
|
|
41
|
+
extractStructuralFindings,
|
|
42
|
+
buildStructuralAmendmentPrompt,
|
|
43
|
+
printDslGaps,
|
|
44
|
+
printStructuralFindings,
|
|
45
|
+
} from "../../core/dsl-feedback";
|
|
46
|
+
import { DesignDialogue } from "../../core/design-dialogue";
|
|
47
|
+
import { AiSpecConfig, resolveApiKey } from "../utils";
|
|
48
|
+
import {
|
|
49
|
+
VcrRecordingProvider,
|
|
50
|
+
VcrReplayProvider,
|
|
51
|
+
loadVcrRecording,
|
|
52
|
+
} from "../../core/vcr";
|
|
53
|
+
import { printBanner } from "./helpers";
|
|
54
|
+
|
|
55
|
+
// ─── Pipeline Options ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface SingleRepoPipelineOpts {
|
|
58
|
+
provider?: string;
|
|
59
|
+
model?: string;
|
|
60
|
+
key?: string;
|
|
61
|
+
codegen?: string;
|
|
62
|
+
codegenProvider?: string;
|
|
63
|
+
codegenModel?: string;
|
|
64
|
+
codegenKey?: string;
|
|
65
|
+
fast?: boolean;
|
|
66
|
+
auto?: boolean;
|
|
67
|
+
force?: boolean;
|
|
68
|
+
tdd?: boolean;
|
|
69
|
+
resume?: boolean;
|
|
70
|
+
skipTasks?: boolean;
|
|
71
|
+
skipDsl?: boolean;
|
|
72
|
+
skipTests?: boolean;
|
|
73
|
+
skipReview?: boolean;
|
|
74
|
+
skipAssessment?: boolean;
|
|
75
|
+
skipErrorFeedback?: boolean;
|
|
76
|
+
skipWorktree?: boolean;
|
|
77
|
+
worktree?: boolean;
|
|
78
|
+
vcrRecord?: boolean;
|
|
79
|
+
vcrReplay?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Single-repo pipeline ────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export async function runSingleRepoPipeline(
|
|
85
|
+
idea: string,
|
|
86
|
+
opts: SingleRepoPipelineOpts,
|
|
87
|
+
currentDir: string,
|
|
88
|
+
config: AiSpecConfig
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
// ── Resolve spec provider ───────────────────────────────────────────────
|
|
91
|
+
const specProviderName = opts.provider || config.provider || "gemini";
|
|
92
|
+
const specModelName =
|
|
93
|
+
opts.model || config.model || DEFAULT_MODELS[specProviderName];
|
|
94
|
+
const specApiKey = await resolveApiKey(specProviderName, opts.key);
|
|
95
|
+
|
|
96
|
+
// ── Resolve codegen ─────────────────────────────────────────────────────
|
|
97
|
+
const codegenMode: CodeGenMode =
|
|
98
|
+
(opts.codegen as CodeGenMode) || config.codegen || "claude-code";
|
|
99
|
+
const codegenProviderName =
|
|
100
|
+
opts.codegenProvider || config.codegenProvider || specProviderName;
|
|
101
|
+
const codegenModelName =
|
|
102
|
+
opts.codegenModel ||
|
|
103
|
+
config.codegenModel ||
|
|
104
|
+
DEFAULT_MODELS[codegenProviderName];
|
|
105
|
+
const codegenApiKey =
|
|
106
|
+
codegenProviderName === specProviderName
|
|
107
|
+
? specApiKey
|
|
108
|
+
: await resolveApiKey(codegenProviderName, opts.codegenKey);
|
|
109
|
+
|
|
110
|
+
// ── VCR: replay mode — load recording and create replay providers ───────
|
|
111
|
+
let vcrReplayProvider: VcrReplayProvider | null = null;
|
|
112
|
+
if (opts.vcrReplay) {
|
|
113
|
+
const recording = await loadVcrRecording(currentDir, opts.vcrReplay);
|
|
114
|
+
if (!recording) {
|
|
115
|
+
console.error(chalk.red(`VCR recording not found: ${opts.vcrReplay}`));
|
|
116
|
+
console.error(chalk.gray(` Expected: .ai-spec-vcr/${opts.vcrReplay}.json`));
|
|
117
|
+
console.error(chalk.gray(` List available recordings: ai-spec vcr list`));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
vcrReplayProvider = new VcrReplayProvider(recording);
|
|
121
|
+
console.log(chalk.cyan(`\n[VCR] Replay mode — ${recording.entryCount} recorded responses loaded`));
|
|
122
|
+
console.log(chalk.gray(` Recording: ${opts.vcrReplay} (${recording.recordedAt.slice(0, 10)})`));
|
|
123
|
+
console.log(chalk.gray(` No API calls will be made during this run.\n`));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── VCR: record mode — wrap providers ────────────────────────────────────
|
|
127
|
+
let specVcrRecorder: VcrRecordingProvider | null = null;
|
|
128
|
+
let codegenVcrRecorder: VcrRecordingProvider | null = null;
|
|
129
|
+
|
|
130
|
+
printBanner({
|
|
131
|
+
specProvider: vcrReplayProvider ? "vcr-replay" : specProviderName,
|
|
132
|
+
specModel: vcrReplayProvider ? opts.vcrReplay! : specModelName,
|
|
133
|
+
codegenMode,
|
|
134
|
+
codegenProvider: vcrReplayProvider ? "vcr-replay" : codegenProviderName,
|
|
135
|
+
codegenModel: vcrReplayProvider ? opts.vcrReplay! : codegenModelName,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── Run tracking ────────────────────────────────────────────────────────
|
|
139
|
+
const runId = generateRunId();
|
|
140
|
+
console.log(chalk.gray(` Run ID: ${runId}`));
|
|
141
|
+
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
142
|
+
setActiveSnapshot(runSnapshot);
|
|
143
|
+
const runLogger = new RunLogger(currentDir, runId, {
|
|
144
|
+
provider: specProviderName,
|
|
145
|
+
model: specModelName,
|
|
146
|
+
});
|
|
147
|
+
setActiveLogger(runLogger);
|
|
148
|
+
|
|
149
|
+
const promptHash = computePromptHash();
|
|
150
|
+
runLogger.setPromptHash(promptHash);
|
|
151
|
+
|
|
152
|
+
// ── Step 1: Context ─────────────────────────────────────────────────────
|
|
153
|
+
console.log(chalk.blue("[1/6] Loading project context..."));
|
|
154
|
+
runLogger.stageStart("context_load");
|
|
155
|
+
const loader = new ContextLoader(currentDir);
|
|
156
|
+
const context = await loader.loadProjectContext();
|
|
157
|
+
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
158
|
+
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
159
|
+
console.log(chalk.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
160
|
+
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
161
|
+
console.log(chalk.gray(` API files : ${context.apiStructure.length} files`));
|
|
162
|
+
if (context.schema) {
|
|
163
|
+
console.log(chalk.gray(` Prisma schema: found`));
|
|
164
|
+
}
|
|
165
|
+
if (context.constitution) {
|
|
166
|
+
console.log(chalk.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
167
|
+
if (context.constitution.length > 6000) {
|
|
168
|
+
console.log(chalk.yellow(` ⚠ Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
console.log(chalk.yellow(" Constitution : not found — auto-generating..."));
|
|
172
|
+
try {
|
|
173
|
+
const constitutionGen = new ConstitutionGenerator(
|
|
174
|
+
createProvider(specProviderName, specApiKey, specModelName)
|
|
175
|
+
);
|
|
176
|
+
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
177
|
+
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
178
|
+
context.constitution = constitutionContent;
|
|
179
|
+
console.log(chalk.green(` Constitution : ✔ generated and saved (.ai-spec-constitution.md)`));
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.log(chalk.yellow(` Constitution : ⚠ auto-generation failed (${(err as Error).message}), continuing without it.`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Step 1.5: Design Options Dialogue (skip in --fast / --auto / --vcr-replay) ──
|
|
186
|
+
let architectureDecision: string | undefined;
|
|
187
|
+
if (!opts.fast && !opts.auto && !opts.vcrReplay) {
|
|
188
|
+
runLogger.stageStart("design_dialogue");
|
|
189
|
+
const dialogue = new DesignDialogue(
|
|
190
|
+
vcrReplayProvider ?? createProvider(specProviderName, specApiKey, specModelName)
|
|
191
|
+
);
|
|
192
|
+
const choice = await dialogue.run(idea, {
|
|
193
|
+
techStack: context.techStack,
|
|
194
|
+
repoType: detectedRepoType,
|
|
195
|
+
constitution: context.constitution ?? undefined,
|
|
196
|
+
});
|
|
197
|
+
architectureDecision = choice.selectedApproach ?? undefined;
|
|
198
|
+
runLogger.stageEnd("design_dialogue", {
|
|
199
|
+
skipped: !choice.selectedApproach,
|
|
200
|
+
approach: choice.selectedApproach?.slice(0, 80),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Step 2: Spec + Tasks Generation (single AI call) ───────────────────
|
|
205
|
+
console.log(chalk.blue(`\n[2/6] Generating spec with ${specProviderName}/${specModelName}...`));
|
|
206
|
+
let specProvider: AIProvider = vcrReplayProvider ?? createProvider(specProviderName, specApiKey, specModelName);
|
|
207
|
+
if (!vcrReplayProvider && opts.vcrRecord) {
|
|
208
|
+
specVcrRecorder = new VcrRecordingProvider(specProvider);
|
|
209
|
+
specProvider = specVcrRecorder;
|
|
210
|
+
console.log(chalk.cyan(` [VCR] Recording spec AI calls → .ai-spec-vcr/${runId}.json`));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let initialSpec: string;
|
|
214
|
+
let initialTasks: import("../../core/task-generator").SpecTask[] = [];
|
|
215
|
+
|
|
216
|
+
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
217
|
+
try {
|
|
218
|
+
if (opts.skipTasks) {
|
|
219
|
+
const { SpecGenerator } = await import("../../core/spec-generator");
|
|
220
|
+
const generator = new SpecGenerator(specProvider);
|
|
221
|
+
initialSpec = await generator.generateSpec(idea, context, architectureDecision);
|
|
222
|
+
console.log(chalk.green(" ✔ Spec generated."));
|
|
223
|
+
} else {
|
|
224
|
+
const result = await generateSpecWithTasks(specProvider, idea, context, architectureDecision);
|
|
225
|
+
initialSpec = result.spec;
|
|
226
|
+
initialTasks = result.tasks;
|
|
227
|
+
console.log(chalk.green(` ✔ Spec generated.`));
|
|
228
|
+
if (initialTasks.length > 0) {
|
|
229
|
+
console.log(chalk.green(` ✔ ${initialTasks.length} tasks generated (combined call).`));
|
|
230
|
+
} else {
|
|
231
|
+
console.log(chalk.yellow(" ⚠ Tasks not parsed from response — will retry separately after refinement."));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
runLogger.stageFail("spec_gen", (err as Error).message);
|
|
237
|
+
console.error(chalk.red(" ✘ Spec generation failed:"), err);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Step 3: Interactive Refinement ──────────────────────────────────────
|
|
242
|
+
let finalSpec: string;
|
|
243
|
+
if (opts.fast) {
|
|
244
|
+
console.log(chalk.gray("\n[3/6] Skipping refinement (--fast)."));
|
|
245
|
+
finalSpec = initialSpec;
|
|
246
|
+
} else {
|
|
247
|
+
console.log(chalk.blue("\n[3/6] Interactive spec refinement..."));
|
|
248
|
+
runLogger.stageStart("spec_refine");
|
|
249
|
+
const refiner = new SpecRefiner(specProvider);
|
|
250
|
+
finalSpec = await refiner.refineLoop(initialSpec);
|
|
251
|
+
runLogger.stageEnd("spec_refine");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const featureSlug = slugify(idea);
|
|
255
|
+
|
|
256
|
+
// ── Step 3.4: Spec Quality Pre-Assessment ──────────────────────────────
|
|
257
|
+
const minScore = config.minSpecScore ?? 0;
|
|
258
|
+
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
259
|
+
|
|
260
|
+
if (shouldRunAssessment) {
|
|
261
|
+
if (!opts.auto) {
|
|
262
|
+
console.log(chalk.blue("\n[3.4/6] Spec quality assessment..."));
|
|
263
|
+
}
|
|
264
|
+
runLogger.stageStart("spec_assess");
|
|
265
|
+
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? undefined);
|
|
266
|
+
if (assessment) {
|
|
267
|
+
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
268
|
+
if (!opts.auto) printSpecAssessment(assessment);
|
|
269
|
+
|
|
270
|
+
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
271
|
+
if (opts.force) {
|
|
272
|
+
console.log(chalk.yellow(`\n ⚠ Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 — bypassed with --force.`));
|
|
273
|
+
} else {
|
|
274
|
+
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
275
|
+
console.log(chalk.red(`\n ✘ Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
276
|
+
if (!opts.auto) {
|
|
277
|
+
console.log(chalk.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
278
|
+
} else {
|
|
279
|
+
console.log(chalk.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
280
|
+
}
|
|
281
|
+
console.log(chalk.gray(` Gate threshold set in .ai-spec.json → "minSpecScore": ${minScore}`));
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
287
|
+
if (!opts.auto) {
|
|
288
|
+
console.log(chalk.gray(" (Assessment skipped — AI call failed or timed out)"));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Step 3.5: Approval Gate ─────────────────────────────────────────────
|
|
294
|
+
if (!opts.auto) {
|
|
295
|
+
console.log(chalk.blue("\n[3.5/6] Approval Gate — review before code generation"));
|
|
296
|
+
|
|
297
|
+
const specLines = finalSpec.split("\n").length;
|
|
298
|
+
const specWords = finalSpec.split(/\s+/).length;
|
|
299
|
+
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
300
|
+
console.log(chalk.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
301
|
+
if (taskCountHint) console.log(chalk.gray(taskCountHint));
|
|
302
|
+
|
|
303
|
+
// Estimate DSL scope from spec text (no AI needed — regex on § headings)
|
|
304
|
+
const endpointMatches = finalSpec.match(/^\s*[-*]\s+`?(GET|POST|PUT|PATCH|DELETE)\s+\//gim);
|
|
305
|
+
const modelMatches = finalSpec.match(/^#{1,4}\s+\w.*model|^[-*]\s+\*\*\w+\*\*\s*[:(]/gim);
|
|
306
|
+
const estimatedEndpoints = endpointMatches?.length ?? 0;
|
|
307
|
+
const estimatedModels = modelMatches?.length ?? 0;
|
|
308
|
+
const estimatedFiles = Math.max(3, estimatedEndpoints + estimatedModels + 2);
|
|
309
|
+
if (estimatedEndpoints > 0 || estimatedModels > 0) {
|
|
310
|
+
console.log(chalk.cyan(` Est. DSL scope : ~${estimatedEndpoints} endpoint(s), ~${estimatedModels} model(s) → ~${estimatedFiles} files`));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const previewSpecsDir = path.join(currentDir, "specs");
|
|
314
|
+
const slug = featureSlug;
|
|
315
|
+
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
316
|
+
if (prevVersion) {
|
|
317
|
+
console.log(chalk.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
|
|
318
|
+
const diff = computeDiff(prevVersion.content, finalSpec);
|
|
319
|
+
console.log(chalk.cyan("\n ── Changes vs previous version ──────────────"));
|
|
320
|
+
printDiffSummary(diff, `v${prevVersion.version} → v${prevVersion.version + 1}`);
|
|
321
|
+
printDiff(diff);
|
|
322
|
+
console.log(chalk.cyan(" ────────────────────────────────────────────"));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const gate = await select({
|
|
326
|
+
message: "Ready to proceed to code generation?",
|
|
327
|
+
choices: [
|
|
328
|
+
{ name: "✅ Proceed — start code generation", value: "proceed" },
|
|
329
|
+
{ name: "📋 View full spec", value: "view" },
|
|
330
|
+
{ name: "❌ Abort", value: "abort" },
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (gate === "view") {
|
|
335
|
+
console.log(chalk.cyan("\n" + "─".repeat(52)));
|
|
336
|
+
console.log(finalSpec);
|
|
337
|
+
console.log(chalk.cyan("─".repeat(52) + "\n"));
|
|
338
|
+
|
|
339
|
+
const confirm2 = await select({
|
|
340
|
+
message: "Proceed to code generation?",
|
|
341
|
+
choices: [
|
|
342
|
+
{ name: "✅ Proceed", value: "proceed" },
|
|
343
|
+
{ name: "❌ Abort", value: "abort" },
|
|
344
|
+
],
|
|
345
|
+
});
|
|
346
|
+
if (confirm2 === "abort") {
|
|
347
|
+
console.log(chalk.yellow(" Aborted. Spec was NOT saved."));
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
} else if (gate === "abort") {
|
|
351
|
+
console.log(chalk.yellow(" Aborted. Spec was NOT saved."));
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(chalk.green(" ✔ Approved — continuing to code generation."));
|
|
356
|
+
} else {
|
|
357
|
+
console.log(chalk.gray("[3.5/6] Approval Gate: skipped (--auto)."));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Step 3.8: DSL Extraction + Validation ──────────────────────────────
|
|
361
|
+
let extractedDsl: SpecDSL | null = null;
|
|
362
|
+
|
|
363
|
+
if (opts.skipDsl) {
|
|
364
|
+
console.log(chalk.gray("\n[DSL] Skipped (--skip-dsl)."));
|
|
365
|
+
} else {
|
|
366
|
+
console.log(chalk.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
367
|
+
console.log(chalk.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
368
|
+
runLogger.stageStart("dsl_extract");
|
|
369
|
+
try {
|
|
370
|
+
const isFrontend = isFrontendDeps(context.dependencies);
|
|
371
|
+
if (isFrontend) console.log(chalk.gray(" Frontend project detected — using ComponentSpec extractor"));
|
|
372
|
+
const dslExtractor = new DslExtractor(specProvider);
|
|
373
|
+
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
374
|
+
if (extractedDsl) {
|
|
375
|
+
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
376
|
+
console.log(chalk.green(" ✔ DSL extracted and validated."));
|
|
377
|
+
} else {
|
|
378
|
+
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
379
|
+
console.log(chalk.yellow(" ⚠ DSL skipped — codegen will use Spec + Tasks only."));
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
runLogger.stageFail("dsl_extract", (err as Error).message);
|
|
383
|
+
console.log(chalk.yellow(` ⚠ DSL extraction error: ${(err as Error).message} — continuing without DSL.`));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Loop 1: DSL Gap Feedback ────────────────────────────────────────────
|
|
388
|
+
if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
|
|
389
|
+
const dslGaps = assessDslRichness(extractedDsl);
|
|
390
|
+
|
|
391
|
+
// Spec↔DSL coverage check: detect uncovered requirements
|
|
392
|
+
const specReqs = extractSpecRequirements(finalSpec);
|
|
393
|
+
if (specReqs.length > 0) {
|
|
394
|
+
const coverage = checkDslCoverage(specReqs, extractedDsl);
|
|
395
|
+
if (coverage.coverageRatio < 0.8) {
|
|
396
|
+
for (const req of coverage.uncovered) {
|
|
397
|
+
dslGaps.push({
|
|
398
|
+
code: "uncovered_requirement",
|
|
399
|
+
message: `[${req.id}] Spec requirement not covered in DSL: "${req.text.slice(0, 80)}"`,
|
|
400
|
+
hint: `Add endpoints, models, or behaviors that implement: "${req.text.slice(0, 120)}"`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
console.log(
|
|
404
|
+
chalk.yellow(
|
|
405
|
+
` Spec↔DSL coverage: ${Math.round(coverage.coverageRatio * 100)}% (${coverage.uncovered.length} uncovered requirement(s))`
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
console.log(
|
|
410
|
+
chalk.gray(` Spec↔DSL coverage: ${Math.round(coverage.coverageRatio * 100)}% — OK`)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (dslGaps.length > 0) {
|
|
416
|
+
printDslGaps(dslGaps);
|
|
417
|
+
runLogger.stageStart("dsl_gap_feedback", { gapCount: dslGaps.length, gaps: dslGaps.map((g) => g.code) });
|
|
418
|
+
|
|
419
|
+
const refineChoice = await select({
|
|
420
|
+
message: "How would you like to proceed?",
|
|
421
|
+
choices: [
|
|
422
|
+
{ name: "🔧 Refine spec (AI fills the gaps, then re-extract DSL)", value: "refine" },
|
|
423
|
+
{ name: "⏭ Skip — proceed with the current DSL", value: "skip" },
|
|
424
|
+
],
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (refineChoice === "refine") {
|
|
428
|
+
console.log(chalk.blue(" Refining spec to fill DSL gaps..."));
|
|
429
|
+
try {
|
|
430
|
+
const refinedSpec = await specProvider.generate(
|
|
431
|
+
buildDslGapRefinementPrompt(finalSpec, dslGaps),
|
|
432
|
+
"You are a Senior Tech Lead doing a targeted spec revision. Output only the complete revised Markdown spec."
|
|
433
|
+
);
|
|
434
|
+
finalSpec = refinedSpec;
|
|
435
|
+
console.log(chalk.green(" ✔ Spec refined."));
|
|
436
|
+
|
|
437
|
+
console.log(chalk.blue(" Re-extracting DSL from refined spec..."));
|
|
438
|
+
const isFrontend2 = isFrontendDeps(context.dependencies);
|
|
439
|
+
const reExtractor = new DslExtractor(specProvider);
|
|
440
|
+
const reExtractedDsl = await reExtractor.extract(finalSpec, { auto: true, isFrontend: isFrontend2 });
|
|
441
|
+
if (reExtractedDsl) {
|
|
442
|
+
extractedDsl = reExtractedDsl;
|
|
443
|
+
console.log(chalk.green(` ✔ DSL re-extracted: ${extractedDsl.endpoints.length} endpoint(s), ${extractedDsl.models.length} model(s).`));
|
|
444
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refined", endpoints: extractedDsl.endpoints.length, models: extractedDsl.models.length });
|
|
445
|
+
} else {
|
|
446
|
+
console.log(chalk.yellow(" ⚠ Re-extraction failed — keeping original DSL."));
|
|
447
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refined_but_reextract_failed" });
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.log(chalk.yellow(` ⚠ Spec refinement failed: ${(err as Error).message} — keeping original DSL.`));
|
|
451
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refinement_error", error: (err as Error).message });
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "skipped" });
|
|
455
|
+
console.log(chalk.gray(" Continuing with current DSL."));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── Step 4: Git Worktree ────────────────────────────────────────────────
|
|
461
|
+
const isFrontendProject = isFrontendDeps(context.dependencies ?? []);
|
|
462
|
+
const skipWorktree = opts.worktree
|
|
463
|
+
? false
|
|
464
|
+
: opts.skipWorktree || isFrontendProject;
|
|
465
|
+
|
|
466
|
+
let workingDir = currentDir;
|
|
467
|
+
if (!skipWorktree) {
|
|
468
|
+
console.log(chalk.blue("\n[4/6] Setting up git worktree..."));
|
|
469
|
+
const worktreeManager = new GitWorktreeManager(currentDir);
|
|
470
|
+
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
471
|
+
if (worktreePath) workingDir = worktreePath;
|
|
472
|
+
} else {
|
|
473
|
+
const reason = opts.worktree
|
|
474
|
+
? ""
|
|
475
|
+
: isFrontendProject
|
|
476
|
+
? " (frontend project — use --worktree to override)"
|
|
477
|
+
: " (--skip-worktree)";
|
|
478
|
+
console.log(chalk.gray(`[4/6] Skipping worktree${reason}.`));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Step 5: Save Spec (versioned) + Generate Tasks ──────────────────────
|
|
482
|
+
const specsDir = path.join(workingDir, "specs");
|
|
483
|
+
await fs.ensureDir(specsDir);
|
|
484
|
+
|
|
485
|
+
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
486
|
+
await fs.writeFile(specFile, finalSpec, "utf-8");
|
|
487
|
+
console.log(chalk.green(`\n[5/6] ✔ Spec saved: ${specFile}`) + chalk.gray(` (v${specVersion})`));
|
|
488
|
+
|
|
489
|
+
let savedDslFile: string | null = null;
|
|
490
|
+
if (extractedDsl) {
|
|
491
|
+
const dslExtractor = new DslExtractor(specProvider);
|
|
492
|
+
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
493
|
+
console.log(chalk.green(` ✔ DSL saved : ${savedDslFile}`));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (!opts.skipTasks) {
|
|
497
|
+
const taskGen = new TaskGenerator(specProvider);
|
|
498
|
+
let tasksToSave = initialTasks;
|
|
499
|
+
|
|
500
|
+
if (tasksToSave.length === 0) {
|
|
501
|
+
console.log(chalk.blue(`\n Generating tasks (separate call)...`));
|
|
502
|
+
try {
|
|
503
|
+
tasksToSave = await taskGen.generateTasks(finalSpec, context);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
console.log(chalk.yellow(` ⚠ Task generation failed: ${(err as Error).message}`));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (tasksToSave.length > 0) {
|
|
510
|
+
const sorted = taskGen.sortByLayer(tasksToSave);
|
|
511
|
+
const tasksFile = await taskGen.saveTasks(sorted, specFile);
|
|
512
|
+
printTasks(sorted);
|
|
513
|
+
console.log(chalk.green(` ✔ Tasks saved: ${tasksFile}`));
|
|
514
|
+
} else {
|
|
515
|
+
console.log(chalk.yellow(" ⚠ No tasks generated — code generation will use fallback file planning."));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ── Step 6: Code Generation ─────────────────────────────────────────────
|
|
520
|
+
console.log(chalk.blue(`\n[6/6] Code generation (mode: ${codegenMode})...`));
|
|
521
|
+
const rawCodegenProvider: AIProvider =
|
|
522
|
+
codegenProviderName === specProviderName && codegenApiKey === specApiKey
|
|
523
|
+
? specProvider
|
|
524
|
+
: createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
525
|
+
let codegenProvider: AIProvider;
|
|
526
|
+
if (!vcrReplayProvider && opts.vcrRecord && !(rawCodegenProvider instanceof VcrRecordingProvider)) {
|
|
527
|
+
// Different provider from spec — needs its own recorder
|
|
528
|
+
codegenVcrRecorder = new VcrRecordingProvider(rawCodegenProvider);
|
|
529
|
+
codegenProvider = codegenVcrRecorder;
|
|
530
|
+
console.log(chalk.cyan(` [VCR] Recording codegen AI calls → .ai-spec-vcr/${runId}.json`));
|
|
531
|
+
} else {
|
|
532
|
+
codegenProvider = rawCodegenProvider;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ── TDD: generate failing tests BEFORE implementation ──────────────────
|
|
536
|
+
let generatedTestFiles: string[] = [];
|
|
537
|
+
if (opts.tdd && extractedDsl) {
|
|
538
|
+
console.log(chalk.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
|
|
539
|
+
const testGen = new TestGenerator(codegenProvider);
|
|
540
|
+
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
runLogger.stageStart("codegen", { mode: codegenMode, provider: codegenProviderName, model: codegenModelName });
|
|
544
|
+
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
545
|
+
const generatedFiles = await codegen.generateCode(specFile, workingDir, context, {
|
|
546
|
+
auto: opts.auto,
|
|
547
|
+
resume: opts.resume,
|
|
548
|
+
dslFilePath: savedDslFile ?? undefined,
|
|
549
|
+
repoType: detectedRepoType,
|
|
550
|
+
});
|
|
551
|
+
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
552
|
+
|
|
553
|
+
// ── Step 7: Test Skeleton Generation ───────────────────────────────────
|
|
554
|
+
if (opts.tdd) {
|
|
555
|
+
console.log(chalk.gray("\n[7/9] TDD mode — test files already written pre-implementation."));
|
|
556
|
+
} else if (opts.skipTests) {
|
|
557
|
+
console.log(chalk.gray("\n[7/9] Skipping test generation (--skip-tests)."));
|
|
558
|
+
} else if (!extractedDsl) {
|
|
559
|
+
console.log(chalk.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
560
|
+
} else {
|
|
561
|
+
console.log(chalk.blue(`\n[7/9] Test skeleton generation...`));
|
|
562
|
+
runLogger.stageStart("test_gen");
|
|
563
|
+
const testGen = new TestGenerator(codegenProvider);
|
|
564
|
+
generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
|
|
565
|
+
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── Step 8: Error Feedback Loop ─────────────────────────────────────────
|
|
569
|
+
let compilePassed = false;
|
|
570
|
+
if (opts.skipErrorFeedback) {
|
|
571
|
+
console.log(chalk.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
572
|
+
compilePassed = true;
|
|
573
|
+
} else {
|
|
574
|
+
if (opts.tdd) {
|
|
575
|
+
console.log(chalk.cyan("[8/9] TDD mode — error feedback loop driving implementation to pass tests..."));
|
|
576
|
+
}
|
|
577
|
+
runLogger.stageStart("error_feedback");
|
|
578
|
+
const defaultCycles = opts.tdd ? 3 : 2;
|
|
579
|
+
const maxCycles = config.maxErrorCycles ?? defaultCycles;
|
|
580
|
+
compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
581
|
+
maxCycles,
|
|
582
|
+
generatedTestFiles,
|
|
583
|
+
});
|
|
584
|
+
runLogger.stageEnd("error_feedback");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── Step 9: Code Review ─────────────────────────────────────────────────
|
|
588
|
+
let reviewResult = "";
|
|
589
|
+
let accumulatePromise: Promise<void> | undefined;
|
|
590
|
+
if (!opts.skipReview) {
|
|
591
|
+
console.log(chalk.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
592
|
+
runLogger.stageStart("review");
|
|
593
|
+
const reviewer = new CodeReviewer(specProvider, workingDir);
|
|
594
|
+
const savedSpec = await fs.readFile(specFile, "utf-8");
|
|
595
|
+
|
|
596
|
+
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
597
|
+
reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
|
|
598
|
+
} else {
|
|
599
|
+
reviewResult = await reviewer.reviewCode(savedSpec, specFile);
|
|
600
|
+
}
|
|
601
|
+
runLogger.stageEnd("review");
|
|
602
|
+
|
|
603
|
+
// Surface Pass 0 compliance score
|
|
604
|
+
const complianceScore = extractComplianceScore(reviewResult);
|
|
605
|
+
const missingCount = extractMissingCount(reviewResult);
|
|
606
|
+
if (complianceScore > 0) {
|
|
607
|
+
const scoreColor = complianceScore >= 8 ? chalk.green : complianceScore >= 6 ? chalk.yellow : chalk.red;
|
|
608
|
+
console.log(
|
|
609
|
+
chalk.gray("\n Spec Compliance (Pass 0): ") +
|
|
610
|
+
scoreColor(`${complianceScore}/10`) +
|
|
611
|
+
(missingCount > 0
|
|
612
|
+
? chalk.red(` · ${missingCount} missing requirement(s) — see Blockers section above`)
|
|
613
|
+
: chalk.green(" · all requirements covered"))
|
|
614
|
+
);
|
|
615
|
+
runLogger.stageEnd("compliance_check", { complianceScore, missingCount });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Fire async — don't block the remaining pipeline steps
|
|
619
|
+
accumulatePromise = accumulateReviewKnowledge(specProvider, currentDir, reviewResult)
|
|
620
|
+
.then(() => {
|
|
621
|
+
maybeAutoConsolidate(specProvider, currentDir, {
|
|
622
|
+
threshold: config.autoConsolidateThreshold,
|
|
623
|
+
});
|
|
624
|
+
})
|
|
625
|
+
.catch((err) => console.log(chalk.yellow(` ⚠ §9 accumulation failed: ${(err as Error).message}`)));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── Loop 2: Review → DSL Structural Feedback ────────────────────────────
|
|
629
|
+
if (reviewResult && !opts.skipReview && !opts.auto && extractedDsl && savedDslFile) {
|
|
630
|
+
const structuralFindings = extractStructuralFindings(reviewResult);
|
|
631
|
+
|
|
632
|
+
if (structuralFindings.length > 0) {
|
|
633
|
+
printStructuralFindings(structuralFindings);
|
|
634
|
+
runLogger.stageStart("review_dsl_feedback", { findingCount: structuralFindings.length, categories: structuralFindings.map((f) => f.category) });
|
|
635
|
+
|
|
636
|
+
const savedSpecContent = await fs.readFile(specFile, "utf-8");
|
|
637
|
+
|
|
638
|
+
const patchChoice = await select({
|
|
639
|
+
message: "These are design issues in the Spec/DSL. How would you like to handle them?",
|
|
640
|
+
choices: [
|
|
641
|
+
{ name: "🔧 Amend spec + update DSL (AI fixes the design issues, no regen yet)", value: "amend" },
|
|
642
|
+
{ name: "📝 Note in §9 only (already done — no DSL change)", value: "note" },
|
|
643
|
+
{ name: "⏭ Skip", value: "skip" },
|
|
644
|
+
],
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
if (patchChoice === "amend") {
|
|
648
|
+
console.log(chalk.blue(" Amending spec to address structural findings..."));
|
|
649
|
+
try {
|
|
650
|
+
const amendedSpec = await specProvider.generate(
|
|
651
|
+
buildStructuralAmendmentPrompt(savedSpecContent, structuralFindings),
|
|
652
|
+
"You are a Senior Tech Lead doing a targeted spec correction. Output only the complete revised Markdown spec."
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
await runSnapshot.snapshotFile(specFile);
|
|
656
|
+
if (savedDslFile) await runSnapshot.snapshotFile(savedDslFile);
|
|
657
|
+
|
|
658
|
+
await fs.writeFile(specFile, amendedSpec, "utf-8");
|
|
659
|
+
console.log(chalk.green(` ✔ Spec updated: ${specFile}`));
|
|
660
|
+
|
|
661
|
+
console.log(chalk.blue(" Re-extracting DSL from amended spec..."));
|
|
662
|
+
const isFrontend3 = isFrontendDeps(context.dependencies);
|
|
663
|
+
const amendExtractor = new DslExtractor(specProvider);
|
|
664
|
+
const amendedDsl = await amendExtractor.extract(amendedSpec, { auto: true, isFrontend: isFrontend3 });
|
|
665
|
+
if (amendedDsl) {
|
|
666
|
+
const dslWriter = new DslExtractor(specProvider);
|
|
667
|
+
const newDslPath = await dslWriter.saveDsl(amendedDsl, specFile);
|
|
668
|
+
extractedDsl = amendedDsl;
|
|
669
|
+
console.log(chalk.green(` ✔ DSL updated: ${newDslPath}`));
|
|
670
|
+
console.log(chalk.cyan(
|
|
671
|
+
`\n Next step: run ${chalk.white("ai-spec update --codegen")} to regenerate files affected by the DSL change.`
|
|
672
|
+
));
|
|
673
|
+
runLogger.stageEnd("review_dsl_feedback", {
|
|
674
|
+
action: "amended",
|
|
675
|
+
endpoints: amendedDsl.endpoints.length,
|
|
676
|
+
models: amendedDsl.models.length,
|
|
677
|
+
});
|
|
678
|
+
} else {
|
|
679
|
+
console.log(chalk.yellow(" ⚠ DSL re-extraction failed — spec was updated but DSL file unchanged."));
|
|
680
|
+
runLogger.stageEnd("review_dsl_feedback", { action: "amended_spec_only" });
|
|
681
|
+
}
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.log(chalk.yellow(` ⚠ Spec amendment failed: ${(err as Error).message}`));
|
|
684
|
+
runLogger.stageEnd("review_dsl_feedback", { action: "amendment_error", error: (err as Error).message });
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
runLogger.stageEnd("review_dsl_feedback", { action: patchChoice });
|
|
688
|
+
if (patchChoice === "note") {
|
|
689
|
+
console.log(chalk.gray(" Structural findings retained in §9. DSL unchanged."));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ── Step 10: Harness Self-Evaluation ────────────────────────────────────
|
|
696
|
+
runLogger.stageStart("self_eval");
|
|
697
|
+
const selfEvalResult = runSelfEval({
|
|
698
|
+
dsl: extractedDsl,
|
|
699
|
+
generatedFiles,
|
|
700
|
+
compilePassed,
|
|
701
|
+
reviewText: reviewResult,
|
|
702
|
+
promptHash,
|
|
703
|
+
logger: runLogger,
|
|
704
|
+
});
|
|
705
|
+
printSelfEval(selfEvalResult);
|
|
706
|
+
|
|
707
|
+
// ── Harness Score Gate ─────────────────────────────────────────────────
|
|
708
|
+
const minHarness = config.minHarnessScore ?? 0;
|
|
709
|
+
if (minHarness > 0 && selfEvalResult.harnessScore < minHarness && !opts.force) {
|
|
710
|
+
console.log(chalk.red(
|
|
711
|
+
`\n ✘ Harness score ${selfEvalResult.harnessScore}/10 is below the minimum threshold ${minHarness}/10.`
|
|
712
|
+
));
|
|
713
|
+
console.log(chalk.gray(` Gate threshold set in .ai-spec.json → "minHarnessScore": ${minHarness}`));
|
|
714
|
+
console.log(chalk.gray(` Use --force to bypass, or improve the spec and re-run.`));
|
|
715
|
+
runLogger.stageEnd("self_eval", { gateBlocked: true, score: selfEvalResult.harnessScore, threshold: minHarness });
|
|
716
|
+
runLogger.finish();
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ── Await async §9 accumulation (fire-and-await pattern) ────────────────
|
|
721
|
+
if (accumulatePromise) await accumulatePromise;
|
|
722
|
+
|
|
723
|
+
// ── VCR: save recording ─────────────────────────────────────────────────
|
|
724
|
+
if (specVcrRecorder) {
|
|
725
|
+
const vcrPath = await specVcrRecorder.save(currentDir, runId, codegenVcrRecorder ?? undefined);
|
|
726
|
+
console.log(chalk.cyan(`[VCR] Recording saved: ${path.relative(currentDir, vcrPath)}`));
|
|
727
|
+
console.log(chalk.gray(` Replay with: ai-spec create --vcr-replay ${runId} <idea>`));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ── VCR: report prompt hash mismatches ──────────────────────────────────
|
|
731
|
+
if (vcrReplayProvider?.hasMismatches) {
|
|
732
|
+
console.log(chalk.yellow(`\n[VCR] ⚠ ${vcrReplayProvider.mismatches.length} prompt hash mismatch(es) detected during replay:`));
|
|
733
|
+
for (const m of vcrReplayProvider.mismatches) {
|
|
734
|
+
console.log(chalk.gray(` call #${m.index}: expected ${m.expected}, got ${m.actual}`));
|
|
735
|
+
}
|
|
736
|
+
console.log(chalk.yellow(" The pipeline structure may have changed since the recording was made."));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ── Done ────────────────────────────────────────────────────────────────
|
|
740
|
+
runLogger.finish();
|
|
741
|
+
console.log(chalk.bold.green("\n✔ All done!"));
|
|
742
|
+
console.log(chalk.gray(` Spec : ${specFile}`));
|
|
743
|
+
if (savedDslFile) console.log(chalk.gray(` DSL : ${savedDslFile}`));
|
|
744
|
+
if (generatedTestFiles.length > 0) {
|
|
745
|
+
console.log(chalk.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
|
|
746
|
+
}
|
|
747
|
+
console.log(chalk.gray(` Working dir : ${workingDir}`));
|
|
748
|
+
if (workingDir !== currentDir) {
|
|
749
|
+
console.log(chalk.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
750
|
+
}
|
|
751
|
+
runLogger.printSummary();
|
|
752
|
+
if (runSnapshot.fileCount > 0) {
|
|
753
|
+
console.log(chalk.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
754
|
+
}
|
|
755
|
+
}
|