ai-spec-dev 0.31.0 → 0.35.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 (70) hide show
  1. package/.claude/commands/add-lesson.md +34 -0
  2. package/.claude/commands/check-layers.md +65 -0
  3. package/.claude/commands/installed-deps.md +35 -0
  4. package/.claude/commands/recall-lessons.md +40 -0
  5. package/.claude/commands/scan-singletons.md +45 -0
  6. package/.claude/commands/verify-imports.md +48 -0
  7. package/.claude/settings.local.json +15 -1
  8. package/README.md +531 -213
  9. package/RELEASE_LOG.md +460 -0
  10. package/cli/commands/config.ts +93 -0
  11. package/cli/commands/create.ts +1233 -0
  12. package/cli/commands/dashboard.ts +62 -0
  13. package/cli/commands/export.ts +66 -0
  14. package/cli/commands/init.ts +190 -0
  15. package/cli/commands/learn.ts +30 -0
  16. package/cli/commands/logs.ts +106 -0
  17. package/cli/commands/mock.ts +175 -0
  18. package/cli/commands/model.ts +156 -0
  19. package/cli/commands/restore.ts +22 -0
  20. package/cli/commands/review.ts +63 -0
  21. package/cli/commands/scan.ts +99 -0
  22. package/cli/commands/trend.ts +36 -0
  23. package/cli/commands/types.ts +69 -0
  24. package/cli/commands/update.ts +178 -0
  25. package/cli/commands/vcr.ts +70 -0
  26. package/cli/commands/workspace.ts +219 -0
  27. package/cli/index.ts +34 -2240
  28. package/cli/utils.ts +83 -0
  29. package/core/combined-generator.ts +13 -3
  30. package/core/dashboard-generator.ts +340 -0
  31. package/core/design-dialogue.ts +124 -0
  32. package/core/dsl-feedback.ts +285 -0
  33. package/core/error-feedback.ts +46 -2
  34. package/core/project-index.ts +301 -0
  35. package/core/reviewer.ts +84 -6
  36. package/core/run-logger.ts +109 -3
  37. package/core/run-trend.ts +261 -0
  38. package/core/self-evaluator.ts +139 -7
  39. package/core/spec-generator.ts +14 -8
  40. package/core/task-generator.ts +17 -0
  41. package/core/types-generator.ts +219 -0
  42. package/core/vcr.ts +210 -0
  43. package/dist/cli/index.js +6692 -4512
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/cli/index.mjs +6692 -4512
  46. package/dist/cli/index.mjs.map +1 -1
  47. package/dist/index.d.mts +19 -5
  48. package/dist/index.d.ts +19 -5
  49. package/dist/index.js +420 -224
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.mjs +418 -224
  52. package/dist/index.mjs.map +1 -1
  53. package/docs-assets/purpose/architecture-overview.svg +64 -0
  54. package/docs-assets/purpose/create-pipeline.svg +113 -0
  55. package/docs-assets/purpose/task-layering.svg +74 -0
  56. package/package.json +6 -3
  57. package/prompts/codegen.prompt.ts +97 -9
  58. package/prompts/design.prompt.ts +59 -0
  59. package/prompts/spec.prompt.ts +8 -1
  60. package/prompts/tasks.prompt.ts +27 -2
  61. package/purpose.md +600 -174
  62. package/tests/dsl-extractor.test.ts +264 -0
  63. package/tests/dsl-feedback.test.ts +266 -0
  64. package/tests/dsl-validator.test.ts +283 -0
  65. package/tests/error-feedback.test.ts +292 -0
  66. package/tests/provider-utils.test.ts +173 -0
  67. package/tests/run-trend.test.ts +186 -0
  68. package/tests/self-evaluator.test.ts +339 -0
  69. package/tests/spec-assessor.test.ts +142 -0
  70. package/tests/task-generator.test.ts +230 -0
@@ -0,0 +1,70 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { listVcrRecordings, loadVcrRecording } from "../../core/vcr";
4
+
5
+ export function registerVcr(program: Command): void {
6
+ const vcr = program
7
+ .command("vcr")
8
+ .description("Manage VCR recordings for offline pipeline replay");
9
+
10
+ // ── ai-spec vcr list ──────────────────────────────────────────────────────
11
+ vcr
12
+ .command("list")
13
+ .description("List available VCR recordings in .ai-spec-vcr/")
14
+ .action(async () => {
15
+ const cwd = process.cwd();
16
+ const recordings = await listVcrRecordings(cwd);
17
+
18
+ if (recordings.length === 0) {
19
+ console.log(chalk.gray("No VCR recordings found."));
20
+ console.log(chalk.gray("Record a run with: ai-spec create --vcr-record <idea>"));
21
+ return;
22
+ }
23
+
24
+ console.log(chalk.cyan("\n─── VCR Recordings ─────────────────────────────"));
25
+ for (const r of recordings) {
26
+ console.log(
27
+ " " + chalk.white(r.runId) +
28
+ chalk.gray(` · ${r.entryCount} AI calls · ${r.providers.join(", ")}`) +
29
+ chalk.gray(` · ${r.recordedAt.slice(0, 10)}`)
30
+ );
31
+ }
32
+ console.log(chalk.cyan("─".repeat(49)));
33
+ console.log(chalk.gray("\nInspect : ai-spec vcr show <runId>"));
34
+ console.log(chalk.gray("Replay : ai-spec create --vcr-replay <runId> <idea>"));
35
+ });
36
+
37
+ // ── ai-spec vcr show <runId> ──────────────────────────────────────────────
38
+ vcr
39
+ .command("show <runId>")
40
+ .description("Show call-by-call details of a VCR recording")
41
+ .action(async (runId: string) => {
42
+ const cwd = process.cwd();
43
+ const recording = await loadVcrRecording(cwd, runId);
44
+
45
+ if (!recording) {
46
+ console.log(chalk.red(`Recording not found: ${runId}`));
47
+ console.log(chalk.gray(`Expected: .ai-spec-vcr/${runId}.json`));
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(chalk.cyan(`\n─── VCR: ${recording.runId} ──────────────────────────`));
52
+ console.log(chalk.gray(` Recorded at : ${recording.recordedAt}`));
53
+ console.log(chalk.gray(` Providers : ${recording.providers.join(", ")}`));
54
+ console.log(chalk.gray(` Total calls : ${recording.entryCount}`));
55
+ console.log(chalk.cyan("\n Calls:"));
56
+
57
+ for (const entry of recording.entries) {
58
+ const idx = String(entry.index).padStart(2, "0");
59
+ const preview = entry.promptPreview.slice(0, 90).replace(/\s+/g, " ");
60
+ console.log(
61
+ chalk.gray(` [${idx}]`) + " " +
62
+ chalk.white(`${entry.providerName}/${entry.modelName}`) +
63
+ chalk.gray(` ${entry.durationMs}ms hash:${entry.callHash}`)
64
+ );
65
+ console.log(chalk.gray(` "${preview}..."`));
66
+ }
67
+
68
+ console.log(chalk.cyan("─".repeat(49)));
69
+ });
70
+ }
@@ -0,0 +1,219 @@
1
+ import { Command } from "commander";
2
+ import * as path from "path";
3
+ import * as fs from "fs-extra";
4
+ import chalk from "chalk";
5
+ import { input, select, confirm } from "@inquirer/prompts";
6
+ import {
7
+ WorkspaceLoader,
8
+ WorkspaceConfig,
9
+ RepoConfig,
10
+ WORKSPACE_CONFIG_FILE,
11
+ detectRepoType,
12
+ } from "../../core/workspace-loader";
13
+
14
+ export function registerWorkspace(program: Command): void {
15
+ const workspaceCmd = program
16
+ .command("workspace")
17
+ .description("Manage multi-repo workspace configuration");
18
+
19
+ // ── workspace init ──────────────────────────────────────────────────────────
20
+ workspaceCmd
21
+ .command("init")
22
+ .description(`Interactive workspace setup — creates ${WORKSPACE_CONFIG_FILE}`)
23
+ .action(async () => {
24
+ const currentDir = process.cwd();
25
+ const configPath = path.join(currentDir, WORKSPACE_CONFIG_FILE);
26
+
27
+ if (await fs.pathExists(configPath)) {
28
+ const overwrite = await confirm({
29
+ message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
30
+ default: false,
31
+ });
32
+ if (!overwrite) {
33
+ console.log(chalk.gray(" Cancelled."));
34
+ return;
35
+ }
36
+ }
37
+
38
+ console.log(chalk.blue("\n─── Workspace Setup ────────────────────────────"));
39
+
40
+ const workspaceName = await input({
41
+ message: "Workspace name:",
42
+ validate: (v) => v.trim().length > 0 || "Name cannot be empty",
43
+ });
44
+
45
+ const repos: RepoConfig[] = [];
46
+
47
+ const useAutoScan = await confirm({
48
+ message: "Auto-scan sibling directories for repos?",
49
+ default: true,
50
+ });
51
+
52
+ if (useAutoScan) {
53
+ const workspaceLoader = new WorkspaceLoader(currentDir);
54
+ const detected = await workspaceLoader.autoDetect();
55
+
56
+ if (detected.length === 0) {
57
+ console.log(chalk.yellow(" No recognizable repos found in sibling directories."));
58
+ } else {
59
+ console.log(chalk.cyan("\n Detected repos:"));
60
+ for (const r of detected) {
61
+ console.log(chalk.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
62
+ }
63
+
64
+ const keepAll = await confirm({
65
+ message: `Include all ${detected.length} detected repo(s)?`,
66
+ default: true,
67
+ });
68
+
69
+ if (keepAll) {
70
+ repos.push(...detected);
71
+ } else {
72
+ for (const r of detected) {
73
+ const keep = await confirm({
74
+ message: `Include "${r.name}" (${r.role}, ${r.type})?`,
75
+ default: true,
76
+ });
77
+ if (keep) repos.push(r);
78
+ }
79
+ }
80
+ console.log(chalk.green(` ✔ ${repos.length} repo(s) added from auto-scan.`));
81
+ }
82
+ }
83
+
84
+ const repoTypeChoices = [
85
+ { name: "node-express (Node.js/Express backend)", value: "node-express" },
86
+ { name: "node-koa (Node.js/Koa backend)", value: "node-koa" },
87
+ { name: "go (Go backend)", value: "go" },
88
+ { name: "python (Python backend)", value: "python" },
89
+ { name: "java (Java/Spring backend)", value: "java" },
90
+ { name: "rust (Rust backend)", value: "rust" },
91
+ { name: "php (PHP/Lumen/Laravel backend)", value: "php" },
92
+ { name: "react (React frontend)", value: "react" },
93
+ { name: "next (Next.js)", value: "next" },
94
+ { name: "vue (Vue frontend)", value: "vue" },
95
+ { name: "react-native (React Native mobile)", value: "react-native" },
96
+ { name: "unknown", value: "unknown" },
97
+ ];
98
+
99
+ let addMore = await confirm({
100
+ message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
101
+ default: repos.length === 0,
102
+ });
103
+
104
+ while (addMore) {
105
+ console.log(chalk.cyan(`\n Adding repo #${repos.length + 1}`));
106
+
107
+ const repoName = await input({
108
+ message: "Repo name (e.g. api, web, app):",
109
+ validate: (v) => {
110
+ if (!v.trim()) return "Name cannot be empty";
111
+ if (repos.some((r) => r.name === v.trim())) return "Name already used";
112
+ return true;
113
+ },
114
+ });
115
+
116
+ const repoPath = await input({
117
+ message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
118
+ default: `./${repoName}`,
119
+ });
120
+
121
+ const absPath = path.resolve(currentDir, repoPath);
122
+ let detectedType = "unknown";
123
+ let detectedRole = "shared";
124
+
125
+ if (await fs.pathExists(absPath)) {
126
+ const { type, role } = await detectRepoType(absPath);
127
+ detectedType = type;
128
+ detectedRole = role;
129
+ console.log(chalk.gray(` Auto-detected: type=${type}, role=${role}`));
130
+ } else {
131
+ console.log(chalk.yellow(` Path "${absPath}" not found — type/role will be manual.`));
132
+ }
133
+
134
+ const repoType = await select({
135
+ message: `Repo type for "${repoName}":`,
136
+ choices: repoTypeChoices,
137
+ default: detectedType,
138
+ });
139
+
140
+ const repoRole = await select({
141
+ message: `Repo role for "${repoName}":`,
142
+ choices: [
143
+ { name: "backend", value: "backend" },
144
+ { name: "frontend", value: "frontend" },
145
+ { name: "mobile", value: "mobile" },
146
+ { name: "shared", value: "shared" },
147
+ ],
148
+ default: detectedRole,
149
+ });
150
+
151
+ repos.push({
152
+ name: repoName,
153
+ path: repoPath,
154
+ type: repoType as RepoConfig["type"],
155
+ role: repoRole as RepoConfig["role"],
156
+ });
157
+
158
+ console.log(chalk.green(` ✔ Added: ${repoName} (${repoRole}, ${repoType})`));
159
+
160
+ addMore = await confirm({
161
+ message: "Add another repo?",
162
+ default: false,
163
+ });
164
+ }
165
+
166
+ const workspaceConfig: WorkspaceConfig = { name: workspaceName, repos };
167
+
168
+ console.log(chalk.cyan("\n Workspace summary:"));
169
+ console.log(chalk.gray(` Name: ${workspaceName}`));
170
+ for (const r of repos) {
171
+ console.log(chalk.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
172
+ }
173
+
174
+ const ok = await confirm({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
175
+ if (!ok) {
176
+ console.log(chalk.gray(" Cancelled."));
177
+ return;
178
+ }
179
+
180
+ const loader = new WorkspaceLoader(currentDir);
181
+ const saved = await loader.save(workspaceConfig);
182
+ console.log(chalk.green(`\n ✔ Workspace saved: ${saved}`));
183
+ console.log(chalk.gray(` Run \`ai-spec create "your feature"\` — workspace mode will activate automatically.`));
184
+ });
185
+
186
+ // ── workspace status ────────────────────────────────────────────────────────
187
+ workspaceCmd
188
+ .command("status")
189
+ .description("Show current workspace configuration")
190
+ .action(async () => {
191
+ const currentDir = process.cwd();
192
+ const loader = new WorkspaceLoader(currentDir);
193
+ const config = await loader.load();
194
+
195
+ if (!config) {
196
+ console.log(chalk.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
197
+ console.log(chalk.gray(" Run `ai-spec workspace init` to create one."));
198
+ return;
199
+ }
200
+
201
+ console.log(chalk.bold(`\nWorkspace: ${config.name}`));
202
+ console.log(chalk.gray(` Config: ${path.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
203
+ console.log(chalk.gray(` Repos (${config.repos.length}):\n`));
204
+
205
+ for (const repo of config.repos) {
206
+ const absPath = loader.resolveAbsPath(repo);
207
+ const exists = await fs.pathExists(absPath);
208
+ const status = exists ? chalk.green("found") : chalk.red("not found");
209
+
210
+ console.log(
211
+ ` ${chalk.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
212
+ );
213
+ console.log(chalk.gray(` path: ${absPath}`));
214
+ if (repo.constitution) {
215
+ console.log(chalk.green(` constitution: found`));
216
+ }
217
+ }
218
+ });
219
+ }