@bamptee/aia-code 0.2.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 +308 -0
- package/bin/aia.js +6 -0
- package/package.json +41 -0
- package/src/cli.js +25 -0
- package/src/commands/feature.js +17 -0
- package/src/commands/init.js +20 -0
- package/src/commands/repo.js +29 -0
- package/src/commands/reset.js +17 -0
- package/src/commands/run.js +16 -0
- package/src/commands/status.js +37 -0
- package/src/constants.js +43 -0
- package/src/knowledge-loader.js +43 -0
- package/src/logger.js +28 -0
- package/src/models.js +78 -0
- package/src/prompt-builder.js +97 -0
- package/src/providers/anthropic.js +11 -0
- package/src/providers/cli-runner.js +53 -0
- package/src/providers/gemini.js +11 -0
- package/src/providers/openai.js +11 -0
- package/src/providers/registry.js +38 -0
- package/src/services/config.js +33 -0
- package/src/services/feature.js +57 -0
- package/src/services/model-call.js +11 -0
- package/src/services/repo-scan.js +59 -0
- package/src/services/runner.js +46 -0
- package/src/services/scaffold.js +9 -0
- package/src/services/status.js +70 -0
- package/src/utils.js +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# AIA - AI Architecture Assistant
|
|
2
|
+
|
|
3
|
+
CLI tool that orchestrates AI-assisted development workflows using a `.aia` folder convention.
|
|
4
|
+
|
|
5
|
+
AIA structures your feature development into steps (brief, spec, tech-spec, etc.), builds rich prompts from project context and knowledge files, and delegates execution to AI CLI tools (Claude Code, Codex CLI, Gemini CLI) with weighted random model selection.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
node bin/aia.js init
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
AIA delegates to AI CLI tools. Install the ones you need:
|
|
17
|
+
|
|
18
|
+
| Provider | CLI | Install |
|
|
19
|
+
|----------|-----|---------|
|
|
20
|
+
| Anthropic | `claude` (Claude Code) | `npm install -g @anthropic-ai/claude-code` |
|
|
21
|
+
| OpenAI | `codex` (Codex CLI) | `npm install -g @openai/codex` |
|
|
22
|
+
| Google | `gemini` (Gemini CLI) | `npm install -g @anthropic-ai/gemini-cli` |
|
|
23
|
+
|
|
24
|
+
Each CLI manages its own authentication. Run `claude`, `codex`, or `gemini` once to log in before using AIA.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
| Command | Description |
|
|
29
|
+
|---------|-------------|
|
|
30
|
+
| `aia init` | Create `.aia/` folder structure and default config |
|
|
31
|
+
| `aia feature <name>` | Create a new feature workspace |
|
|
32
|
+
| `aia run <step> <feature>` | Execute a step for a feature using AI |
|
|
33
|
+
| `aia status <feature>` | Show the current status of a feature |
|
|
34
|
+
| `aia reset <step> <feature>` | Reset a step to pending so it can be re-run |
|
|
35
|
+
| `aia repo scan` | Scan codebase and generate `repo-map.json` |
|
|
36
|
+
|
|
37
|
+
## Integrate into an existing project
|
|
38
|
+
|
|
39
|
+
### 1. Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd your-project
|
|
43
|
+
npm install /path/to/aia-code
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or add it as a dev dependency in your `package.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"aia": "file:../aia-code"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Initialize
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx aia init
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This creates:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
your-project/
|
|
66
|
+
.aia/
|
|
67
|
+
config.yaml
|
|
68
|
+
context/
|
|
69
|
+
knowledge/
|
|
70
|
+
prompts/
|
|
71
|
+
features/
|
|
72
|
+
logs/
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Write context files
|
|
76
|
+
|
|
77
|
+
These files describe your project to the AI. They are injected into every prompt.
|
|
78
|
+
|
|
79
|
+
```markdown
|
|
80
|
+
<!-- .aia/context/project.md -->
|
|
81
|
+
# Project
|
|
82
|
+
E-commerce SaaS platform built with Node.js and MongoDB.
|
|
83
|
+
Stack: Express, React, Redis, PostgreSQL.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```markdown
|
|
87
|
+
<!-- .aia/context/architecture.md -->
|
|
88
|
+
# Architecture
|
|
89
|
+
Microservices communicating via RabbitMQ.
|
|
90
|
+
API gateway with JWT auth.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reference them in `config.yaml`:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
context_files:
|
|
97
|
+
- context/project.md
|
|
98
|
+
- context/architecture.md
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 4. Write knowledge files
|
|
102
|
+
|
|
103
|
+
Knowledge files contain reusable technical guidelines, organized by category.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
.aia/knowledge/
|
|
107
|
+
backend/
|
|
108
|
+
nodejs.md # Node.js patterns and conventions
|
|
109
|
+
mongo-patterns.md # MongoDB query patterns
|
|
110
|
+
api-design.md # REST API guidelines
|
|
111
|
+
frontend/
|
|
112
|
+
react-patterns.md # React component patterns
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Set the default knowledge categories in `config.yaml`:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
knowledge_default:
|
|
119
|
+
- backend
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each feature can override this via its `status.yaml` `knowledge` field.
|
|
123
|
+
|
|
124
|
+
### 5. Write prompt templates
|
|
125
|
+
|
|
126
|
+
One template per step, stored in `.aia/prompts/`:
|
|
127
|
+
|
|
128
|
+
```markdown
|
|
129
|
+
<!-- .aia/prompts/brief.md -->
|
|
130
|
+
Write a product brief for this feature.
|
|
131
|
+
Include: problem statement, target users, success metrics.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```markdown
|
|
135
|
+
<!-- .aia/prompts/tech-spec.md -->
|
|
136
|
+
Write a technical specification.
|
|
137
|
+
Include: data models, API endpoints, architecture decisions, trade-offs.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Required templates (one per step you want to run):
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
.aia/prompts/brief.md
|
|
144
|
+
.aia/prompts/ba-spec.md
|
|
145
|
+
.aia/prompts/questions.md
|
|
146
|
+
.aia/prompts/tech-spec.md
|
|
147
|
+
.aia/prompts/challenge.md
|
|
148
|
+
.aia/prompts/dev-plan.md
|
|
149
|
+
.aia/prompts/review.md
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 6. Configure models
|
|
153
|
+
|
|
154
|
+
In `config.yaml`, assign models to steps with probability weights:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
models:
|
|
158
|
+
brief:
|
|
159
|
+
- model: claude-sonnet-4-6
|
|
160
|
+
weight: 1
|
|
161
|
+
|
|
162
|
+
questions:
|
|
163
|
+
- model: claude-sonnet-4-6
|
|
164
|
+
weight: 0.5
|
|
165
|
+
- model: o3
|
|
166
|
+
weight: 0.5
|
|
167
|
+
|
|
168
|
+
tech-spec:
|
|
169
|
+
- model: gpt-4.1
|
|
170
|
+
weight: 0.6
|
|
171
|
+
- model: gemini-2.5-pro
|
|
172
|
+
weight: 0.4
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Weights don't need to sum to 1 -- they are normalized at runtime.
|
|
176
|
+
|
|
177
|
+
Supported model prefixes and the CLI used:
|
|
178
|
+
|
|
179
|
+
| Prefix | CLI | Examples |
|
|
180
|
+
|--------|-----|----------|
|
|
181
|
+
| `claude-*` | `claude -p --model` | `claude-sonnet-4-6`, `claude-3-7-sonnet` |
|
|
182
|
+
| `gpt-*`, `o[0-9]*` | `codex exec` | `gpt-4.1`, `o3`, `o4-mini` |
|
|
183
|
+
| `gemini-*` | `gemini` | `gemini-2.5-pro`, `gemini-2.5-flash` |
|
|
184
|
+
|
|
185
|
+
### 7. Create a feature and run steps
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx aia feature session-replay
|
|
189
|
+
npx aia run brief session-replay
|
|
190
|
+
npx aia status session-replay
|
|
191
|
+
npx aia run tech-spec session-replay
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Each run:
|
|
195
|
+
1. Loads context files + knowledge + prior step outputs
|
|
196
|
+
2. Selects a model based on weights
|
|
197
|
+
3. Sends the assembled prompt to the CLI tool via stdin
|
|
198
|
+
4. Streams the response to stdout in real-time
|
|
199
|
+
5. Saves the output to `.aia/features/<name>/<step>.md`
|
|
200
|
+
6. Updates `status.yaml` (marks step `done`, advances `current_step`)
|
|
201
|
+
7. Logs execution to `.aia/logs/execution.log`
|
|
202
|
+
|
|
203
|
+
To re-run a step:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx aia reset tech-spec session-replay
|
|
207
|
+
npx aia run tech-spec session-replay
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 8. Scan your repo
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx aia repo scan
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Generates `.aia/repo-map.json` -- a categorized index of your source files (services, models, routes, controllers, middleware, utils, config). Useful as additional context for prompts.
|
|
217
|
+
|
|
218
|
+
## Project structure
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
bin/
|
|
222
|
+
aia.js # CLI entrypoint
|
|
223
|
+
src/
|
|
224
|
+
cli.js # Commander program, registers commands
|
|
225
|
+
constants.js # Shared constants (dirs, steps, scan config)
|
|
226
|
+
models.js # Config loader + validation, weighted model selection
|
|
227
|
+
logger.js # Execution log writer
|
|
228
|
+
knowledge-loader.js # Recursive markdown loader by category
|
|
229
|
+
prompt-builder.js # Assembles full prompt from all sources
|
|
230
|
+
utils.js # Shared filesystem helpers
|
|
231
|
+
commands/
|
|
232
|
+
init.js # aia init
|
|
233
|
+
feature.js # aia feature <name>
|
|
234
|
+
run.js # aia run <step> <feature>
|
|
235
|
+
status.js # aia status <feature>
|
|
236
|
+
reset.js # aia reset <step> <feature>
|
|
237
|
+
repo.js # aia repo scan
|
|
238
|
+
providers/
|
|
239
|
+
registry.js # Model name -> provider routing
|
|
240
|
+
cli-runner.js # Shared CLI spawn logic (stdout streaming, timeout, error handling)
|
|
241
|
+
openai.js # codex exec
|
|
242
|
+
anthropic.js # claude -p
|
|
243
|
+
gemini.js # gemini
|
|
244
|
+
services/
|
|
245
|
+
scaffold.js # .aia/ folder creation
|
|
246
|
+
config.js # Default config generation
|
|
247
|
+
feature.js # Feature workspace creation + validation
|
|
248
|
+
status.js # status.yaml read/write/reset
|
|
249
|
+
runner.js # Step execution orchestrator
|
|
250
|
+
model-call.js # Provider dispatch
|
|
251
|
+
repo-scan.js # Codebase scanner + categorizer
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Feature workflow
|
|
255
|
+
|
|
256
|
+
Each feature follows a fixed pipeline:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
brief -> ba-spec -> questions -> tech-spec -> challenge -> dev-plan -> review
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`status.yaml` tracks progress:
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
feature: session-replay
|
|
266
|
+
current_step: tech-spec
|
|
267
|
+
steps:
|
|
268
|
+
brief: done
|
|
269
|
+
ba-spec: done
|
|
270
|
+
questions: pending
|
|
271
|
+
tech-spec: pending
|
|
272
|
+
challenge: pending
|
|
273
|
+
dev-plan: pending
|
|
274
|
+
review: pending
|
|
275
|
+
knowledge:
|
|
276
|
+
- backend
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Prompt assembly
|
|
280
|
+
|
|
281
|
+
When you run a step, the prompt is built from four sections:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
=== CONTEXT ===
|
|
285
|
+
(content of context files from config.yaml)
|
|
286
|
+
|
|
287
|
+
=== KNOWLEDGE ===
|
|
288
|
+
(all .md files from the knowledge categories)
|
|
289
|
+
|
|
290
|
+
=== FEATURE ===
|
|
291
|
+
(outputs of all prior steps for this feature)
|
|
292
|
+
|
|
293
|
+
=== TASK ===
|
|
294
|
+
(content of prompts/<step>.md)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The full prompt is piped to the CLI tool via stdin, so there are no argument length limits.
|
|
298
|
+
|
|
299
|
+
## Dependencies
|
|
300
|
+
|
|
301
|
+
Only four runtime dependencies:
|
|
302
|
+
|
|
303
|
+
- `commander` -- CLI framework
|
|
304
|
+
- `yaml` -- YAML parse/stringify
|
|
305
|
+
- `fs-extra` -- filesystem utilities
|
|
306
|
+
- `chalk` -- terminal colors
|
|
307
|
+
|
|
308
|
+
AI calls use `child_process.spawn` to delegate to installed CLI tools.
|
package/bin/aia.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bamptee/aia-code",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AI Architecture Assistant - orchestrate AI-assisted development workflows via CLI tools (Claude, Codex, Gemini)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"aia": "bin/aia.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"cli",
|
|
17
|
+
"architecture",
|
|
18
|
+
"claude",
|
|
19
|
+
"codex",
|
|
20
|
+
"gemini",
|
|
21
|
+
"orchestrator",
|
|
22
|
+
"development",
|
|
23
|
+
"workflow"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/bamptee/aia-code.git"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"start": "node bin/aia.js"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"chalk": "^5.3.0",
|
|
37
|
+
"commander": "^12.1.0",
|
|
38
|
+
"fs-extra": "^11.2.0",
|
|
39
|
+
"yaml": "^2.7.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { registerInitCommand } from './commands/init.js';
|
|
3
|
+
import { registerFeatureCommand } from './commands/feature.js';
|
|
4
|
+
import { registerRunCommand } from './commands/run.js';
|
|
5
|
+
import { registerRepoCommand } from './commands/repo.js';
|
|
6
|
+
import { registerStatusCommand } from './commands/status.js';
|
|
7
|
+
import { registerResetCommand } from './commands/reset.js';
|
|
8
|
+
|
|
9
|
+
export function createCli() {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('aia')
|
|
14
|
+
.description('AI Architecture Assistant')
|
|
15
|
+
.version('0.1.0');
|
|
16
|
+
|
|
17
|
+
registerInitCommand(program);
|
|
18
|
+
registerFeatureCommand(program);
|
|
19
|
+
registerRunCommand(program);
|
|
20
|
+
registerRepoCommand(program);
|
|
21
|
+
registerStatusCommand(program);
|
|
22
|
+
registerResetCommand(program);
|
|
23
|
+
|
|
24
|
+
return program;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { createFeature } from '../services/feature.js';
|
|
3
|
+
|
|
4
|
+
export function registerFeatureCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('feature <name>')
|
|
7
|
+
.description('Create a new feature workspace under .aia/features/')
|
|
8
|
+
.action(async (name) => {
|
|
9
|
+
try {
|
|
10
|
+
await createFeature(name);
|
|
11
|
+
console.log(chalk.green(`Feature "${name}" created.`));
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error(chalk.red(err.message));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { createAiaStructure } from '../services/scaffold.js';
|
|
3
|
+
import { writeDefaultConfig } from '../services/config.js';
|
|
4
|
+
import { AIA_DIR } from '../constants.js';
|
|
5
|
+
|
|
6
|
+
export function registerInitCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('init')
|
|
9
|
+
.description('Initialize .aia folder structure and default config')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
await createAiaStructure();
|
|
13
|
+
await writeDefaultConfig();
|
|
14
|
+
console.log(chalk.green(`Initialized ${AIA_DIR}/ project structure.`));
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error(chalk.red(`Init failed: ${err.message}`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { scanRepo } from '../services/repo-scan.js';
|
|
3
|
+
|
|
4
|
+
export function registerRepoCommand(program) {
|
|
5
|
+
const repo = program
|
|
6
|
+
.command('repo')
|
|
7
|
+
.description('Repository intelligence tools');
|
|
8
|
+
|
|
9
|
+
repo
|
|
10
|
+
.command('scan')
|
|
11
|
+
.description('Scan the codebase and generate .aia/repo-map.json')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const { map, total, outputPath } = await scanRepo();
|
|
15
|
+
|
|
16
|
+
const categories = Object.keys(map);
|
|
17
|
+
const matched = Object.values(map).reduce((s, f) => s + f.length, 0);
|
|
18
|
+
|
|
19
|
+
console.log(chalk.green(`Scanned ${total} files, categorized ${matched} into ${categories.length} groups.`));
|
|
20
|
+
for (const [cat, files] of Object.entries(map)) {
|
|
21
|
+
console.log(chalk.cyan(` ${cat}: ${files.length} files`));
|
|
22
|
+
}
|
|
23
|
+
console.log(chalk.gray(`Written to ${outputPath}`));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error(chalk.red(err.message));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { resetStep } from '../services/status.js';
|
|
3
|
+
|
|
4
|
+
export function registerResetCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('reset <step> <feature>')
|
|
7
|
+
.description('Reset a step to pending so it can be re-run')
|
|
8
|
+
.action(async (step, feature) => {
|
|
9
|
+
try {
|
|
10
|
+
await resetStep(feature, step);
|
|
11
|
+
console.log(chalk.green(`Step "${step}" reset to pending for feature "${feature}".`));
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error(chalk.red(err.message));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { runStep } from '../services/runner.js';
|
|
3
|
+
|
|
4
|
+
export function registerRunCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('run <step> <feature>')
|
|
7
|
+
.description('Execute a step for a feature using the configured AI model')
|
|
8
|
+
.action(async (step, feature) => {
|
|
9
|
+
try {
|
|
10
|
+
await runStep(step, feature);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
console.error(chalk.red(err.message));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { STEP_STATUS } from '../constants.js';
|
|
3
|
+
import { loadStatus } from '../services/status.js';
|
|
4
|
+
|
|
5
|
+
const STATUS_COLORS = {
|
|
6
|
+
[STEP_STATUS.DONE]: chalk.green,
|
|
7
|
+
[STEP_STATUS.IN_PROGRESS]: chalk.yellow,
|
|
8
|
+
[STEP_STATUS.ERROR]: chalk.red,
|
|
9
|
+
[STEP_STATUS.PENDING]: chalk.gray,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function registerStatusCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('status <feature>')
|
|
15
|
+
.description('Show the current status of a feature')
|
|
16
|
+
.action(async (feature) => {
|
|
17
|
+
try {
|
|
18
|
+
const status = await loadStatus(feature);
|
|
19
|
+
|
|
20
|
+
console.log(chalk.bold(`Feature: ${status.feature}`));
|
|
21
|
+
console.log(chalk.bold(`Current step: ${status.current_step}\n`));
|
|
22
|
+
|
|
23
|
+
for (const [step, value] of Object.entries(status.steps)) {
|
|
24
|
+
const colorize = STATUS_COLORS[value] ?? chalk.white;
|
|
25
|
+
const marker = value === STEP_STATUS.DONE ? 'v' : value === STEP_STATUS.ERROR ? 'x' : '-';
|
|
26
|
+
console.log(` ${colorize(`[${marker}] ${step}: ${value}`)}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (status.knowledge?.length) {
|
|
30
|
+
console.log(chalk.gray(`\nKnowledge: ${status.knowledge.join(', ')}`));
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(chalk.red(err.message));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const AIA_DIR = '.aia';
|
|
2
|
+
|
|
3
|
+
export const AIA_FOLDERS = [
|
|
4
|
+
'context',
|
|
5
|
+
'knowledge',
|
|
6
|
+
'prompts',
|
|
7
|
+
'features',
|
|
8
|
+
'logs',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const SCAN_IGNORE = new Set([
|
|
12
|
+
'node_modules',
|
|
13
|
+
'dist',
|
|
14
|
+
'.git',
|
|
15
|
+
'.aia',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export const SCAN_CATEGORIES = {
|
|
19
|
+
services: /\bservices?\b/i,
|
|
20
|
+
models: /\bmodels?\b/i,
|
|
21
|
+
routes: /\broutes?\b/i,
|
|
22
|
+
controllers: /\bcontrollers?\b/i,
|
|
23
|
+
middleware: /\bmiddleware\b/i,
|
|
24
|
+
utils: /\b(utils?|helpers?)\b/i,
|
|
25
|
+
config: /\bconfig\b/i,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const FEATURE_STEPS = [
|
|
29
|
+
'brief',
|
|
30
|
+
'ba-spec',
|
|
31
|
+
'questions',
|
|
32
|
+
'tech-spec',
|
|
33
|
+
'challenge',
|
|
34
|
+
'dev-plan',
|
|
35
|
+
'review',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const STEP_STATUS = {
|
|
39
|
+
PENDING: 'pending',
|
|
40
|
+
IN_PROGRESS: 'in-progress',
|
|
41
|
+
DONE: 'done',
|
|
42
|
+
ERROR: 'error',
|
|
43
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { AIA_DIR } from './constants.js';
|
|
4
|
+
|
|
5
|
+
async function collectMarkdownFiles(dir) {
|
|
6
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
7
|
+
const files = [];
|
|
8
|
+
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const fullPath = path.join(dir, entry.name);
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
files.push(...await collectMarkdownFiles(fullPath));
|
|
13
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
14
|
+
files.push(fullPath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return files;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadKnowledge(categories, root = process.cwd()) {
|
|
22
|
+
const knowledgeDir = path.join(root, AIA_DIR, 'knowledge');
|
|
23
|
+
const sections = [];
|
|
24
|
+
|
|
25
|
+
for (const category of categories) {
|
|
26
|
+
const categoryDir = path.join(knowledgeDir, category);
|
|
27
|
+
|
|
28
|
+
if (!(await fs.pathExists(categoryDir))) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const files = (await collectMarkdownFiles(categoryDir)).sort();
|
|
33
|
+
|
|
34
|
+
for (const filePath of files) {
|
|
35
|
+
const content = (await fs.readFile(filePath, 'utf-8')).trim();
|
|
36
|
+
if (content) {
|
|
37
|
+
sections.push(content);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return sections.join('\n\n---\n\n');
|
|
43
|
+
}
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { AIA_DIR } from './constants.js';
|
|
4
|
+
|
|
5
|
+
const LOG_FILE = 'execution.log';
|
|
6
|
+
|
|
7
|
+
function formatEntry({ feature, step, model, duration }) {
|
|
8
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, '');
|
|
9
|
+
const durationSec = (duration / 1000).toFixed(1);
|
|
10
|
+
|
|
11
|
+
return [
|
|
12
|
+
timestamp,
|
|
13
|
+
`feature=${feature}`,
|
|
14
|
+
`step=${step}`,
|
|
15
|
+
`model=${model}`,
|
|
16
|
+
`duration=${durationSec}s`,
|
|
17
|
+
'',
|
|
18
|
+
].join('\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function logExecution(entry, root = process.cwd()) {
|
|
22
|
+
const logDir = path.join(root, AIA_DIR, 'logs');
|
|
23
|
+
await fs.ensureDir(logDir);
|
|
24
|
+
|
|
25
|
+
const logPath = path.join(logDir, LOG_FILE);
|
|
26
|
+
const line = formatEntry(entry);
|
|
27
|
+
await fs.appendFile(logPath, line + '\n', 'utf-8');
|
|
28
|
+
}
|
package/src/models.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { AIA_DIR } from './constants.js';
|
|
6
|
+
|
|
7
|
+
export async function loadConfig(root = process.cwd()) {
|
|
8
|
+
const configPath = path.join(root, AIA_DIR, 'config.yaml');
|
|
9
|
+
|
|
10
|
+
if (!(await fs.pathExists(configPath))) {
|
|
11
|
+
throw new Error(`Config not found: ${configPath}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
15
|
+
const config = yaml.parse(raw);
|
|
16
|
+
|
|
17
|
+
validateConfig(config, configPath);
|
|
18
|
+
|
|
19
|
+
return config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validateConfig(config, configPath) {
|
|
23
|
+
if (!config || typeof config !== 'object') {
|
|
24
|
+
throw new Error(`Invalid config: ${configPath} must be a YAML object.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!config.models || typeof config.models !== 'object') {
|
|
28
|
+
throw new Error(`Invalid config: "models" section is required in ${configPath}.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const [step, models] of Object.entries(config.models)) {
|
|
32
|
+
if (!Array.isArray(models)) {
|
|
33
|
+
throw new Error(`Invalid config: models.${step} must be an array.`);
|
|
34
|
+
}
|
|
35
|
+
for (const entry of models) {
|
|
36
|
+
if (!entry.model || typeof entry.model !== 'string') {
|
|
37
|
+
throw new Error(`Invalid config: each entry in models.${step} must have a "model" string.`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof entry.weight !== 'number' || entry.weight <= 0) {
|
|
40
|
+
throw new Error(`Invalid config: each entry in models.${step} must have a positive "weight".`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function selectByWeight(models) {
|
|
47
|
+
if (models.length === 1) {
|
|
48
|
+
return models[0].model;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const totalWeight = models.reduce((sum, m) => sum + m.weight, 0);
|
|
52
|
+
const roll = Math.random() * totalWeight;
|
|
53
|
+
|
|
54
|
+
let cumulative = 0;
|
|
55
|
+
for (const entry of models) {
|
|
56
|
+
cumulative += entry.weight;
|
|
57
|
+
if (roll < cumulative) {
|
|
58
|
+
return entry.model;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return models[models.length - 1].model;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function resolveModel(step, root = process.cwd()) {
|
|
66
|
+
const config = await loadConfig(root);
|
|
67
|
+
const models = config.models?.[step];
|
|
68
|
+
|
|
69
|
+
if (!models || models.length === 0) {
|
|
70
|
+
throw new Error(`No models configured for step "${step}".`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const selected = selectByWeight(models);
|
|
74
|
+
|
|
75
|
+
console.log(chalk.cyan(`[AI] step=${step} model=${selected}`));
|
|
76
|
+
|
|
77
|
+
return selected;
|
|
78
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS } from './constants.js';
|
|
5
|
+
import { loadConfig } from './models.js';
|
|
6
|
+
import { loadKnowledge } from './knowledge-loader.js';
|
|
7
|
+
import { readIfExists } from './utils.js';
|
|
8
|
+
|
|
9
|
+
async function loadContextFiles(config, root) {
|
|
10
|
+
const files = config.context_files ?? [];
|
|
11
|
+
const sections = [];
|
|
12
|
+
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const content = await readIfExists(path.join(root, AIA_DIR, file));
|
|
15
|
+
if (content) {
|
|
16
|
+
sections.push(content);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return sections.join('\n\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loadFeatureFiles(feature, step, root) {
|
|
24
|
+
const stepIndex = FEATURE_STEPS.indexOf(step);
|
|
25
|
+
if (stepIndex === -1) {
|
|
26
|
+
throw new Error(`Unknown step "${step}".`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const featureDir = path.join(root, AIA_DIR, 'features', feature);
|
|
30
|
+
if (!(await fs.pathExists(featureDir))) {
|
|
31
|
+
throw new Error(`Feature "${feature}" not found.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const priorSteps = FEATURE_STEPS.slice(0, stepIndex);
|
|
35
|
+
const sections = [];
|
|
36
|
+
|
|
37
|
+
for (const s of priorSteps) {
|
|
38
|
+
const content = await readIfExists(path.join(featureDir, `${s}.md`));
|
|
39
|
+
if (content) {
|
|
40
|
+
sections.push(content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return sections.join('\n\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function resolveKnowledgeCategories(feature, config, root) {
|
|
48
|
+
const statusPath = path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
49
|
+
const raw = await readIfExists(statusPath);
|
|
50
|
+
|
|
51
|
+
if (raw) {
|
|
52
|
+
const status = yaml.parse(raw);
|
|
53
|
+
if (status?.knowledge?.length) {
|
|
54
|
+
return status.knowledge;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return config.knowledge_default ?? [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function loadPromptTemplate(step, root) {
|
|
62
|
+
const templatePath = path.join(root, AIA_DIR, 'prompts', `${step}.md`);
|
|
63
|
+
const content = await readIfExists(templatePath);
|
|
64
|
+
if (!content) {
|
|
65
|
+
throw new Error(`Prompt template not found: prompts/${step}.md`);
|
|
66
|
+
}
|
|
67
|
+
return content;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function buildPrompt(feature, step, root = process.cwd()) {
|
|
71
|
+
const config = await loadConfig(root);
|
|
72
|
+
|
|
73
|
+
const [context, knowledgeCategories, featureContent, task] = await Promise.all([
|
|
74
|
+
loadContextFiles(config, root),
|
|
75
|
+
resolveKnowledgeCategories(feature, config, root),
|
|
76
|
+
loadFeatureFiles(feature, step, root),
|
|
77
|
+
loadPromptTemplate(step, root),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const knowledge = await loadKnowledge(knowledgeCategories, root);
|
|
81
|
+
|
|
82
|
+
const parts = [];
|
|
83
|
+
|
|
84
|
+
parts.push('=== CONTEXT ===\n');
|
|
85
|
+
parts.push(context || '(no context files)');
|
|
86
|
+
|
|
87
|
+
parts.push('\n\n=== KNOWLEDGE ===\n');
|
|
88
|
+
parts.push(knowledge || '(no knowledge)');
|
|
89
|
+
|
|
90
|
+
parts.push('\n\n=== FEATURE ===\n');
|
|
91
|
+
parts.push(featureContent || '(no prior steps)');
|
|
92
|
+
|
|
93
|
+
parts.push('\n\n=== TASK ===\n');
|
|
94
|
+
parts.push(task);
|
|
95
|
+
|
|
96
|
+
return parts.join('\n');
|
|
97
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
4
|
+
|
|
5
|
+
export function runCli(command, args, { stdin: stdinData, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(command, args, {
|
|
8
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
9
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const chunks = [];
|
|
13
|
+
let stderr = '';
|
|
14
|
+
|
|
15
|
+
child.stdout.on('data', (data) => {
|
|
16
|
+
const text = data.toString();
|
|
17
|
+
process.stdout.write(text);
|
|
18
|
+
chunks.push(text);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
child.stderr.on('data', (data) => {
|
|
22
|
+
stderr += data.toString();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
child.kill('SIGTERM');
|
|
27
|
+
reject(new Error(`CLI timed out after ${timeoutMs / 1000}s: ${command} ${args.join(' ')}`));
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
|
|
30
|
+
child.on('error', (err) => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
if (err.code === 'ENOENT') {
|
|
33
|
+
reject(new Error(`CLI not found: "${command}". Make sure it is installed and in your PATH.`));
|
|
34
|
+
} else {
|
|
35
|
+
reject(err);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on('close', (code) => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (code !== 0) {
|
|
42
|
+
reject(new Error(`${command} exited with code ${code}:\n${stderr.trim()}`));
|
|
43
|
+
} else {
|
|
44
|
+
resolve(chunks.join(''));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (stdinData) {
|
|
49
|
+
child.stdin.write(stdinData);
|
|
50
|
+
child.stdin.end();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as openai from './openai.js';
|
|
2
|
+
import * as anthropic from './anthropic.js';
|
|
3
|
+
import * as gemini from './gemini.js';
|
|
4
|
+
|
|
5
|
+
const MODEL_ALIASES = {
|
|
6
|
+
'claude-default': { provider: anthropic, model: null },
|
|
7
|
+
'openai-default': { provider: openai, model: null },
|
|
8
|
+
'codex-default': { provider: openai, model: null },
|
|
9
|
+
'gemini-default': { provider: gemini, model: null },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const MODEL_PREFIXES = [
|
|
13
|
+
{ test: (m) => m.startsWith('gpt-') || /^o[0-9]/.test(m), provider: openai },
|
|
14
|
+
{ test: (m) => m.startsWith('claude-'), provider: anthropic },
|
|
15
|
+
{ test: (m) => m.startsWith('gemini-'), provider: gemini },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function resolveModelAlias(model) {
|
|
19
|
+
if (!model || typeof model !== 'string') {
|
|
20
|
+
throw new Error('Model name must be a non-empty string.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const alias = MODEL_ALIASES[model];
|
|
24
|
+
if (alias) {
|
|
25
|
+
return { provider: alias.provider, model: alias.model };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const match = MODEL_PREFIXES.find((entry) => entry.test(model));
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`No provider found for model "${model}".`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { provider: match.provider, model };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getProvider(model) {
|
|
37
|
+
return resolveModelAlias(model).provider;
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { AIA_DIR } from '../constants.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
models: {
|
|
8
|
+
questions: [
|
|
9
|
+
{ model: 'claude-default', weight: 0.5 },
|
|
10
|
+
{ model: 'openai-default', weight: 0.5 },
|
|
11
|
+
],
|
|
12
|
+
'tech-spec': [
|
|
13
|
+
{ model: 'claude-default', weight: 0.5 },
|
|
14
|
+
{ model: 'openai-default', weight: 0.5 },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
knowledge_default: ['backend'],
|
|
18
|
+
context_files: [
|
|
19
|
+
'context/project.md',
|
|
20
|
+
'context/architecture.md',
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function writeDefaultConfig(root = process.cwd()) {
|
|
25
|
+
const configPath = path.join(root, AIA_DIR, 'config.yaml');
|
|
26
|
+
|
|
27
|
+
if (await fs.pathExists(configPath)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = yaml.stringify(DEFAULT_CONFIG);
|
|
32
|
+
await fs.writeFile(configPath, content, 'utf-8');
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS } from '../constants.js';
|
|
5
|
+
|
|
6
|
+
const FEATURE_FILES = [
|
|
7
|
+
'status.yaml',
|
|
8
|
+
'brief.md',
|
|
9
|
+
'ba-spec.md',
|
|
10
|
+
'questions.md',
|
|
11
|
+
'tech-spec.md',
|
|
12
|
+
'challenge.md',
|
|
13
|
+
'dev-plan.md',
|
|
14
|
+
'review.md',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const FEATURE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
18
|
+
|
|
19
|
+
export function validateFeatureName(name) {
|
|
20
|
+
if (!name || !FEATURE_NAME_RE.test(name)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Invalid feature name "${name}". Use lowercase alphanumeric with hyphens (e.g. session-replay).`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildStatusYaml(name) {
|
|
28
|
+
const steps = {};
|
|
29
|
+
for (const step of FEATURE_STEPS) {
|
|
30
|
+
steps[step] = 'pending';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return yaml.stringify({
|
|
34
|
+
feature: name,
|
|
35
|
+
current_step: 'brief',
|
|
36
|
+
steps,
|
|
37
|
+
knowledge: ['backend'],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createFeature(name, root = process.cwd()) {
|
|
42
|
+
validateFeatureName(name);
|
|
43
|
+
|
|
44
|
+
const featureDir = path.join(root, AIA_DIR, 'features', name);
|
|
45
|
+
|
|
46
|
+
if (await fs.pathExists(featureDir)) {
|
|
47
|
+
throw new Error(`Feature "${name}" already exists.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await fs.ensureDir(featureDir);
|
|
51
|
+
|
|
52
|
+
for (const file of FEATURE_FILES) {
|
|
53
|
+
const filePath = path.join(featureDir, file);
|
|
54
|
+
const content = file === 'status.yaml' ? buildStatusYaml(name) : '';
|
|
55
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { resolveModelAlias } from '../providers/registry.js';
|
|
3
|
+
|
|
4
|
+
export async function callModel(model, prompt) {
|
|
5
|
+
const resolved = resolveModelAlias(model);
|
|
6
|
+
const displayName = resolved.model ?? `${model} (CLI default)`;
|
|
7
|
+
|
|
8
|
+
console.log(chalk.yellow(`[AI] Calling ${displayName}...`));
|
|
9
|
+
|
|
10
|
+
return resolved.provider.generate(prompt, resolved.model);
|
|
11
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { AIA_DIR, SCAN_IGNORE, SCAN_CATEGORIES } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
const CODE_EXTENSIONS = new Set([
|
|
6
|
+
'.js', '.mjs', '.cjs',
|
|
7
|
+
'.ts', '.mts', '.cts',
|
|
8
|
+
'.jsx', '.tsx',
|
|
9
|
+
'.json',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
async function walk(dir, root) {
|
|
13
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
14
|
+
const files = [];
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (SCAN_IGNORE.has(entry.name)) continue;
|
|
18
|
+
|
|
19
|
+
const fullPath = path.join(dir, entry.name);
|
|
20
|
+
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
files.push(...await walk(fullPath, root));
|
|
23
|
+
} else if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
24
|
+
files.push(path.relative(root, fullPath));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function categorize(files) {
|
|
32
|
+
const map = {};
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
for (const [category, pattern] of Object.entries(SCAN_CATEGORIES)) {
|
|
36
|
+
if (pattern.test(file)) {
|
|
37
|
+
(map[category] ??= []).push(file);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const key of Object.keys(map)) {
|
|
44
|
+
map[key].sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return map;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function scanRepo(root = process.cwd()) {
|
|
51
|
+
const files = (await walk(root, root)).sort();
|
|
52
|
+
const map = categorize(files);
|
|
53
|
+
|
|
54
|
+
const outputPath = path.join(root, AIA_DIR, 'repo-map.json');
|
|
55
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
56
|
+
await fs.writeJson(outputPath, map, { spaces: 2 });
|
|
57
|
+
|
|
58
|
+
return { map, total: files.length, outputPath };
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS, STEP_STATUS } from '../constants.js';
|
|
5
|
+
import { resolveModel } from '../models.js';
|
|
6
|
+
import { buildPrompt } from '../prompt-builder.js';
|
|
7
|
+
import { callModel } from './model-call.js';
|
|
8
|
+
import { loadStatus, updateStepStatus } from './status.js';
|
|
9
|
+
import { logExecution } from '../logger.js';
|
|
10
|
+
|
|
11
|
+
export async function runStep(step, feature, root = process.cwd()) {
|
|
12
|
+
if (!FEATURE_STEPS.includes(step)) {
|
|
13
|
+
throw new Error(`Unknown step "${step}". Valid steps: ${FEATURE_STEPS.join(', ')}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const status = await loadStatus(feature, root);
|
|
17
|
+
|
|
18
|
+
if (status.steps[step] === STEP_STATUS.DONE) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Step "${step}" already done for feature "${feature}". Use "aia reset ${step} ${feature}" to re-run.`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await updateStepStatus(feature, step, STEP_STATUS.IN_PROGRESS, root);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const model = await resolveModel(step, root);
|
|
28
|
+
const prompt = await buildPrompt(feature, step, root);
|
|
29
|
+
|
|
30
|
+
const start = performance.now();
|
|
31
|
+
const output = await callModel(model, prompt);
|
|
32
|
+
const duration = performance.now() - start;
|
|
33
|
+
|
|
34
|
+
const outputPath = path.join(root, AIA_DIR, 'features', feature, `${step}.md`);
|
|
35
|
+
await fs.writeFile(outputPath, output, 'utf-8');
|
|
36
|
+
|
|
37
|
+
await updateStepStatus(feature, step, STEP_STATUS.DONE, root);
|
|
38
|
+
await logExecution({ feature, step, model, duration }, root);
|
|
39
|
+
|
|
40
|
+
console.log(chalk.green(`Step "${step}" completed for feature "${feature}".`));
|
|
41
|
+
return output;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
await updateStepStatus(feature, step, STEP_STATUS.ERROR, root);
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { AIA_DIR, AIA_FOLDERS } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
export async function createAiaStructure(root = process.cwd()) {
|
|
6
|
+
for (const folder of AIA_FOLDERS) {
|
|
7
|
+
await fs.ensureDir(path.join(root, AIA_DIR, folder));
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS, STEP_STATUS } from '../constants.js';
|
|
5
|
+
|
|
6
|
+
function statusPath(feature, root) {
|
|
7
|
+
return path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function validateStatus(status, feature) {
|
|
11
|
+
if (!status || typeof status !== 'object') {
|
|
12
|
+
throw new Error(`Corrupted status.yaml for feature "${feature}".`);
|
|
13
|
+
}
|
|
14
|
+
if (!status.steps || typeof status.steps !== 'object') {
|
|
15
|
+
throw new Error(`Missing "steps" in status.yaml for feature "${feature}".`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function loadStatus(feature, root = process.cwd()) {
|
|
20
|
+
const filePath = statusPath(feature, root);
|
|
21
|
+
|
|
22
|
+
if (!(await fs.pathExists(filePath))) {
|
|
23
|
+
throw new Error(`Feature "${feature}" not found.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
27
|
+
const status = yaml.parse(raw);
|
|
28
|
+
|
|
29
|
+
validateStatus(status, feature);
|
|
30
|
+
|
|
31
|
+
return status;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function updateStepStatus(feature, step, value, root = process.cwd()) {
|
|
35
|
+
const status = await loadStatus(feature, root);
|
|
36
|
+
|
|
37
|
+
status.steps[step] = value;
|
|
38
|
+
|
|
39
|
+
const stepIndex = FEATURE_STEPS.indexOf(step);
|
|
40
|
+
const nextStep = FEATURE_STEPS[stepIndex + 1] ?? null;
|
|
41
|
+
if (value === STEP_STATUS.DONE && nextStep) {
|
|
42
|
+
status.current_step = nextStep;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const content = yaml.stringify(status);
|
|
46
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function resetStep(feature, step, root = process.cwd()) {
|
|
50
|
+
if (!FEATURE_STEPS.includes(step)) {
|
|
51
|
+
throw new Error(`Unknown step "${step}". Valid steps: ${FEATURE_STEPS.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const status = await loadStatus(feature, root);
|
|
55
|
+
|
|
56
|
+
status.steps[step] = STEP_STATUS.PENDING;
|
|
57
|
+
|
|
58
|
+
const firstPending = FEATURE_STEPS.find((s) => status.steps[s] !== STEP_STATUS.DONE);
|
|
59
|
+
if (firstPending) {
|
|
60
|
+
status.current_step = firstPending;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const content = yaml.stringify(status);
|
|
64
|
+
await fs.writeFile(statusPath(feature, root), content, 'utf-8');
|
|
65
|
+
|
|
66
|
+
const outputPath = path.join(root, AIA_DIR, 'features', feature, `${step}.md`);
|
|
67
|
+
if (await fs.pathExists(outputPath)) {
|
|
68
|
+
await fs.writeFile(outputPath, '', 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
}
|