@bamptee/aia-code 0.2.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamptee/aia-code",
3
- "version": "0.2.0",
3
+ "version": "0.4.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
@@ -5,6 +5,7 @@ import { registerRunCommand } from './commands/run.js';
5
5
  import { registerRepoCommand } from './commands/repo.js';
6
6
  import { registerStatusCommand } from './commands/status.js';
7
7
  import { registerResetCommand } from './commands/reset.js';
8
+ import { registerNextCommand } from './commands/next.js';
8
9
 
9
10
  export function createCli() {
10
11
  const program = new Command();
@@ -17,6 +18,7 @@ export function createCli() {
17
18
  registerInitCommand(program);
18
19
  registerFeatureCommand(program);
19
20
  registerRunCommand(program);
21
+ registerNextCommand(program);
20
22
  registerRepoCommand(program);
21
23
  registerStatusCommand(program);
22
24
  registerResetCommand(program);
@@ -0,0 +1,28 @@
1
+ import chalk from 'chalk';
2
+ import { loadStatus } from '../services/status.js';
3
+ import { runStep } from '../services/runner.js';
4
+
5
+ export function registerNextCommand(program) {
6
+ program
7
+ .command('next <feature> [description]')
8
+ .description('Run the next pending step for a feature')
9
+ .option('-v, --verbose', 'Show CLI logs (thinking, tool use, etc.)')
10
+ .option('-a, --apply', 'Let the AI edit and create files in the project')
11
+ .action(async (feature, description, opts) => {
12
+ try {
13
+ const status = await loadStatus(feature);
14
+ const nextStep = status.current_step;
15
+
16
+ if (!nextStep) {
17
+ console.log(chalk.green(`All steps completed for feature "${feature}".`));
18
+ return;
19
+ }
20
+
21
+ console.log(chalk.cyan(`[next] Running step: ${nextStep}`));
22
+ await runStep(nextStep, feature, { description, verbose: opts.verbose, apply: opts.apply });
23
+ } catch (err) {
24
+ console.error(chalk.red(err.message));
25
+ process.exit(1);
26
+ }
27
+ });
28
+ }
@@ -3,11 +3,13 @@ import { runStep } from '../services/runner.js';
3
3
 
4
4
  export function registerRunCommand(program) {
5
5
  program
6
- .command('run <step> <feature>')
6
+ .command('run <step> <feature> [description]')
7
7
  .description('Execute a step for a feature using the configured AI model')
8
- .action(async (step, feature) => {
8
+ .option('-v, --verbose', 'Show CLI logs (thinking, tool use, etc.)')
9
+ .option('-a, --apply', 'Let the AI edit and create files in the project')
10
+ .action(async (step, feature, description, opts) => {
9
11
  try {
10
- await runStep(step, feature);
12
+ await runStep(step, feature, { description, verbose: opts.verbose, apply: opts.apply });
11
13
  } catch (err) {
12
14
  console.error(chalk.red(err.message));
13
15
  process.exit(1);
@@ -44,9 +44,14 @@ async function loadFeatureFiles(feature, step, root) {
44
44
  return sections.join('\n\n');
45
45
  }
46
46
 
47
+ async function loadPreviousOutput(feature, step, root) {
48
+ const filePath = path.join(root, AIA_DIR, 'features', feature, `${step}.md`);
49
+ return readIfExists(filePath);
50
+ }
51
+
47
52
  async function resolveKnowledgeCategories(feature, config, root) {
48
- const statusPath = path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
49
- const raw = await readIfExists(statusPath);
53
+ const statusFile = path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
54
+ const raw = await readIfExists(statusFile);
50
55
 
51
56
  if (raw) {
52
57
  const status = yaml.parse(raw);
@@ -67,13 +72,14 @@ async function loadPromptTemplate(step, root) {
67
72
  return content;
68
73
  }
69
74
 
70
- export async function buildPrompt(feature, step, root = process.cwd()) {
75
+ export async function buildPrompt(feature, step, { description, root = process.cwd() } = {}) {
71
76
  const config = await loadConfig(root);
72
77
 
73
- const [context, knowledgeCategories, featureContent, task] = await Promise.all([
78
+ const [context, knowledgeCategories, featureContent, previousOutput, task] = await Promise.all([
74
79
  loadContextFiles(config, root),
75
80
  resolveKnowledgeCategories(feature, config, root),
76
81
  loadFeatureFiles(feature, step, root),
82
+ loadPreviousOutput(feature, step, root),
77
83
  loadPromptTemplate(step, root),
78
84
  ]);
79
85
 
@@ -81,6 +87,12 @@ export async function buildPrompt(feature, step, root = process.cwd()) {
81
87
 
82
88
  const parts = [];
83
89
 
90
+ if (description) {
91
+ parts.push('=== DESCRIPTION ===\n');
92
+ parts.push(description);
93
+ parts.push('');
94
+ }
95
+
84
96
  parts.push('=== CONTEXT ===\n');
85
97
  parts.push(context || '(no context files)');
86
98
 
@@ -90,6 +102,12 @@ export async function buildPrompt(feature, step, root = process.cwd()) {
90
102
  parts.push('\n\n=== FEATURE ===\n');
91
103
  parts.push(featureContent || '(no prior steps)');
92
104
 
105
+ if (previousOutput) {
106
+ parts.push('\n\n=== PREVIOUS OUTPUT ===\n');
107
+ parts.push(previousOutput);
108
+ parts.push('\n\nThe above is a previous version of this step. Rewrite it incorporating any new information, answers to questions, and improvements.');
109
+ }
110
+
93
111
  parts.push('\n\n=== TASK ===\n');
94
112
  parts.push(task);
95
113
 
@@ -1,11 +1,17 @@
1
1
  import { runCli } from './cli-runner.js';
2
2
 
3
- export async function generate(prompt, model) {
3
+ export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
4
4
  const args = ['-p'];
5
5
  if (model) {
6
6
  args.push('--model', model);
7
7
  }
8
+ if (apply) {
9
+ args.push('--allowedTools', 'Edit', 'Write', 'Bash', 'Read', 'Glob', 'Grep');
10
+ }
11
+ if (verbose) {
12
+ args.push('--verbose');
13
+ }
8
14
  args.push('-');
9
15
 
10
- return runCli('claude', args, { stdin: prompt });
16
+ return runCli('claude', args, { stdin: prompt, verbose });
11
17
  }
@@ -1,8 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import chalk from 'chalk';
2
3
 
3
- const DEFAULT_TIMEOUT_MS = 300_000;
4
+ const DEFAULT_IDLE_TIMEOUT_MS = 180_000;
4
5
 
5
- export function runCli(command, args, { stdin: stdinData, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
6
+ export function runCli(command, args, { stdin: stdinData, verbose = false, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS } = {}) {
6
7
  return new Promise((resolve, reject) => {
7
8
  const child = spawn(command, args, {
8
9
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -11,37 +12,56 @@ export function runCli(command, args, { stdin: stdinData, timeoutMs = DEFAULT_TI
11
12
 
12
13
  const chunks = [];
13
14
  let stderr = '';
15
+ let settled = false;
16
+
17
+ function resetTimer() {
18
+ clearTimeout(timer);
19
+ timer = setTimeout(() => {
20
+ child.kill('SIGTERM');
21
+ finish(new Error(`CLI idle timeout (no output for ${idleTimeoutMs / 1000}s): ${command} ${args.join(' ')}`));
22
+ }, idleTimeoutMs);
23
+ }
24
+
25
+ function finish(err, result) {
26
+ if (settled) return;
27
+ settled = true;
28
+ clearTimeout(timer);
29
+ if (err) reject(err);
30
+ else resolve(result);
31
+ }
32
+
33
+ let timer;
34
+ resetTimer();
14
35
 
15
36
  child.stdout.on('data', (data) => {
16
37
  const text = data.toString();
17
38
  process.stdout.write(text);
18
39
  chunks.push(text);
40
+ resetTimer();
19
41
  });
20
42
 
21
43
  child.stderr.on('data', (data) => {
22
- stderr += data.toString();
44
+ const text = data.toString();
45
+ stderr += text;
46
+ if (verbose) {
47
+ process.stderr.write(chalk.gray(text));
48
+ }
49
+ resetTimer();
23
50
  });
24
51
 
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
52
  child.on('error', (err) => {
31
- clearTimeout(timer);
32
53
  if (err.code === 'ENOENT') {
33
- reject(new Error(`CLI not found: "${command}". Make sure it is installed and in your PATH.`));
54
+ finish(new Error(`CLI not found: "${command}". Make sure it is installed and in your PATH.`));
34
55
  } else {
35
- reject(err);
56
+ finish(err);
36
57
  }
37
58
  });
38
59
 
39
60
  child.on('close', (code) => {
40
- clearTimeout(timer);
41
61
  if (code !== 0) {
42
- reject(new Error(`${command} exited with code ${code}:\n${stderr.trim()}`));
62
+ finish(new Error(`${command} exited with code ${code}:\n${stderr.trim()}`));
43
63
  } else {
44
- resolve(chunks.join(''));
64
+ finish(null, chunks.join(''));
45
65
  }
46
66
  });
47
67
 
@@ -1,11 +1,14 @@
1
1
  import { runCli } from './cli-runner.js';
2
2
 
3
- export async function generate(prompt, model) {
3
+ export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
4
4
  const args = [];
5
5
  if (model) {
6
6
  args.push('-m', model);
7
7
  }
8
+ if (apply) {
9
+ args.push('--sandbox', 'false');
10
+ }
8
11
  args.push('-');
9
12
 
10
- return runCli('gemini', args, { stdin: prompt });
13
+ return runCli('gemini', args, { stdin: prompt, verbose });
11
14
  }
@@ -1,11 +1,14 @@
1
1
  import { runCli } from './cli-runner.js';
2
2
 
3
- export async function generate(prompt, model) {
3
+ export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
4
4
  const args = ['exec'];
5
5
  if (model) {
6
6
  args.push('-c', `model="${model}"`);
7
7
  }
8
+ if (apply) {
9
+ args.push('-c', 'approval_policy="auto-edit"');
10
+ }
8
11
  args.push('-');
9
12
 
10
- return runCli('codex', args, { stdin: prompt });
13
+ return runCli('codex', args, { stdin: prompt, verbose });
11
14
  }
@@ -1,11 +1,12 @@
1
1
  import chalk from 'chalk';
2
2
  import { resolveModelAlias } from '../providers/registry.js';
3
3
 
4
- export async function callModel(model, prompt) {
4
+ export async function callModel(model, prompt, { verbose = false, apply = false } = {}) {
5
5
  const resolved = resolveModelAlias(model);
6
6
  const displayName = resolved.model ?? `${model} (CLI default)`;
7
+ const mode = apply ? 'agent' : 'print';
7
8
 
8
- console.log(chalk.yellow(`[AI] Calling ${displayName}...`));
9
+ console.log(chalk.yellow(`[AI] Calling ${displayName} (${mode} mode)...`));
9
10
 
10
- return resolved.provider.generate(prompt, resolved.model);
11
+ return resolved.provider.generate(prompt, resolved.model, { verbose, apply });
11
12
  }
@@ -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, root = process.cwd()) {
11
+ export async function runStep(step, feature, { description, verbose = false, apply = false, root = process.cwd() } = {}) {
12
12
  if (!FEATURE_STEPS.includes(step)) {
13
13
  throw new Error(`Unknown step "${step}". Valid steps: ${FEATURE_STEPS.join(', ')}`);
14
14
  }
@@ -25,10 +25,10 @@ export async function runStep(step, feature, root = process.cwd()) {
25
25
 
26
26
  try {
27
27
  const model = await resolveModel(step, root);
28
- const prompt = await buildPrompt(feature, step, root);
28
+ const prompt = await buildPrompt(feature, step, { description, root });
29
29
 
30
30
  const start = performance.now();
31
- const output = await callModel(model, prompt);
31
+ const output = await callModel(model, prompt, { verbose, apply });
32
32
  const duration = performance.now() - start;
33
33
 
34
34
  const outputPath = path.join(root, AIA_DIR, 'features', feature, `${step}.md`);
@@ -38,7 +38,7 @@ export async function updateStepStatus(feature, step, value, root = process.cwd(
38
38
 
39
39
  const stepIndex = FEATURE_STEPS.indexOf(step);
40
40
  const nextStep = FEATURE_STEPS[stepIndex + 1] ?? null;
41
- if (value === STEP_STATUS.DONE && nextStep) {
41
+ if (value === STEP_STATUS.DONE) {
42
42
  status.current_step = nextStep;
43
43
  }
44
44
 
@@ -63,8 +63,5 @@ export async function resetStep(feature, step, root = process.cwd()) {
63
63
  const content = yaml.stringify(status);
64
64
  await fs.writeFile(statusPath(feature, root), content, 'utf-8');
65
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
- }
66
+ // Keep the existing output it will be fed back as context on re-run
70
67
  }