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
package/README.md CHANGED
@@ -12,8 +12,8 @@
12
12
  <p align="center">
13
13
  <a href="https://github.com/hzhongzhong/ai-spec"><img src="https://img.shields.io/badge/GitHub-ai--spec-181717?logo=github" alt="GitHub" /></a>
14
14
  <a href="https://www.npmjs.com/package/ai-spec-dev"><img src="https://img.shields.io/npm/v/ai-spec-dev?color=cb3837&logo=npm" alt="npm" /></a>
15
- <img src="https://img.shields.io/badge/version-0.45.0-blue" alt="version" />
16
- <img src="https://img.shields.io/badge/tests-409%20passed-brightgreen" alt="tests" />
15
+ <img src="https://img.shields.io/badge/version-0.55.0-blue" alt="version" />
16
+ <img src="https://img.shields.io/badge/tests-913%20passed-brightgreen" alt="tests" />
17
17
  <img src="https://img.shields.io/badge/providers-9-orange" alt="providers" />
18
18
  <img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
19
19
  </p>
@@ -122,20 +122,35 @@ ai-spec create "Add login functionality to user module"
122
122
  ### Commands
123
123
 
124
124
  ```
125
- ai-spec init Register repos, generate project + global constitutions
126
- ai-spec init --add-repo Quick-add a single repo
127
- ai-spec create [idea] Select repo(s) full pipeline: spec → DSL → codegen → review
128
- ai-spec update [change] Incremental update: modify spec re-extract DSL → regenerate affected files
129
- ai-spec learn [lesson] Inject knowledge directly into constitution §9
130
- ai-spec review [file] 3-pass AI code review (architecture + implementation + impact)
131
- ai-spec export DSL OpenAPI 3.1.0 YAML/JSON
132
- ai-spec types DSL TypeScript types (models + endpoint types + API_ENDPOINTS)
133
- ai-spec mock DSL Mock server + MSW handlers + proxy config
134
- ai-spec dashboard Generate static HTML harness dashboard
135
- ai-spec restore <runId> Rollback all files modified by a specific run
136
- ai-spec model Interactive provider/model switcher
137
- ai-spec config View/modify project configuration
138
- ai-spec workspace init Initialize multi-repo workspace
125
+ # Core workflow
126
+ ai-spec init Register repos, generate constitutions (auto-scans projects)
127
+ ai-spec init --add-repo Quick-add a single repo
128
+ ai-spec init --status Show registered repos and constitution health
129
+ ai-spec create [idea] Full pipeline: spec DSL → codegen → review
130
+ ai-spec create [idea] --openapi Also auto-generate OpenAPI 3.1.0 YAML after DSL
131
+ ai-spec create [idea] --types Also auto-generate TypeScript types after DSL
132
+ ai-spec update [change] Incremental: modify spec re-extract DSL regen affected files
133
+ ai-spec review [file] 3-pass AI code review (architecture + implementation + impact)
134
+
135
+ # Knowledge & constitution
136
+ ai-spec learn [lesson] Inject a lesson directly into constitution §9
137
+
138
+ # DSL-derived artifacts
139
+ ai-spec export DSL → OpenAPI 3.1.0 YAML/JSON
140
+ ai-spec types DSL → TypeScript types (models + endpoint types + API_ENDPOINTS)
141
+ ai-spec mock DSL → Mock server + MSW handlers + proxy config
142
+
143
+ # Configuration (run without flags for interactive model/provider picker)
144
+ ai-spec config Interactive provider/model setup (merged from `model`)
145
+ ai-spec config --show Print current configuration
146
+ ai-spec config --list List all available providers and models
147
+
148
+ # Observability
149
+ ai-spec logs [runId] View run history or stage breakdown
150
+ ai-spec trend Harness score trend across runs
151
+ ai-spec dashboard Generate static HTML harness dashboard
152
+ ai-spec restore <runId> Rollback all files modified by a specific run
153
+ ai-spec vcr list List VCR recordings
139
154
  ```
140
155
 
141
156
  ### Architecture
@@ -318,20 +333,35 @@ ai-spec create "给用户模块增加登录功能"
318
333
  ### 命令总览
319
334
 
320
335
  ```
321
- ai-spec init 注册仓库,生成项目宪法 + 全局宪法
322
- ai-spec init --add-repo 快速添加单个仓库
323
- ai-spec create [idea] 选择仓库 → 完整流水线:spec → DSL → 代码生成 → 审查
324
- ai-spec update [change] 增量更新:修改 Spec → 重提取 DSL → 重新生成受影响文件
325
- ai-spec learn [lesson] 零摩擦知识注入,直接写入宪法 §9
326
- ai-spec review [file] 3-pass AI 代码审查(架构 + 实现 + 影响面)
327
- ai-spec export DSL OpenAPI 3.1.0 YAML/JSON
328
- ai-spec types DSL → TypeScript 类型文件
329
- ai-spec mock DSL Mock 服务器 + MSW Handlers + 代理配置
330
- ai-spec dashboard 生成静态 HTML Harness Dashboard
331
- ai-spec restore <runId> 回滚指定 run 修改的所有文件
332
- ai-spec model 交互式切换 AI provider/model
333
- ai-spec config 查看/修改项目配置
334
- ai-spec workspace init 初始化多 Repo 工作区
336
+ # 核心开发流程
337
+ ai-spec init 注册仓库,生成项目宪法 + 全局宪法(自动扫描项目)
338
+ ai-spec init --add-repo 快速添加单个仓库
339
+ ai-spec init --status 查看已注册仓库和宪法状态
340
+ ai-spec create [idea] 完整流水线:spec → DSL → 代码生成 → 审查
341
+ ai-spec create [idea] --openapi DSL 提取后自动生成 OpenAPI 3.1.0 YAML
342
+ ai-spec create [idea] --types DSL 提取后自动生成 TypeScript 类型文件
343
+ ai-spec update [change] 增量更新:修改 Spec → 重提取 DSL → 重新生成受影响文件
344
+ ai-spec review [file] 3-pass AI 代码审查(架构 + 实现 + 影响面)
345
+
346
+ # 知识 & 宪法
347
+ ai-spec learn [lesson] 零摩擦知识注入,直接写入宪法 §9
348
+
349
+ # DSL 衍生产物
350
+ ai-spec export DSL → OpenAPI 3.1.0 YAML/JSON
351
+ ai-spec types DSL → TypeScript 类型文件
352
+ ai-spec mock DSL → Mock 服务器 + MSW Handlers + 代理配置
353
+
354
+ # 配置(无参数运行进入交互式 provider/model 选择)
355
+ ai-spec config 交互式 provider/model 配置(已合并原 model 命令)
356
+ ai-spec config --show 查看当前配置
357
+ ai-spec config --list 列出所有可用 provider 和模型
358
+
359
+ # 可观测性
360
+ ai-spec logs [runId] 查看 run 历史 / 指定 run 的 stage 详情
361
+ ai-spec trend Harness score 趋势分析
362
+ ai-spec dashboard 生成静态 HTML Harness Dashboard
363
+ ai-spec restore <runId> 回滚指定 run 修改的所有文件
364
+ ai-spec vcr list 列出 VCR 录制
335
365
  ```
336
366
 
337
367
  <details>
@@ -2,14 +2,21 @@ import { Command } from "commander";
2
2
  import * as path from "path";
3
3
  import * as fs from "fs-extra";
4
4
  import chalk from "chalk";
5
+ import { input, select, confirm } from "@inquirer/prompts";
5
6
  import { CodeGenMode } from "../../core/code-generator";
6
7
  import { clearAllKeys, clearKey, getSavedKey, KEY_STORE_FILE } from "../../core/key-store";
7
8
  import { AiSpecConfig, CONFIG_FILE, loadConfig } from "../utils";
9
+ import {
10
+ DEFAULT_MODELS,
11
+ ENV_KEY_MAP,
12
+ PROVIDER_CATALOG,
13
+ } from "../../core/spec-generator";
14
+ import { AiSpecGlobalConfig, GLOBAL_CONFIG_FILE, loadGlobalConfig, saveGlobalConfig } from "../utils";
8
15
 
9
16
  export function registerConfig(program: Command): void {
10
17
  program
11
18
  .command("config")
12
- .description(`Set default configuration for this project (saved to ${CONFIG_FILE})`)
19
+ .description(`Configure ai-spec: run without flags for interactive model/provider setup`)
13
20
  .option("--provider <name>", "Default AI provider for spec generation")
14
21
  .option("--model <name>", "Default model for spec generation")
15
22
  .option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)")
@@ -18,7 +25,9 @@ export function registerConfig(program: Command): void {
18
25
  .option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)")
19
26
  .option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)")
20
27
  .option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)")
28
+ .option("--max-codegen-concurrency <n>", "Max concurrent tasks per batch in api codegen mode (1-10, default: 3)")
21
29
  .option("--show", "Print current configuration")
30
+ .option("--list", "List all available providers and models")
22
31
  .option("--reset", "Reset configuration to empty")
23
32
  .option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json")
24
33
  .option("--clear-key <provider>", "Delete saved API key for a specific provider")
@@ -27,6 +36,117 @@ export function registerConfig(program: Command): void {
27
36
  const currentDir = process.cwd();
28
37
  const configPath = path.join(currentDir, CONFIG_FILE);
29
38
 
39
+ // ── --list: show all providers ──────────────────────────────────────────
40
+ if (opts.list) {
41
+ console.log(chalk.bold("\nAvailable providers & models:\n"));
42
+ for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
43
+ console.log(` ${chalk.bold.cyan(key.padEnd(10))} ${chalk.white(meta.displayName)}`);
44
+ console.log(chalk.gray(` ${meta.description}`));
45
+ console.log(chalk.gray(` env: ${meta.envKey} | models: ${meta.models.join(", ")}`));
46
+ console.log();
47
+ }
48
+ return;
49
+ }
50
+
51
+ // ── No flags → interactive model/provider picker ────────────────────────
52
+ const anyFlagSet = opts.provider || opts.model || opts.codegen || opts.codegenProvider ||
53
+ opts.codegenModel || opts.minSpecScore !== undefined || opts.minHarnessScore !== undefined ||
54
+ opts.maxErrorCycles !== undefined || opts.maxCodegenConcurrency !== undefined ||
55
+ opts.show || opts.reset || opts.clearKeys || opts.clearKey || opts.listKeys;
56
+
57
+ if (!anyFlagSet) {
58
+ const existing: AiSpecGlobalConfig = await loadGlobalConfig();
59
+
60
+ console.log(chalk.blue("\n─── ai-spec config ─────────────────────────────"));
61
+ console.log(chalk.gray(` Global config: ${GLOBAL_CONFIG_FILE}`));
62
+ if (Object.keys(existing).length > 0) {
63
+ console.log(chalk.gray(
64
+ ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` +
65
+ (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
66
+ ));
67
+ }
68
+ console.log();
69
+
70
+ const target = await select({
71
+ message: "Configure model for:",
72
+ choices: [
73
+ { name: "Spec generation (spec writing & refinement)", value: "spec" },
74
+ { name: "Code generation (used when --codegen api is active)", value: "codegen" },
75
+ { name: "Both (same provider/model for all tasks)", value: "both" },
76
+ ],
77
+ });
78
+
79
+ async function pickProviderAndModel(label: string): Promise<{ provider: string; model: string }> {
80
+ const providerKey = await select({
81
+ message: `${label} — select provider:`,
82
+ choices: Object.entries(PROVIDER_CATALOG).map(([key, meta]) => ({
83
+ name: `${meta.displayName.padEnd(22)} ${chalk.gray(meta.description)}`,
84
+ value: key,
85
+ short: meta.displayName,
86
+ })),
87
+ });
88
+ const meta = PROVIDER_CATALOG[providerKey];
89
+ const modelChoices = [
90
+ ...meta.models.map((m) => ({ name: m, value: m })),
91
+ { name: chalk.italic("✎ Enter custom model name..."), value: "__custom__" },
92
+ ];
93
+ let chosenModel = await select({
94
+ message: `${label} — select model (${meta.displayName}):`,
95
+ choices: modelChoices,
96
+ });
97
+ if (chosenModel === "__custom__") {
98
+ chosenModel = await input({
99
+ message: "Enter model name:",
100
+ validate: (v) => v.trim().length > 0 || "Model name cannot be empty",
101
+ });
102
+ }
103
+ return { provider: providerKey, model: chosenModel };
104
+ }
105
+
106
+ const updated: AiSpecGlobalConfig = { ...existing };
107
+
108
+ if (target === "spec" || target === "both") {
109
+ const { provider, model } = await pickProviderAndModel("Spec");
110
+ updated.provider = provider;
111
+ updated.model = model;
112
+ }
113
+ if (target === "codegen" || target === "both") {
114
+ if (target === "both") {
115
+ updated.codegenProvider = updated.provider;
116
+ updated.codegenModel = updated.model;
117
+ } else {
118
+ const { provider, model } = await pickProviderAndModel("Codegen");
119
+ updated.codegenProvider = provider;
120
+ updated.codegenModel = model;
121
+ }
122
+ const effectiveCodegenProvider = updated.codegenProvider ?? updated.provider ?? "gemini";
123
+ if (effectiveCodegenProvider !== "claude" && updated.codegen === "claude-code") {
124
+ updated.codegen = "api";
125
+ console.log(chalk.yellow(`\n ⚠ provider "${effectiveCodegenProvider}" does not support "claude-code" mode.`));
126
+ console.log(chalk.gray(` codegen mode auto-set to "api".`));
127
+ }
128
+ }
129
+
130
+ console.log(chalk.blue("\n Preview:"));
131
+ console.log(chalk.gray(` spec → ${updated.provider}/${updated.model}`));
132
+ if (updated.codegenProvider) {
133
+ console.log(chalk.gray(` codegen → ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "api"})`));
134
+ }
135
+
136
+ const ok = await confirm({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
137
+ if (!ok) { console.log(chalk.gray(" Cancelled.")); return; }
138
+
139
+ await saveGlobalConfig(updated);
140
+ console.log(chalk.green(`\n ✔ Saved to ${GLOBAL_CONFIG_FILE}`));
141
+
142
+ const providerToCheck = updated.provider ?? "gemini";
143
+ const envKey = ENV_KEY_MAP[providerToCheck];
144
+ if (envKey && !process.env[envKey]) {
145
+ console.log(chalk.yellow(` ⚠ Remember to set ${envKey} in your environment or .env file.`));
146
+ }
147
+ return;
148
+ }
149
+
30
150
  if (opts.clearKeys) {
31
151
  await clearAllKeys();
32
152
  console.log(chalk.green(`✔ All saved API keys cleared.`));
@@ -95,6 +215,14 @@ export function registerConfig(program: Command): void {
95
215
  }
96
216
  updated.minHarnessScore = score;
97
217
  }
218
+ if (opts.maxCodegenConcurrency !== undefined) {
219
+ const n = parseInt(opts.maxCodegenConcurrency, 10);
220
+ if (isNaN(n) || n < 1 || n > 10) {
221
+ console.error(chalk.red(" --max-codegen-concurrency must be a number between 1 and 10"));
222
+ process.exit(1);
223
+ }
224
+ updated.maxCodegenConcurrency = n;
225
+ }
98
226
  if (opts.maxErrorCycles !== undefined) {
99
227
  const cycles = parseInt(opts.maxErrorCycles, 10);
100
228
  if (isNaN(cycles) || cycles < 1 || cycles > 10) {
@@ -205,6 +205,8 @@ export function registerCreate(program: Command): void {
205
205
  .option("--serve", "After workspace pipeline completes, auto-start mock server + patch frontend proxy")
206
206
  .option("--vcr-record", "Record all AI responses to .ai-spec-vcr/ for offline replay")
207
207
  .option("--vcr-replay <runId>", "Replay AI responses from a previous recording (zero API calls)")
208
+ .option("--openapi", "Auto-generate OpenAPI 3.1.0 YAML after DSL extraction")
209
+ .option("--types", "Auto-generate TypeScript types after DSL extraction")
208
210
  .action(async (idea: string | undefined, opts) => {
209
211
  const currentDir = process.cwd();
210
212
  const config = await loadConfig(currentDir);
@@ -237,6 +239,18 @@ export function registerCreate(program: Command): void {
237
239
  const apiKey = await resolveApiKey(providerName, opts.key as string | undefined);
238
240
  const specProvider = createProvider(providerName, apiKey, modelName);
239
241
 
242
+ // Persist the resolved key on opts so downstream pipelines (single/multi-repo)
243
+ // short-circuit `resolveApiKey` via the cliKey path and skip a duplicate prompt.
244
+ // Without this, the user gets prompted twice for the same provider when
245
+ // create.ts and the pipeline both call resolveApiKey independently.
246
+ if (!opts.key) opts.key = apiKey;
247
+ // If user did NOT specify a separate codegen provider/key, the pipeline will
248
+ // fall back to spec provider — pre-fill codegenKey too so that path is silent.
249
+ const codegenProviderName = (opts.codegenProvider as string) || config.codegenProvider || providerName;
250
+ if (codegenProviderName === providerName && !opts.codegenKey) {
251
+ opts.codegenKey = apiKey;
252
+ }
253
+
240
254
  // ── Select repo(s) from registered list ────────────────────────────────
241
255
  const registeredRepos = await getRegisteredRepos();
242
256
 
@@ -0,0 +1,176 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { confirm } from "@inquirer/prompts";
4
+ import {
5
+ loadFixHistory,
6
+ pruneFixHistory,
7
+ aggregateFixPatterns,
8
+ detectPromotionCandidates,
9
+ computeFixHistoryStats,
10
+ FIX_HISTORY_FILE,
11
+ } from "../../core/fix-history";
12
+ import { appendDirectLesson } from "../../core/knowledge-memory";
13
+ import { loadConfig } from "../utils";
14
+
15
+ export function registerFixHistory(program: Command): void {
16
+ program
17
+ .command("fix-history")
18
+ .description("Inspect and manage the import auto-fix history ledger")
19
+ .option("--list", "List raw entries instead of the aggregated summary")
20
+ .option("--prune <days>", "Remove entries older than N days")
21
+ .option("--promote", "Review patterns above threshold and promote to constitution §9")
22
+ .option("--threshold <n>", "Override promotion threshold (default from config, usually 5)")
23
+ .action(async (opts) => {
24
+ const currentDir = process.cwd();
25
+ const config = await loadConfig(currentDir);
26
+ const history = await loadFixHistory(currentDir);
27
+
28
+ // ── --prune ────────────────────────────────────────────────────────────
29
+ if (opts.prune !== undefined) {
30
+ const days = parseInt(opts.prune, 10);
31
+ if (isNaN(days) || days < 0) {
32
+ console.error(chalk.red(` --prune must be a non-negative integer (days)`));
33
+ process.exit(1);
34
+ }
35
+ const removed = await pruneFixHistory(currentDir, days);
36
+ if (removed === 0) {
37
+ console.log(chalk.gray(` No entries older than ${days} day(s) to remove.`));
38
+ } else {
39
+ console.log(chalk.green(` ✔ Removed ${removed} entry/entries older than ${days} day(s).`));
40
+ }
41
+ return;
42
+ }
43
+
44
+ // ── Empty ledger ──────────────────────────────────────────────────────
45
+ if (history.entries.length === 0) {
46
+ console.log(chalk.gray(`\nNo fix history found. Ledger: ${FIX_HISTORY_FILE}`));
47
+ console.log(chalk.gray(` The ledger is populated automatically when import-fixer repairs broken imports.`));
48
+ return;
49
+ }
50
+
51
+ // ── --promote ─────────────────────────────────────────────────────────
52
+ if (opts.promote) {
53
+ const threshold = opts.threshold
54
+ ? parseInt(opts.threshold, 10)
55
+ : config.fixHistoryPromotionThreshold ?? 5;
56
+
57
+ const candidates = detectPromotionCandidates(history, threshold);
58
+ if (candidates.length === 0) {
59
+ console.log(chalk.gray(`\n No patterns have crossed the promotion threshold (${threshold}x).`));
60
+ console.log(chalk.gray(` Run the pipeline more to accumulate patterns, or lower the threshold with --threshold <n>.`));
61
+ return;
62
+ }
63
+
64
+ console.log(
65
+ chalk.bold(`\n─── Promotion Candidates (threshold: ${threshold}x) ────────────────`)
66
+ );
67
+ console.log(
68
+ chalk.gray(` ${candidates.length} pattern(s) seen at least ${threshold} time(s).`)
69
+ );
70
+ console.log(
71
+ chalk.gray(` Accepted lessons are written to constitution §9 (accumulated lessons).\n`)
72
+ );
73
+
74
+ let accepted = 0;
75
+ for (const c of candidates) {
76
+ console.log(
77
+ chalk.cyan(`\n Pattern: ${c.aggregate.source}`) +
78
+ chalk.gray(` (${c.aggregate.count}x, ${c.aggregate.uniqueRunIds} run(s))`)
79
+ );
80
+ console.log(chalk.gray(` Names: { ${c.aggregate.names.join(", ")} }`));
81
+ console.log(chalk.gray(` Reason: ${c.aggregate.reason}`));
82
+ console.log(chalk.gray(` Lesson text:`));
83
+ console.log(chalk.white(` ${c.lessonText}`));
84
+
85
+ const ok = await confirm({
86
+ message: `Promote this pattern to constitution §9?`,
87
+ default: true,
88
+ });
89
+ if (!ok) {
90
+ console.log(chalk.gray(` skipped.`));
91
+ continue;
92
+ }
93
+ const result = await appendDirectLesson(currentDir, c.lessonText);
94
+ if (result.appended) {
95
+ console.log(chalk.green(` ✔ Appended to constitution §9.`));
96
+ accepted++;
97
+ } else {
98
+ console.log(chalk.yellow(` ⚠ Not appended: ${result.reason}`));
99
+ }
100
+ }
101
+
102
+ console.log(
103
+ chalk.green(`\n ✔ Promotion complete: ${accepted}/${candidates.length} pattern(s) added to §9.`)
104
+ );
105
+ return;
106
+ }
107
+
108
+ // ── --list: raw entries ───────────────────────────────────────────────
109
+ if (opts.list) {
110
+ console.log(chalk.bold(`\n─── Fix History Entries (${history.entries.length}) ────────────────`));
111
+ console.log(chalk.gray(` File: ${FIX_HISTORY_FILE}\n`));
112
+ // Show newest first
113
+ const sorted = [...history.entries].sort((a, b) => b.ts.localeCompare(a.ts));
114
+ for (const e of sorted.slice(0, 50)) {
115
+ const tsShort = e.ts.slice(0, 19).replace("T", " ");
116
+ const stageTag = e.fix.stage === "deterministic" ? chalk.green("[DSL]") : chalk.cyan("[AI ]");
117
+ console.log(
118
+ ` ${stageTag} ${chalk.gray(tsShort)} ${chalk.white(e.brokenImport.source)} ${chalk.gray(`{ ${e.brokenImport.names.join(", ")} }`)}`
119
+ );
120
+ console.log(
121
+ chalk.gray(` ${e.fix.kind} → ${e.fix.target} (run: ${e.runId}, ${e.brokenImport.file}:${e.brokenImport.line})`)
122
+ );
123
+ }
124
+ if (history.entries.length > 50) {
125
+ console.log(chalk.gray(`\n ... ${history.entries.length - 50} older entry(ies) not shown`));
126
+ }
127
+ return;
128
+ }
129
+
130
+ // ── Default: aggregated summary ───────────────────────────────────────
131
+ const stats = computeFixHistoryStats(history);
132
+ const patterns = aggregateFixPatterns(history);
133
+
134
+ console.log(chalk.bold(`\n─── Fix History Summary ────────────────────────────`));
135
+ console.log(chalk.gray(` File: ${FIX_HISTORY_FILE}\n`));
136
+ console.log(` Total fixes applied : ${chalk.white(String(stats.totalEntries))}`);
137
+ console.log(` Unique patterns : ${chalk.white(String(stats.uniquePatterns))}`);
138
+ console.log(` Runs that triggered : ${chalk.white(String(stats.uniqueRunIds))}`);
139
+ console.log(
140
+ ` Stage A (deterministic): ${chalk.green(String(stats.byStage.deterministic))} · Stage B (AI): ${chalk.cyan(String(stats.byStage.ai))}`
141
+ );
142
+ console.log(
143
+ ` Reasons : file_not_found ${stats.byReason.file_not_found} · missing_export ${stats.byReason.missing_export}`
144
+ );
145
+ if (stats.lastEntryTs) {
146
+ console.log(` Last fix : ${chalk.gray(stats.lastEntryTs.slice(0, 19).replace("T", " "))}`);
147
+ }
148
+
149
+ console.log(chalk.bold(`\n Top patterns (by frequency):`));
150
+ const top = patterns.slice(0, 10);
151
+ for (const p of top) {
152
+ const stageTag = p.fix.stage === "deterministic" ? chalk.green("[DSL]") : chalk.cyan("[AI ]");
153
+ const countColor = p.count >= 5 ? chalk.red : p.count >= 3 ? chalk.yellow : chalk.gray;
154
+ console.log(
155
+ ` ${stageTag} ${countColor(`${p.count}x`.padStart(4))} ${chalk.white(p.source)} ${chalk.gray(`{ ${p.names.join(", ")} }`)}`
156
+ );
157
+ console.log(chalk.gray(` last seen: ${p.lastSeen.slice(0, 10)} · ${p.uniqueRunIds} run(s)`));
158
+ }
159
+
160
+ const promotionThreshold = config.fixHistoryPromotionThreshold ?? 5;
161
+ const promotable = patterns.filter((p) => p.count >= promotionThreshold).length;
162
+ if (promotable > 0) {
163
+ console.log(
164
+ chalk.yellow(
165
+ `\n ⚠ ${promotable} pattern(s) have crossed the promotion threshold (${promotionThreshold}x). ` +
166
+ `Run \`ai-spec fix-history --promote\` to review.`
167
+ )
168
+ );
169
+ }
170
+
171
+ console.log(chalk.gray(`\n Hints:`));
172
+ console.log(chalk.gray(` ai-spec fix-history --list Show raw entries (newest first)`));
173
+ console.log(chalk.gray(` ai-spec fix-history --promote Promote repeated patterns to constitution §9`));
174
+ console.log(chalk.gray(` ai-spec fix-history --prune 30 Remove entries older than 30 days`));
175
+ });
176
+ }
@@ -18,7 +18,7 @@ import {
18
18
  buildGlobalConstitutionPrompt,
19
19
  } from "../../prompts/global-constitution.prompt";
20
20
  import { loadConfig, resolveApiKey } from "../utils";
21
- import { loadIndex, ProjectEntry } from "../../core/project-index";
21
+ import { loadIndex, runScan, saveIndex, ProjectEntry } from "../../core/project-index";
22
22
  import { detectRepoType } from "../../core/workspace-loader";
23
23
  import {
24
24
  RegisteredRepo,
@@ -224,8 +224,30 @@ export function registerInit(program: Command): void {
224
224
  .option("--consolidate", "Consolidate §9 accumulated lessons into §1–§8 core rules")
225
225
  .option("--dry-run", "Preview consolidation result without writing (use with --consolidate)")
226
226
  .option("--add-repo", "Add a new repo to the registered list")
227
+ .option("--status", "Show registered repos and constitution status (no changes made)")
227
228
  .action(async (opts) => {
228
229
  const currentDir = process.cwd();
230
+
231
+ // ── --status: show registered repos and constitution health ───────────
232
+ if (opts.status) {
233
+ const repos = await getRegisteredRepos();
234
+ if (repos.length === 0) {
235
+ console.log(chalk.yellow("\nNo repos registered. Run `ai-spec init` to add repos."));
236
+ return;
237
+ }
238
+ console.log(chalk.bold(`\n─── Registered Repos (${repos.length}) ──────────────────────`));
239
+ for (const r of repos) {
240
+ const constitutionIcon = r.hasConstitution ? chalk.green("✔ §C") : chalk.gray("○ §C");
241
+ const roleColor = r.role === "frontend" ? chalk.green : r.role === "backend" ? chalk.blue : r.role === "mobile" ? chalk.magenta : chalk.gray;
242
+ const pathExists = await fs.pathExists(r.path);
243
+ const pathStatus = pathExists ? chalk.gray(r.path) : chalk.red(`${r.path} (not found)`);
244
+ console.log(` ${constitutionIcon} ${roleColor(r.role.padEnd(9))} ${chalk.white(r.name.padEnd(20))} ${pathStatus}`);
245
+ }
246
+ console.log(chalk.gray(`\n Store: ${REPO_STORE_FILE}`));
247
+ console.log(chalk.gray(" Run `ai-spec init` to add repos or regenerate constitutions."));
248
+ return;
249
+ }
250
+
229
251
  const config = await loadConfig(currentDir);
230
252
 
231
253
  const providerName = opts.provider || config.provider || "gemini";
@@ -388,6 +410,19 @@ export function registerInit(program: Command): void {
388
410
  }
389
411
  console.log(chalk.gray(`\n Repo store: ${REPO_STORE_FILE}`));
390
412
  console.log(chalk.gray(` Next step: ai-spec create "your feature idea"`));
413
+
414
+ // ── Auto-scan: silently update project index ───────────────────────────
415
+ try {
416
+ const { index, added, updated: upd, nowMissing } = await runScan(currentDir, 2);
417
+ await saveIndex(currentDir, index);
418
+ const changes = added.length + upd.length + nowMissing.length;
419
+ if (changes > 0) {
420
+ console.log(chalk.gray(` Project index updated (${index.projects.filter((p: ProjectEntry) => !p.missing).length} projects found).`));
421
+ }
422
+ } catch {
423
+ // scan failure is non-blocking
424
+ }
425
+
391
426
  process.exit(0);
392
427
  });
393
428
  }
package/cli/index.ts CHANGED
@@ -8,8 +8,6 @@ import { registerCreate } from "./commands/create";
8
8
  import { registerReview } from "./commands/review";
9
9
  import { registerInit } from "./commands/init";
10
10
  import { registerConfig } from "./commands/config";
11
- import { registerModel } from "./commands/model";
12
- import { registerWorkspace } from "./commands/workspace";
13
11
  import { registerUpdate } from "./commands/update";
14
12
  import { registerExport } from "./commands/export";
15
13
  import { registerMock } from "./commands/mock";
@@ -20,7 +18,7 @@ import { registerLogs } from "./commands/logs";
20
18
  import { registerTypes } from "./commands/types";
21
19
  import { registerDashboard } from "./commands/dashboard";
22
20
  import { registerVcr } from "./commands/vcr";
23
- import { registerScan } from "./commands/scan";
21
+ import { registerFixHistory } from "./commands/fix-history";
24
22
 
25
23
  const program = new Command();
26
24
 
@@ -33,8 +31,6 @@ registerCreate(program);
33
31
  registerReview(program);
34
32
  registerInit(program);
35
33
  registerConfig(program);
36
- registerModel(program);
37
- registerWorkspace(program);
38
34
  registerUpdate(program);
39
35
  registerExport(program);
40
36
  registerMock(program);
@@ -45,6 +41,6 @@ registerLogs(program);
45
41
  registerTypes(program);
46
42
  registerDashboard(program);
47
43
  registerVcr(program);
48
- registerScan(program);
44
+ registerFixHistory(program);
49
45
 
50
46
  program.parse();
@@ -10,6 +10,12 @@ export type MultiRepoResult = {
10
10
  dsl: SpecDSL | null;
11
11
  repoAbsPath: string;
12
12
  role: string;
13
+ /** Files written by codegen during this run. Empty when codegen failed or never ran. */
14
+ generatedFiles: string[];
15
+ /** Human-readable reason when status === "failed". */
16
+ failureReason?: string;
17
+ /** Per-repo run ID (when RunLogger was created for this repo). */
18
+ runId?: string;
13
19
  };
14
20
 
15
21
  // ─── Banner ──────────────────────────────────────────────────────────────────