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.
- package/.claude/commands/add-lesson.md +34 -0
- package/.claude/commands/check-layers.md +65 -0
- package/.claude/commands/installed-deps.md +35 -0
- package/.claude/commands/recall-lessons.md +40 -0
- package/.claude/commands/scan-singletons.md +45 -0
- package/.claude/commands/verify-imports.md +48 -0
- package/.claude/settings.local.json +15 -1
- package/README.md +531 -213
- package/RELEASE_LOG.md +460 -0
- package/cli/commands/config.ts +93 -0
- package/cli/commands/create.ts +1233 -0
- package/cli/commands/dashboard.ts +62 -0
- package/cli/commands/export.ts +66 -0
- package/cli/commands/init.ts +190 -0
- package/cli/commands/learn.ts +30 -0
- package/cli/commands/logs.ts +106 -0
- package/cli/commands/mock.ts +175 -0
- package/cli/commands/model.ts +156 -0
- package/cli/commands/restore.ts +22 -0
- package/cli/commands/review.ts +63 -0
- package/cli/commands/scan.ts +99 -0
- package/cli/commands/trend.ts +36 -0
- package/cli/commands/types.ts +69 -0
- package/cli/commands/update.ts +178 -0
- package/cli/commands/vcr.ts +70 -0
- package/cli/commands/workspace.ts +219 -0
- package/cli/index.ts +34 -2240
- package/cli/utils.ts +83 -0
- package/core/combined-generator.ts +13 -3
- package/core/dashboard-generator.ts +340 -0
- package/core/design-dialogue.ts +124 -0
- package/core/dsl-feedback.ts +285 -0
- package/core/error-feedback.ts +46 -2
- package/core/project-index.ts +301 -0
- package/core/reviewer.ts +84 -6
- package/core/run-logger.ts +109 -3
- package/core/run-trend.ts +261 -0
- package/core/self-evaluator.ts +139 -7
- package/core/spec-generator.ts +14 -8
- package/core/task-generator.ts +17 -0
- package/core/types-generator.ts +219 -0
- package/core/vcr.ts +210 -0
- package/dist/cli/index.js +6692 -4512
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6692 -4512
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +420 -224
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +418 -224
- package/dist/index.mjs.map +1 -1
- package/docs-assets/purpose/architecture-overview.svg +64 -0
- package/docs-assets/purpose/create-pipeline.svg +113 -0
- package/docs-assets/purpose/task-layering.svg +74 -0
- package/package.json +6 -3
- package/prompts/codegen.prompt.ts +97 -9
- package/prompts/design.prompt.ts +59 -0
- package/prompts/spec.prompt.ts +8 -1
- package/prompts/tasks.prompt.ts +27 -2
- package/purpose.md +600 -174
- package/tests/dsl-extractor.test.ts +264 -0
- package/tests/dsl-feedback.test.ts +266 -0
- package/tests/dsl-validator.test.ts +283 -0
- package/tests/error-feedback.test.ts +292 -0
- package/tests/provider-utils.test.ts +173 -0
- package/tests/run-trend.test.ts +186 -0
- package/tests/self-evaluator.test.ts +339 -0
- package/tests/spec-assessor.test.ts +142 -0
- 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
|
+
}
|