ai-spec-dev 0.46.0 → 0.56.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.
Files changed (41) hide show
  1. package/README.md +60 -30
  2. package/cli/commands/config.ts +129 -1
  3. package/cli/commands/create.ts +14 -0
  4. package/cli/commands/fix-history.ts +176 -0
  5. package/cli/commands/init.ts +36 -1
  6. package/cli/index.ts +2 -6
  7. package/cli/pipeline/helpers.ts +6 -0
  8. package/cli/pipeline/multi-repo.ts +300 -26
  9. package/cli/pipeline/single-repo.ts +103 -2
  10. package/cli/utils.ts +23 -0
  11. package/core/code-generator.ts +63 -14
  12. package/core/cross-stack-verifier.ts +482 -0
  13. package/core/fix-history.ts +333 -0
  14. package/core/import-fixer.ts +827 -0
  15. package/core/import-verifier.ts +569 -0
  16. package/core/knowledge-memory.ts +55 -6
  17. package/core/self-evaluator.ts +44 -7
  18. package/core/spec-generator.ts +3 -3
  19. package/core/types-generator.ts +2 -2
  20. package/dist/cli/index.js +3968 -2353
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/index.mjs +3810 -2195
  23. package/dist/cli/index.mjs.map +1 -1
  24. package/dist/index.d.mts +14 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.js +249 -128
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +249 -128
  29. package/dist/index.mjs.map +1 -1
  30. package/package.json +2 -2
  31. package/tests/cross-stack-verifier.test.ts +402 -0
  32. package/tests/fix-history.test.ts +335 -0
  33. package/tests/import-fixer.test.ts +944 -0
  34. package/tests/import-verifier.test.ts +420 -0
  35. package/tests/knowledge-memory.test.ts +40 -0
  36. package/tests/self-evaluator.test.ts +97 -0
  37. package/.ai-spec-workspace.json +0 -17
  38. package/.ai-spec.json +0 -7
  39. package/cli/commands/model.ts +0 -152
  40. package/cli/commands/scan.ts +0 -99
  41. package/cli/commands/workspace.ts +0 -219
@@ -35,10 +35,33 @@ import { loadFrontendContext } from "../../core/frontend-context-loader";
35
35
  import { buildFrontendSpecPrompt } from "../../prompts/frontend-spec.prompt";
36
36
  import { AiSpecConfig, resolveApiKey } from "../utils";
37
37
  import { printBanner, MultiRepoResult } from "./helpers";
38
+ import {
39
+ verifyCrossStackContract,
40
+ printCrossStackReport,
41
+ } from "../../core/cross-stack-verifier";
42
+ import {
43
+ verifyImports,
44
+ printImportVerificationReport,
45
+ } from "../../core/import-verifier";
46
+ import { runImportFix, printFixReport } from "../../core/import-fixer";
47
+ import { generateRunId, RunLogger, setActiveLogger } from "../../core/run-logger";
38
48
  import * as fs from "fs-extra";
39
49
 
40
50
  // ─── Single-repo workspace pipeline ──────────────────────────────────────────
41
51
 
52
+ export interface WorkspaceRepoRunResult {
53
+ /** True only when spec_gen + codegen both succeeded and produced files. */
54
+ success: boolean;
55
+ /** Human-readable reason when success === false. */
56
+ failureReason?: string;
57
+ specFile: string | null;
58
+ dsl: SpecDSL | null;
59
+ /** Files written by codegen. Empty when codegen failed or wrote nothing. */
60
+ generatedFiles: string[];
61
+ /** Per-repo RunLogger ID — usable with `ai-spec logs <runId>` from the repo dir. */
62
+ runId: string;
63
+ }
64
+
42
65
  export async function runSingleRepoPipelineInWorkspace(opts: {
43
66
  idea: string;
44
67
  specProvider: AIProvider;
@@ -50,7 +73,10 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
50
73
  repoName: string;
51
74
  cliOpts: Record<string, unknown>;
52
75
  contractContextSection?: string;
53
- }): Promise<{ dsl: SpecDSL | null; specFile: string | null }> {
76
+ maxCodegenConcurrency?: number;
77
+ injectFixHistory?: boolean;
78
+ fixHistoryInjectMax?: number;
79
+ }): Promise<WorkspaceRepoRunResult> {
54
80
  const {
55
81
  idea,
56
82
  specProvider,
@@ -64,11 +90,22 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
64
90
  contractContextSection,
65
91
  } = opts;
66
92
 
93
+ // ── Per-repo RunLogger ─────────────────────────────────────────────────────
94
+ // Each repo in a workspace pipeline gets its own log dir under the repo root
95
+ // so users can run `ai-spec logs` from inside the repo to debug failures.
96
+ const runId = generateRunId();
97
+ const runLogger = new RunLogger(repoAbsPath, runId, {
98
+ provider: specProviderName,
99
+ model: specModelName,
100
+ });
101
+ setActiveLogger(runLogger);
102
+
67
103
  console.log(chalk.blue(`\n [${repoName}] Loading project context...`));
104
+ runLogger.stageStart("context_load");
68
105
  const loader = new ContextLoader(repoAbsPath);
69
106
  let context = await loader.loadProjectContext();
70
-
71
107
  const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
108
+ runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
72
109
 
73
110
  console.log(chalk.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
74
111
  console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
@@ -96,34 +133,55 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
96
133
  fullIdea = `${idea}\n\n${contractContextSection}`;
97
134
  }
98
135
 
136
+ // ── Spec Generation (CRITICAL: failure here aborts the repo) ───────────────
99
137
  console.log(chalk.blue(` [${repoName}] Generating spec...`));
138
+ runLogger.stageStart("spec_gen");
100
139
  let finalSpec: string;
101
140
  try {
102
141
  const result = await generateSpecWithTasks(specProvider, fullIdea, context);
103
142
  finalSpec = result.spec;
143
+ runLogger.stageEnd("spec_gen", { specLength: finalSpec.length });
104
144
  console.log(chalk.green(` Spec generated.`));
105
145
  } catch (err) {
106
- console.error(chalk.red(` Spec generation failed: ${(err as Error).message}`));
107
- return { dsl: null, specFile: null };
146
+ const msg = (err as Error).message;
147
+ runLogger.stageFail("spec_gen", msg);
148
+ runLogger.finish();
149
+ console.error(chalk.red(` ✘ Spec generation failed: ${msg}`));
150
+ return {
151
+ success: false,
152
+ failureReason: `spec_gen failed: ${msg}`,
153
+ specFile: null,
154
+ dsl: null,
155
+ generatedFiles: [],
156
+ runId,
157
+ };
108
158
  }
109
159
 
110
- // DSL Extraction
160
+ // ── DSL Extraction (non-fatal if missing) ──────────────────────────────────
111
161
  let extractedDsl: SpecDSL | null = null;
112
162
  if (!cliOpts.skipDsl) {
113
163
  console.log(chalk.blue(` [${repoName}] Extracting DSL...`));
164
+ runLogger.stageStart("dsl_extract");
114
165
  try {
115
166
  const dslExtractor = new DslExtractor(specProvider);
116
167
  const repoIsFrontend = isFrontendDeps(context.dependencies);
117
168
  extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
118
169
  if (extractedDsl) {
170
+ runLogger.stageEnd("dsl_extract", {
171
+ endpoints: extractedDsl.endpoints?.length ?? 0,
172
+ models: extractedDsl.models?.length ?? 0,
173
+ });
119
174
  console.log(chalk.green(` DSL extracted.`));
175
+ } else {
176
+ runLogger.stageEnd("dsl_extract", { skipped: true });
120
177
  }
121
178
  } catch (err) {
179
+ runLogger.stageFail("dsl_extract", (err as Error).message);
122
180
  console.log(chalk.yellow(` DSL extraction failed: ${(err as Error).message}`));
123
181
  }
124
182
  }
125
183
 
126
- // Git Worktree auto-skip for frontend repos
184
+ // ── Git Worktree (auto-skip for frontend repos) ────────────────────────────
127
185
  const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
128
186
  const skipWorktreeForRepo = cliOpts.worktree
129
187
  ? false
@@ -143,7 +201,7 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
143
201
  console.log(chalk.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
144
202
  }
145
203
 
146
- // Save Spec
204
+ // ── Save Spec ──────────────────────────────────────────────────────────────
147
205
  const specsDir = path.join(workingDir, "specs");
148
206
  await fs.ensureDir(specsDir);
149
207
  const featureSlug = slugify(idea);
@@ -158,55 +216,158 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
158
216
  console.log(chalk.green(` DSL saved: ${path.relative(repoAbsPath, savedDslFile)}`));
159
217
  }
160
218
 
161
- // Code Generation
219
+ // ── Code Generation (CRITICAL: failure or 0 files = repo failed) ───────────
162
220
  console.log(chalk.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
221
+ runLogger.stageStart("codegen", { mode: codegenMode });
222
+ let generatedFiles: string[] = [];
163
223
  try {
164
224
  const codegen = new CodeGenerator(codegenProvider, codegenMode);
165
- await codegen.generateCode(specFile, workingDir, context, {
225
+ generatedFiles = await codegen.generateCode(specFile, workingDir, context, {
166
226
  auto: true,
167
227
  dslFilePath: savedDslFile ?? undefined,
168
228
  repoType: detectedRepoType,
229
+ maxConcurrency: opts.maxCodegenConcurrency,
230
+ injectFixHistory: opts.injectFixHistory,
231
+ fixHistoryInjectMax: opts.fixHistoryInjectMax,
169
232
  });
170
- console.log(chalk.green(` Code generation complete.`));
233
+ runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
171
234
  } catch (err) {
172
- console.log(chalk.yellow(` Code generation failed: ${(err as Error).message}`));
235
+ const msg = (err as Error).message;
236
+ runLogger.stageFail("codegen", msg);
237
+ runLogger.finish();
238
+ console.error(chalk.red(` ✘ Code generation failed: ${msg}`));
239
+ return {
240
+ success: false,
241
+ failureReason: `codegen failed: ${msg}`,
242
+ specFile,
243
+ dsl: extractedDsl,
244
+ generatedFiles: [],
245
+ runId,
246
+ };
173
247
  }
174
248
 
175
- // Test Generation
249
+ // claude-code mode returns [] by design (the claude CLI writes files itself).
250
+ // Only treat empty as failure when we're in api mode where we expect a list.
251
+ if (generatedFiles.length === 0 && codegenMode === "api") {
252
+ const msg = "Code generation produced 0 files (likely planning step returned empty filePlan)";
253
+ runLogger.stageFail("codegen", msg);
254
+ runLogger.finish();
255
+ console.error(chalk.red(` ✘ ${msg}`));
256
+ return {
257
+ success: false,
258
+ failureReason: msg,
259
+ specFile,
260
+ dsl: extractedDsl,
261
+ generatedFiles: [],
262
+ runId,
263
+ };
264
+ }
265
+
266
+ console.log(chalk.green(` Code generation complete (${generatedFiles.length} file(s)).`));
267
+
268
+ // ── Import Verification + Auto-Fix ─────────────────────────────────────────
269
+ // Same two-stage repair flow as single-repo: deterministic DSL stubs + AI fallback.
270
+ if (generatedFiles.length > 0) {
271
+ runLogger.stageStart("import_verify");
272
+ try {
273
+ const absFiles = generatedFiles.map((f) =>
274
+ path.isAbsolute(f) ? f : path.join(workingDir, f)
275
+ );
276
+ const importReport = await verifyImports(absFiles, workingDir);
277
+ printImportVerificationReport(repoName, importReport);
278
+ runLogger.stageEnd("import_verify", {
279
+ totalImports: importReport.totalImports,
280
+ broken: importReport.brokenImports.length,
281
+ external: importReport.externalImports,
282
+ });
283
+
284
+ if (importReport.brokenImports.length > 0) {
285
+ runLogger.stageStart("import_fix");
286
+ try {
287
+ const fixReport = await runImportFix({
288
+ brokenImports: importReport.brokenImports,
289
+ dsl: extractedDsl,
290
+ repoRoot: workingDir,
291
+ generatedFilePaths: absFiles,
292
+ provider: codegenProvider,
293
+ runId,
294
+ recordHistory: true,
295
+ });
296
+ printFixReport(repoName, fixReport);
297
+ runLogger.stageEnd("import_fix", {
298
+ deterministic: fixReport.deterministicCount,
299
+ aiFixed: fixReport.aiFixedCount,
300
+ applied: fixReport.applied.length,
301
+ unresolved: fixReport.unresolvedCount,
302
+ });
303
+
304
+ if (fixReport.applied.length > 0) {
305
+ console.log(chalk.blue(`\n Re-running import verifier after fixes...`));
306
+ const reverifyReport = await verifyImports(absFiles, workingDir);
307
+ printImportVerificationReport(`${repoName} (after fix)`, reverifyReport);
308
+ }
309
+ } catch (err) {
310
+ runLogger.stageFail("import_fix", (err as Error).message);
311
+ console.log(chalk.yellow(` Import auto-fix failed: ${(err as Error).message}`));
312
+ }
313
+ }
314
+ } catch (err) {
315
+ runLogger.stageFail("import_verify", (err as Error).message);
316
+ console.log(chalk.yellow(` Import verification failed: ${(err as Error).message}`));
317
+ }
318
+ }
319
+
320
+ // ── Test Generation (non-fatal) ────────────────────────────────────────────
176
321
  if (!cliOpts.skipTests && extractedDsl) {
177
322
  console.log(chalk.blue(` [${repoName}] Generating test skeletons...`));
323
+ runLogger.stageStart("test_gen");
178
324
  try {
179
325
  const testGen = new TestGenerator(codegenProvider);
180
326
  const testFiles = await testGen.generate(extractedDsl, workingDir);
327
+ runLogger.stageEnd("test_gen", { testFiles: testFiles.length });
181
328
  console.log(chalk.green(` ${testFiles.length} test file(s) generated.`));
182
329
  } catch (err) {
330
+ runLogger.stageFail("test_gen", (err as Error).message);
183
331
  console.log(chalk.yellow(` Test generation failed: ${(err as Error).message}`));
184
332
  }
185
333
  }
186
334
 
187
- // Error Feedback
335
+ // ── Error Feedback (non-fatal) ─────────────────────────────────────────────
188
336
  if (!cliOpts.skipErrorFeedback) {
337
+ runLogger.stageStart("error_feedback");
189
338
  try {
190
339
  await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
340
+ runLogger.stageEnd("error_feedback");
191
341
  } catch (err) {
342
+ runLogger.stageFail("error_feedback", (err as Error).message);
192
343
  console.log(chalk.yellow(` Error feedback failed: ${(err as Error).message}`));
193
344
  }
194
345
  }
195
346
 
196
- // Code Review
347
+ // ── Code Review (non-fatal) ────────────────────────────────────────────────
197
348
  if (!cliOpts.skipReview) {
198
349
  console.log(chalk.blue(` [${repoName}] Running code review...`));
350
+ runLogger.stageStart("review");
199
351
  try {
200
352
  const reviewer = new CodeReviewer(specProvider, workingDir);
201
353
  const reviewResult = await reviewer.reviewCode(finalSpec);
202
354
  await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
355
+ runLogger.stageEnd("review");
203
356
  console.log(chalk.green(` Code review complete.`));
204
357
  } catch (err) {
358
+ runLogger.stageFail("review", (err as Error).message);
205
359
  console.log(chalk.yellow(` Code review failed: ${(err as Error).message}`));
206
360
  }
207
361
  }
208
362
 
209
- return { dsl: extractedDsl, specFile };
363
+ runLogger.finish();
364
+ return {
365
+ success: true,
366
+ specFile,
367
+ dsl: extractedDsl,
368
+ generatedFiles,
369
+ runId,
370
+ };
210
371
  }
211
372
 
212
373
  // ─── Multi-repo pipeline ────────────────────────────────────────────────────
@@ -227,7 +388,7 @@ export async function runMultiRepoPipeline(
227
388
  const specApiKey = await resolveApiKey(specProviderName, opts.key as string | undefined);
228
389
  const specProvider = createProvider(specProviderName, specApiKey, specModelName);
229
390
 
230
- const codegenMode: CodeGenMode = ((opts.codegen as string) as CodeGenMode) || config.codegen || "claude-code";
391
+ const codegenMode: CodeGenMode = ((opts.codegen as string) as CodeGenMode) || config.codegen || "api";
231
392
  const codegenProviderName = (opts.codegenProvider as string) || config.codegenProvider || specProviderName;
232
393
  const codegenModelName = (opts.codegenModel as string) || config.codegenModel || DEFAULT_MODELS[codegenProviderName];
233
394
  const codegenApiKey =
@@ -400,7 +561,7 @@ export async function runMultiRepoPipeline(
400
561
  }
401
562
 
402
563
  try {
403
- const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
564
+ const repoResult = await runSingleRepoPipelineInWorkspace({
404
565
  idea: specIdea,
405
566
  specProvider,
406
567
  specProviderName,
@@ -411,30 +572,143 @@ export async function runMultiRepoPipeline(
411
572
  repoName: repoReq.repoName,
412
573
  cliOpts: opts,
413
574
  contractContextSection,
575
+ maxCodegenConcurrency: config.maxCodegenConcurrency,
576
+ injectFixHistory: config.injectFixHistory,
577
+ fixHistoryInjectMax: config.fixHistoryInjectMax,
414
578
  });
415
579
 
416
- if (repoReq.isContractProvider && dsl) {
417
- contractDsls.set(repoReq.repoName, dsl);
580
+ if (repoResult.success && repoReq.isContractProvider && repoResult.dsl) {
581
+ contractDsls.set(repoReq.repoName, repoResult.dsl);
418
582
  console.log(chalk.green(` Contract stored for downstream repos.`));
419
583
  }
420
584
 
421
- results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
422
- console.log(chalk.green(` ✔ ${repoReq.repoName} complete`));
585
+ if (repoResult.success) {
586
+ results.push({
587
+ repoName: repoReq.repoName,
588
+ status: "success",
589
+ specFile: repoResult.specFile,
590
+ dsl: repoResult.dsl,
591
+ repoAbsPath,
592
+ role: repoReq.role,
593
+ generatedFiles: repoResult.generatedFiles,
594
+ runId: repoResult.runId,
595
+ });
596
+ console.log(chalk.green(` ✔ ${repoReq.repoName} complete (${repoResult.generatedFiles.length} files, runId: ${repoResult.runId})`));
597
+ } else {
598
+ results.push({
599
+ repoName: repoReq.repoName,
600
+ status: "failed",
601
+ specFile: repoResult.specFile,
602
+ dsl: repoResult.dsl,
603
+ repoAbsPath,
604
+ role: repoReq.role,
605
+ generatedFiles: [],
606
+ failureReason: repoResult.failureReason,
607
+ runId: repoResult.runId,
608
+ });
609
+ console.error(chalk.red(` ✘ ${repoReq.repoName} failed: ${repoResult.failureReason ?? "unknown error"}`));
610
+ console.error(chalk.gray(` debug: cd ${repoAbsPath} && ai-spec logs ${repoResult.runId}`));
611
+ }
423
612
  } catch (err) {
424
- console.error(chalk.red(` ✘ ${repoReq.repoName} failed: ${(err as Error).message}`));
425
- results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
613
+ // Unexpected exception (not a stage-level failure caught inside the workspace pipeline).
614
+ console.error(chalk.red(` ✘ ${repoReq.repoName} failed unexpectedly: ${(err as Error).message}`));
615
+ results.push({
616
+ repoName: repoReq.repoName,
617
+ status: "failed",
618
+ specFile: null,
619
+ dsl: null,
620
+ repoAbsPath,
621
+ role: repoReq.role,
622
+ generatedFiles: [],
623
+ failureReason: `unexpected exception: ${(err as Error).message}`,
624
+ });
625
+ }
626
+ }
627
+
628
+ // ── Step W5: Cross-stack contract verification ────────────────────────────
629
+ // Verify only repos that actually produced files. Skipping repos that failed
630
+ // earlier prevents the verifier from scanning pre-existing code and producing
631
+ // misleading "phantom endpoints" reports.
632
+ const backendWithDsl = results.find(
633
+ (r) => r.role === "backend" && r.status === "success" && r.dsl && r.generatedFiles.length > 0
634
+ );
635
+ const frontendCandidates = results.filter(
636
+ (r) => r.role === "frontend" || r.role === "mobile"
637
+ );
638
+
639
+ if (backendWithDsl && backendWithDsl.dsl && frontendCandidates.length > 0) {
640
+ console.log(chalk.blue("\n[W5] Cross-stack contract verification..."));
641
+ for (const fe of frontendCandidates) {
642
+ if (fe.status !== "success") {
643
+ console.log(chalk.gray(
644
+ ` ⊘ Skipped ${fe.repoName}: repo failed earlier (${fe.failureReason ?? "unknown"})`
645
+ ));
646
+ continue;
647
+ }
648
+ if (fe.generatedFiles.length === 0) {
649
+ console.log(chalk.gray(
650
+ ` ⊘ Skipped ${fe.repoName}: codegen produced 0 files — nothing to verify`
651
+ ));
652
+ continue;
653
+ }
654
+ try {
655
+ // Scope verification to files generated in THIS run. Without this,
656
+ // the verifier scans the entire frontend repo and reports historical
657
+ // API calls (unrelated to the current feature) as "phantom endpoints",
658
+ // producing misleading output in mature codebases.
659
+ const report = await verifyCrossStackContract(
660
+ backendWithDsl.dsl,
661
+ fe.repoAbsPath,
662
+ { scopedFiles: fe.generatedFiles }
663
+ );
664
+ printCrossStackReport(fe.repoName, report);
665
+ if (report.hasViolations) {
666
+ console.log(
667
+ chalk.yellow(
668
+ ` ⚠ [W5] ${fe.repoName} has cross-stack violations` +
669
+ ` (${report.phantom.length} phantom, ${report.methodMismatch.length} method mismatch).` +
670
+ ` Review the report above and fix generated frontend code.`
671
+ )
672
+ );
673
+ }
674
+ } catch (err) {
675
+ console.log(chalk.yellow(` ⚠ Verification failed for ${fe.repoName}: ${(err as Error).message}`));
676
+ }
426
677
  }
678
+ } else if (frontendCandidates.length > 0 && !backendWithDsl) {
679
+ console.log(chalk.gray(
680
+ "\n[W5] Cross-stack verification skipped: no backend repo produced a usable DSL in this run."
681
+ ));
427
682
  }
428
683
 
429
684
  // ── Done ──────────────────────────────────────────────────────────────────
430
- console.log(chalk.bold.green("\n✔ Multi-repo pipeline complete!"));
685
+ const successCount = results.filter((r) => r.status === "success").length;
686
+ const failedCount = results.filter((r) => r.status === "failed").length;
687
+ const overallOk = failedCount === 0;
688
+
689
+ if (overallOk) {
690
+ console.log(chalk.bold.green("\n✔ Multi-repo pipeline complete!"));
691
+ } else {
692
+ console.log(chalk.bold.yellow(`\n⚠ Multi-repo pipeline finished with ${failedCount} failure(s).`));
693
+ }
431
694
  console.log(chalk.gray(` Workspace: ${workspace.name}`));
432
695
  console.log(chalk.gray(` Requirement: ${idea}`));
696
+ console.log(chalk.gray(` Result: ${successCount} success / ${failedCount} failed / ${results.length} total`));
433
697
  console.log();
434
698
  for (const r of results) {
435
699
  const icon = r.status === "success" ? chalk.green("✔") : r.status === "failed" ? chalk.red("✘") : chalk.gray("−");
436
- const specInfo = r.specFile ? chalk.gray(` → ${r.specFile}`) : "";
437
- console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
700
+ if (r.status === "success") {
701
+ const fileInfo = chalk.gray(` (${r.generatedFiles.length} files)`);
702
+ const specInfo = r.specFile ? chalk.gray(` → ${r.specFile}`) : "";
703
+ console.log(` ${icon} ${r.repoName}${fileInfo}${specInfo}`);
704
+ } else if (r.status === "failed") {
705
+ console.log(` ${icon} ${chalk.red(r.repoName)} — ${chalk.red(r.failureReason ?? "unknown reason")}`);
706
+ if (r.runId) {
707
+ console.log(chalk.gray(` debug: cd ${r.repoAbsPath} && ai-spec logs ${r.runId}`));
708
+ }
709
+ } else {
710
+ console.log(` ${icon} ${r.repoName} (${r.status})`);
711
+ }
438
712
  }
439
713
 
440
714
  return results;
@@ -52,6 +52,12 @@ import {
52
52
  } from "../../core/vcr";
53
53
  import { printBanner } from "./helpers";
54
54
  import { startSpinner, startStage } from "../../core/cli-ui";
55
+ import { exportOpenApi } from "../../core/openapi-exporter";
56
+ import { saveTypescriptTypes } from "../../core/types-generator";
57
+ import { input } from "@inquirer/prompts";
58
+ import { appendDirectLesson } from "../../core/knowledge-memory";
59
+ import { verifyImports, printImportVerificationReport } from "../../core/import-verifier";
60
+ import { runImportFix, printFixReport } from "../../core/import-fixer";
55
61
 
56
62
  // ─── Pipeline Options ────────────────────────────────────────────────────────
57
63
 
@@ -78,6 +84,8 @@ export interface SingleRepoPipelineOpts {
78
84
  worktree?: boolean;
79
85
  vcrRecord?: boolean;
80
86
  vcrReplay?: string;
87
+ openapi?: boolean;
88
+ types?: boolean;
81
89
  }
82
90
 
83
91
  // ─── Single-repo pipeline ────────────────────────────────────────────────────
@@ -96,7 +104,7 @@ export async function runSingleRepoPipeline(
96
104
 
97
105
  // ── Resolve codegen ─────────────────────────────────────────────────────
98
106
  const codegenMode: CodeGenMode =
99
- (opts.codegen as CodeGenMode) || config.codegen || "claude-code";
107
+ (opts.codegen as CodeGenMode) || config.codegen || "api";
100
108
  const codegenProviderName =
101
109
  opts.codegenProvider || config.codegenProvider || specProviderName;
102
110
  const codegenModelName =
@@ -155,7 +163,7 @@ export async function runSingleRepoPipeline(
155
163
  runLogger.stageStart("context_load");
156
164
  const loader = new ContextLoader(currentDir);
157
165
  const context = await loader.loadProjectContext();
158
- const { type: detectedRepoType } = await detectRepoType(currentDir);
166
+ const { type: detectedRepoType, role: detectedRepoRole } = await detectRepoType(currentDir);
159
167
  runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
160
168
  console.log(chalk.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
161
169
  console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
@@ -498,6 +506,24 @@ export async function runSingleRepoPipeline(
498
506
  const dslExtractor = new DslExtractor(specProvider);
499
507
  savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
500
508
  console.log(chalk.green(` ✔ DSL saved : ${savedDslFile}`));
509
+
510
+ // ── Auto-generate OpenAPI / TypeScript types if requested ──────────────
511
+ if (opts.openapi) {
512
+ try {
513
+ const openapiPath = await exportOpenApi(extractedDsl, currentDir, { format: "yaml" });
514
+ console.log(chalk.green(` ✔ OpenAPI : ${path.relative(currentDir, openapiPath)}`));
515
+ } catch (err) {
516
+ console.log(chalk.yellow(` ⚠ OpenAPI export failed: ${(err as Error).message}`));
517
+ }
518
+ }
519
+ if (opts.types) {
520
+ try {
521
+ const typesPath = await saveTypescriptTypes(extractedDsl, currentDir, {});
522
+ console.log(chalk.green(` ✔ TS Types : ${path.relative(currentDir, typesPath)}`));
523
+ } catch (err) {
524
+ console.log(chalk.yellow(` ⚠ Types generation failed: ${(err as Error).message}`));
525
+ }
526
+ }
501
527
  }
502
528
 
503
529
  if (!opts.skipTasks) {
@@ -554,9 +580,70 @@ export async function runSingleRepoPipeline(
554
580
  resume: opts.resume,
555
581
  dslFilePath: savedDslFile ?? undefined,
556
582
  repoType: detectedRepoType,
583
+ maxConcurrency: config.maxCodegenConcurrency,
584
+ injectFixHistory: config.injectFixHistory,
585
+ fixHistoryInjectMax: config.fixHistoryInjectMax,
557
586
  });
558
587
  runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
559
588
 
589
+ // ── Step 6.5: Import Verification + Auto-Fix ───────────────────────────
590
+ // Static check that every import in the generated files actually resolves.
591
+ // If broken imports are found, run a two-stage fix:
592
+ // Stage A: deterministic DSL-driven stub generation
593
+ // Stage B: AI-driven repair (only for what Stage A could not handle)
594
+ // After fixes, re-verify to confirm.
595
+ if (generatedFiles.length > 0) {
596
+ runLogger.stageStart("import_verify");
597
+ try {
598
+ const absFiles = generatedFiles.map((f) =>
599
+ path.isAbsolute(f) ? f : path.join(workingDir, f)
600
+ );
601
+ const importReport = await verifyImports(absFiles, workingDir);
602
+ printImportVerificationReport(path.basename(workingDir), importReport);
603
+ runLogger.stageEnd("import_verify", {
604
+ totalImports: importReport.totalImports,
605
+ broken: importReport.brokenImports.length,
606
+ external: importReport.externalImports,
607
+ });
608
+
609
+ // ── Auto-fix loop ────────────────────────────────────────────────────
610
+ if (importReport.brokenImports.length > 0) {
611
+ runLogger.stageStart("import_fix");
612
+ try {
613
+ const fixReport = await runImportFix({
614
+ brokenImports: importReport.brokenImports,
615
+ dsl: extractedDsl,
616
+ repoRoot: workingDir,
617
+ generatedFilePaths: absFiles,
618
+ provider: codegenProvider,
619
+ runId,
620
+ recordHistory: true,
621
+ });
622
+ printFixReport(path.basename(workingDir), fixReport);
623
+ runLogger.stageEnd("import_fix", {
624
+ deterministic: fixReport.deterministicCount,
625
+ aiFixed: fixReport.aiFixedCount,
626
+ applied: fixReport.applied.length,
627
+ unresolved: fixReport.unresolvedCount,
628
+ });
629
+
630
+ // Re-verify after fixes
631
+ if (fixReport.applied.length > 0) {
632
+ console.log(chalk.blue("\n Re-running import verifier after fixes..."));
633
+ const reverifyReport = await verifyImports(absFiles, workingDir);
634
+ printImportVerificationReport(`${path.basename(workingDir)} (after fix)`, reverifyReport);
635
+ }
636
+ } catch (err) {
637
+ runLogger.stageFail("import_fix", (err as Error).message);
638
+ console.log(chalk.yellow(` ⚠ Import auto-fix failed: ${(err as Error).message}`));
639
+ }
640
+ }
641
+ } catch (err) {
642
+ runLogger.stageFail("import_verify", (err as Error).message);
643
+ console.log(chalk.yellow(` ⚠ Import verification failed: ${(err as Error).message}`));
644
+ }
645
+ }
646
+
560
647
  // ── Step 7: Test Skeleton Generation ───────────────────────────────────
561
648
  if (opts.tdd) {
562
649
  console.log(chalk.gray("\n[7/9] TDD mode — test files already written pre-implementation."));
@@ -710,6 +797,7 @@ export async function runSingleRepoPipeline(
710
797
  reviewText: reviewResult,
711
798
  promptHash,
712
799
  logger: runLogger,
800
+ repoType: detectedRepoRole,
713
801
  });
714
802
  printSelfEval(selfEvalResult);
715
803
 
@@ -745,6 +833,19 @@ export async function runSingleRepoPipeline(
745
833
  console.log(chalk.yellow(" The pipeline structure may have changed since the recording was made."));
746
834
  }
747
835
 
836
+ // ── Quick lesson capture (skip in auto/fast mode) ───────────────────────
837
+ if (!opts.auto && !opts.fast) {
838
+ try {
839
+ const lesson = await input({ message: "Any lessons to note? (Enter to skip):" });
840
+ if (lesson.trim()) {
841
+ await appendDirectLesson(currentDir, lesson.trim());
842
+ console.log(chalk.green(" ✔ Lesson saved to constitution §9."));
843
+ }
844
+ } catch {
845
+ // non-blocking if prompt is interrupted
846
+ }
847
+ }
848
+
748
849
  // ── Done ────────────────────────────────────────────────────────────────
749
850
  runLogger.finish();
750
851
  console.log(chalk.bold.green("\n✔ All done!"));
package/cli/utils.ts CHANGED
@@ -26,6 +26,29 @@ export interface AiSpecConfig extends AiSpecGlobalConfig {
26
26
  minHarnessScore?: number;
27
27
  /** Maximum error-feedback cycles before giving up (default: 2, TDD default: 3). */
28
28
  maxErrorCycles?: number;
29
+ /**
30
+ * Maximum number of tasks that can run concurrently within a codegen batch
31
+ * (api mode only). Prevents rate-limit errors when a layer has many independent
32
+ * tasks. Default: 3.
33
+ */
34
+ maxCodegenConcurrency?: number;
35
+ /**
36
+ * When true (default), past hallucination patterns from
37
+ * `.ai-spec-fix-history.json` are injected into codegen prompts.
38
+ * Set to false to disable automatic learning from fix history.
39
+ */
40
+ injectFixHistory?: boolean;
41
+ /**
42
+ * Number of times a hallucination pattern must repeat in fix-history before
43
+ * `ai-spec fix-history --promote` offers it as a constitution §9 lesson.
44
+ * Default: 5.
45
+ */
46
+ fixHistoryPromotionThreshold?: number;
47
+ /**
48
+ * Maximum number of past hallucination patterns injected into a single
49
+ * codegen prompt. Prevents prompt bloat. Default: 10.
50
+ */
51
+ fixHistoryInjectMax?: number;
29
52
  /** §9 lesson count threshold for auto-consolidation (default: 12). */
30
53
  autoConsolidateThreshold?: number;
31
54