@bamptee/aia-code 1.0.4 → 2.0.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 CHANGED
@@ -35,10 +35,12 @@ Each CLI manages its own authentication. Run `claude`, `codex`, or `gemini` once
35
35
  | `aia next <feature> [description]` | Run the next pending step automatically |
36
36
  | `aia status <feature>` | Show the current status of a feature |
37
37
  | `aia reset <step> <feature>` | Reset a step to pending so it can be re-run |
38
+ | `aia iterate <step> <feature> <instructions>` | Re-run a step with additional instructions to refine the output |
38
39
  | `aia quick <name> [description]` | Quick story/ticket: dev-plan → implement → review only |
39
40
  | `aia repo scan` | Scan codebase and generate `repo-map.json` |
41
+ | `aia ui` | Launch the local web UI to manage features and config |
40
42
 
41
- ### Options for `run`, `next`, and `quick`
43
+ ### Options for `run`, `next`, `quick`, and `iterate`
42
44
 
43
45
  | Flag | Description |
44
46
  |------|-------------|
@@ -281,6 +283,18 @@ aia reset tech-spec session-replay
281
283
  aia run tech-spec session-replay "Add WebSocket support and rate limiting"
282
284
  ```
283
285
 
286
+ #### Iterating on a step
287
+
288
+ Use `aia iterate` to refine a completed step with specific instructions. It resets the step, feeds back the previous output, and applies your instructions in a single command:
289
+
290
+ ```bash
291
+ aia iterate tech-spec session-replay "Add error handling for WebSocket disconnections"
292
+ aia iterate brief session-replay "Focus more on mobile use cases"
293
+ aia iterate dev-plan session-replay "Split the implementation into smaller PRs" -v
294
+ ```
295
+
296
+ You can iterate multiple times — each run builds on the previous output.
297
+
284
298
  #### Quick mode (stories & tickets)
285
299
 
286
300
  For small stories or tickets that don't need the full 8-step pipeline, use `aia quick`. It skips brief, ba-spec, questions, tech-spec, and challenge, and runs only **dev-plan → implement → review**:
@@ -419,6 +433,7 @@ src/
419
433
  feature.js # aia feature <name>
420
434
  run.js # aia run <step> <feature>
421
435
  next.js # aia next <feature>
436
+ iterate.js # aia iterate <step> <feature> <instructions>
422
437
  quick.js # aia quick <name> [description]
423
438
  status.js # aia status <feature>
424
439
  reset.js # aia reset <step> <feature>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamptee/aia-code",
3
- "version": "1.0.4",
3
+ "version": "2.0.0",
4
4
  "description": "AI Architecture Assistant - orchestrate AI-assisted development workflows via CLI tools (Claude, Codex, Gemini)",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.js CHANGED
@@ -1,4 +1,10 @@
1
1
  import { Command } from 'commander';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import path from 'node:path';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
2
8
  import { registerInitCommand } from './commands/init.js';
3
9
  import { registerFeatureCommand } from './commands/feature.js';
4
10
  import { registerRunCommand } from './commands/run.js';
@@ -7,6 +13,7 @@ import { registerStatusCommand } from './commands/status.js';
7
13
  import { registerResetCommand } from './commands/reset.js';
8
14
  import { registerNextCommand } from './commands/next.js';
9
15
  import { registerQuickCommand } from './commands/quick.js';
16
+ import { registerIterateCommand } from './commands/iterate.js';
10
17
  import { registerUiCommand } from './commands/ui.js';
11
18
 
12
19
  export function createCli() {
@@ -15,7 +22,7 @@ export function createCli() {
15
22
  program
16
23
  .name('aia')
17
24
  .description('AI Architecture Assistant')
18
- .version('0.1.0');
25
+ .version(pkg.version);
19
26
 
20
27
  registerInitCommand(program);
21
28
  registerFeatureCommand(program);
@@ -25,6 +32,7 @@ export function createCli() {
25
32
  registerRepoCommand(program);
26
33
  registerStatusCommand(program);
27
34
  registerResetCommand(program);
35
+ registerIterateCommand(program);
28
36
  registerUiCommand(program);
29
37
 
30
38
  return program;
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk';
2
+ import { resetStep } from '../services/status.js';
3
+ import { runStep } from '../services/runner.js';
4
+
5
+ export function registerIterateCommand(program) {
6
+ program
7
+ .command('iterate <step> <feature> <instructions>')
8
+ .description('Re-run a step with additional instructions to refine the output')
9
+ .option('-v, --verbose', 'Show AI thinking/tool usage')
10
+ .option('-a, --apply', 'Force agent mode (file editing)')
11
+ .action(async (step, feature, instructions, opts) => {
12
+ try {
13
+ await resetStep(feature, step);
14
+ console.log(chalk.gray(`Reset "${step}" — iterating with new instructions...`));
15
+
16
+ await runStep(step, feature, {
17
+ instructions,
18
+ verbose: opts.verbose,
19
+ apply: opts.apply,
20
+ });
21
+ } catch (err) {
22
+ console.error(chalk.red(err.message));
23
+ process.exit(1);
24
+ }
25
+ });
26
+ }
@@ -77,7 +77,7 @@ async function loadPromptTemplate(step, root) {
77
77
  return content;
78
78
  }
79
79
 
80
- export async function buildPrompt(feature, step, { description, root = process.cwd() } = {}) {
80
+ export async function buildPrompt(feature, step, { description, instructions, root = process.cwd() } = {}) {
81
81
  const config = await loadConfig(root);
82
82
 
83
83
  const [context, knowledgeCategories, initSpecs, featureContent, previousOutput, task] = await Promise.all([
@@ -127,6 +127,12 @@ export async function buildPrompt(feature, step, { description, root = process.c
127
127
  parts.push('\n\nThe above is a previous version of this step. Rewrite it incorporating any new information, answers to questions, and improvements.');
128
128
  }
129
129
 
130
+ if (instructions) {
131
+ parts.push('\n\n=== ITERATION INSTRUCTIONS ===\n');
132
+ parts.push('Apply the following changes/feedback to the previous output:\n');
133
+ parts.push(instructions);
134
+ }
135
+
130
136
  parts.push('\n\n=== TASK ===\n');
131
137
  parts.push(task);
132
138
 
@@ -8,7 +8,7 @@ import { callModel } from './model-call.js';
8
8
  import { loadStatus, updateStepStatus } from './status.js';
9
9
  import { logExecution } from '../logger.js';
10
10
 
11
- export async function runStep(step, feature, { description, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
11
+ export async function runStep(step, feature, { description, instructions, model: modelOverride, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
12
12
  if (!FEATURE_STEPS.includes(step)) {
13
13
  throw new Error(`Unknown step "${step}". Valid steps: ${FEATURE_STEPS.join(', ')}`);
14
14
  }
@@ -26,8 +26,8 @@ export async function runStep(step, feature, { description, verbose = false, app
26
26
  const shouldApply = apply || APPLY_STEPS.has(step);
27
27
 
28
28
  try {
29
- const model = await resolveModel(step, root);
30
- const prompt = await buildPrompt(feature, step, { description, root });
29
+ const model = modelOverride || await resolveModel(step, root);
30
+ const prompt = await buildPrompt(feature, step, { description, instructions, root });
31
31
 
32
32
  const start = performance.now();
33
33
  const output = await callModel(model, prompt, { verbose, apply: shouldApply, onData });
@@ -1,8 +1,19 @@
1
1
  import { FEATURE_STEPS, STEP_STATUS, QUICK_STEPS } from '../../constants.js';
2
+ import { loadConfig } from '../../models.js';
2
3
  import { json } from '../router.js';
3
4
 
4
5
  export function registerConstantsRoutes(router) {
5
6
  router.get('/api/constants', (req, res) => {
6
7
  json(res, { FEATURE_STEPS, STEP_STATUS, QUICK_STEPS });
7
8
  });
9
+
10
+ // List available models per step from config
11
+ router.get('/api/models', async (req, res, { root }) => {
12
+ try {
13
+ const config = await loadConfig(root);
14
+ json(res, config.models || {});
15
+ } catch (err) {
16
+ json(res, {});
17
+ }
18
+ });
8
19
  }
@@ -97,6 +97,7 @@ export function registerFeatureRoutes(router) {
97
97
  try {
98
98
  const output = await runStep(params.step, params.name, {
99
99
  description: body.description,
100
+ model: body.model || undefined,
100
101
  verbose: true,
101
102
  apply: body.apply || false,
102
103
  root,
@@ -157,6 +158,34 @@ export function registerFeatureRoutes(router) {
157
158
  res.end();
158
159
  });
159
160
 
161
+ // Iterate a step with SSE streaming (reset + re-run with instructions)
162
+ router.post('/api/features/:name/iterate/:step', async (req, res, { params, root, parseBody }) => {
163
+ const body = await parseBody();
164
+ sseHeaders(res);
165
+ sseSend(res, 'status', { step: params.step, status: 'iterating' });
166
+
167
+ try {
168
+ await resetStep(params.name, params.step, root);
169
+
170
+ const onData = ({ type, text }) => {
171
+ try { sseSend(res, 'log', { type, text }); } catch {}
172
+ };
173
+
174
+ const output = await runStep(params.step, params.name, {
175
+ instructions: body.instructions,
176
+ model: body.model || undefined,
177
+ verbose: true,
178
+ apply: body.apply || false,
179
+ root,
180
+ onData,
181
+ });
182
+ sseSend(res, 'done', { step: params.step });
183
+ } catch (err) {
184
+ sseSend(res, 'error', { message: err.message });
185
+ }
186
+ res.end();
187
+ });
188
+
160
189
  // Reset a step
161
190
  router.post('/api/features/:name/reset/:step', async (req, res, { params, root }) => {
162
191
  try {
@@ -138,75 +138,126 @@ function LogViewer({ logs }) {
138
138
  }, logs.join(''));
139
139
  }
140
140
 
141
- function RunPanel({ name, step, onDone }) {
141
+ function ModelSelect({ step, model, onChange, disabled }) {
142
+ const [models, setModels] = React.useState([]);
143
+
144
+ React.useEffect(() => {
145
+ api.get('/models').then(data => {
146
+ const stepModels = data[step] || [];
147
+ setModels(stepModels.map(m => m.model));
148
+ }).catch(() => {});
149
+ }, [step]);
150
+
151
+ if (models.length <= 1) return null;
152
+
153
+ return React.createElement('select', {
154
+ value: model,
155
+ onChange: e => onChange(e.target.value),
156
+ disabled,
157
+ className: 'bg-aia-card border border-aia-border rounded px-2 py-1 text-xs text-slate-300 focus:border-aia-accent focus:outline-none',
158
+ },
159
+ React.createElement('option', { value: '' }, 'Model: auto (weighted)'),
160
+ ...models.map(m => React.createElement('option', { key: m, value: m }, m)),
161
+ );
162
+ }
163
+
164
+ function RunPanel({ name, step, stepStatus, onDone }) {
165
+ const isDone = stepStatus === 'done';
142
166
  const [description, setDescription] = React.useState('');
167
+ const [instructions, setInstructions] = React.useState('');
168
+ const [model, setModel] = React.useState('');
143
169
  const [apply, setApply] = React.useState(false);
144
170
  const [running, setRunning] = React.useState(false);
145
171
  const [result, setResult] = React.useState(null);
146
172
  const [err, setErr] = React.useState(null);
147
173
  const [logs, setLogs] = React.useState([]);
148
174
 
149
- async function run() {
150
- setRunning(true);
151
- setResult(null);
152
- setErr(null);
153
- setLogs([]);
175
+ const sseCallbacks = {
176
+ onLog: (text) => setLogs(prev => [...prev, text]),
177
+ onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.step || ''}\n`]),
178
+ };
154
179
 
155
- const res = await streamPost(`/features/${name}/run/${step}`, { description, apply }, {
156
- onLog: (text) => setLogs(prev => [...prev, text]),
157
- onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.step || ''}\n`]),
158
- });
180
+ async function run() {
181
+ setRunning(true); setResult(null); setErr(null); setLogs([]);
182
+ const res = await streamPost(`/features/${name}/run/${step}`, { description, apply, model: model || undefined }, sseCallbacks);
183
+ if (res.ok) { setResult('Step completed.'); if (onDone) onDone(); }
184
+ else setErr(res.error);
185
+ setRunning(false);
186
+ }
159
187
 
160
- if (res.ok) {
161
- setResult('Step completed.');
162
- if (onDone) onDone();
163
- } else {
164
- setErr(res.error);
165
- }
188
+ async function iterate() {
189
+ setRunning(true); setResult(null); setErr(null); setLogs([]);
190
+ const res = await streamPost(`/features/${name}/iterate/${step}`, { instructions, apply, model: model || undefined }, sseCallbacks);
191
+ if (res.ok) { setResult('Iteration completed.'); setInstructions(''); if (onDone) onDone(); }
192
+ else setErr(res.error);
166
193
  setRunning(false);
167
194
  }
168
195
 
169
196
  async function reset() {
170
- try {
171
- await api.post(`/features/${name}/reset/${step}`);
172
- if (onDone) onDone();
173
- } catch (e) {
174
- setErr(e.message);
175
- }
197
+ try { await api.post(`/features/${name}/reset/${step}`); if (onDone) onDone(); }
198
+ catch (e) { setErr(e.message); }
176
199
  }
177
200
 
178
- return React.createElement('div', { className: 'bg-slate-900 border border-aia-border rounded p-4 space-y-3' },
179
- React.createElement('h4', { className: 'text-sm font-semibold text-slate-300' }, `Run: ${step}`),
180
- React.createElement('input', {
181
- type: 'text',
182
- value: description,
183
- onChange: e => setDescription(e.target.value),
184
- placeholder: 'Optional description...',
185
- disabled: running,
186
- className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-aia-accent focus:outline-none',
187
- }),
188
- React.createElement('div', { className: 'flex items-center gap-4' },
189
- React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
190
- React.createElement('input', {
191
- type: 'checkbox',
192
- checked: apply,
193
- onChange: e => setApply(e.target.checked),
194
- disabled: running,
195
- className: 'rounded',
196
- }),
197
- 'Agent mode (--apply)'
198
- ),
199
- React.createElement('button', {
200
- onClick: run,
201
+ return React.createElement('div', { className: 'space-y-3' },
202
+
203
+ // --- Run block (when step is not done) ---
204
+ !isDone && React.createElement('div', { className: 'bg-slate-900 border border-aia-border rounded p-4 space-y-3' },
205
+ React.createElement('h4', { className: 'text-sm font-semibold text-emerald-400' }, `Run: ${step}`),
206
+ React.createElement('input', {
207
+ type: 'text',
208
+ value: description,
209
+ onChange: e => setDescription(e.target.value),
210
+ placeholder: 'Optional description...',
201
211
  disabled: running,
202
- className: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded px-4 py-1.5 text-sm hover:bg-emerald-500/30 disabled:opacity-40',
203
- }, running ? 'Running...' : 'Run Step'),
204
- React.createElement('button', {
205
- onClick: reset,
212
+ className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-emerald-400 focus:outline-none',
213
+ }),
214
+ React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
215
+ React.createElement(ModelSelect, { step, model, onChange: setModel, disabled: running }),
216
+ React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
217
+ React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
218
+ 'Agent mode (--apply)'
219
+ ),
220
+ React.createElement('button', {
221
+ onClick: run, disabled: running,
222
+ className: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded px-4 py-1.5 text-sm hover:bg-emerald-500/30 disabled:opacity-40',
223
+ }, running ? 'Running...' : 'Run Step'),
224
+ ),
225
+ ),
226
+
227
+ // --- Iterate block (when step is done) ---
228
+ isDone && React.createElement('div', { className: 'bg-slate-900 border border-violet-500/30 rounded p-4 space-y-3' },
229
+ React.createElement('div', { className: 'flex items-center justify-between' },
230
+ React.createElement('h4', { className: 'text-sm font-semibold text-violet-400' }, `Iterate: ${step}`),
231
+ React.createElement('span', { className: 'text-xs bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded' }, 'done'),
232
+ ),
233
+ React.createElement('p', { className: 'text-xs text-slate-500' }, 'The previous output will be used as base. Describe what to change below.'),
234
+ React.createElement('label', { className: 'text-xs font-medium text-slate-400 block' }, 'Iteration instructions'),
235
+ React.createElement('textarea', {
236
+ value: instructions,
237
+ onChange: e => setInstructions(e.target.value),
238
+ placeholder: 'e.g. "Add error handling for edge cases", "Focus more on mobile", "Split into smaller functions"...',
206
239
  disabled: running,
207
- className: 'text-slate-500 hover:text-slate-300 text-xs',
208
- }, 'Reset'),
240
+ rows: 3,
241
+ className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:border-violet-400 focus:outline-none resize-y',
242
+ }),
243
+ React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
244
+ React.createElement(ModelSelect, { step, model, onChange: setModel, disabled: running }),
245
+ React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
246
+ React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
247
+ 'Agent mode (--apply)'
248
+ ),
249
+ React.createElement('button', {
250
+ onClick: iterate, disabled: running || !instructions.trim(),
251
+ className: 'bg-violet-500/20 text-violet-400 border border-violet-500/30 rounded px-4 py-1.5 text-sm hover:bg-violet-500/30 disabled:opacity-40',
252
+ }, running ? 'Iterating...' : 'Iterate'),
253
+ React.createElement('button', {
254
+ onClick: reset, disabled: running,
255
+ className: 'text-slate-500 hover:text-slate-300 text-xs',
256
+ }, 'Reset to pending'),
257
+ ),
209
258
  ),
259
+
260
+ // --- Shared log viewer + results ---
210
261
  React.createElement(LogViewer, { logs }),
211
262
  result && React.createElement('p', { className: 'text-emerald-400 text-xs' }, result),
212
263
  err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
@@ -258,8 +309,8 @@ export function FeatureDetail({ name }) {
258
309
  )
259
310
  ),
260
311
 
261
- // Run panel
262
- activeStep && React.createElement(RunPanel, { name, step: activeStep, onDone: load }),
312
+ // Run / Iterate panel
313
+ activeStep && React.createElement(RunPanel, { name, step: activeStep, stepStatus: steps[activeStep], onDone: load }),
263
314
 
264
315
  // File tabs
265
316
  React.createElement('div', { className: 'flex gap-1 border-b border-aia-border' },