@bamptee/aia-code 2.0.1 → 2.0.3

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
@@ -162,7 +162,58 @@ Required templates (one per step you want to run):
162
162
  .aia/prompts/review.md
163
163
  ```
164
164
 
165
- ### 6. Configure models
165
+ ### 6. Configuration (user + project)
166
+
167
+ AIA uses two configuration files:
168
+
169
+ | File | Scope | Content |
170
+ |------|-------|---------|
171
+ | `~/.aia/config.yaml` | **User (global)** | user_name, communication_language |
172
+ | `.aia/config.yaml` | **Project** | projectName, document_output_language, models, knowledge_default, context_files |
173
+
174
+ When you run AIA, both configs are merged (user preferences + project config).
175
+
176
+ #### User config (`~/.aia/config.yaml`)
177
+
178
+ Your personal preferences, created automatically on first use:
179
+
180
+ ```yaml
181
+ # ~/.aia/config.yaml
182
+ user_name: John Doe
183
+ communication_language: French
184
+ ```
185
+
186
+ - **user_name**: Your name (shown to the AI for context)
187
+ - **communication_language**: Language for AI responses and questions
188
+
189
+ These are stored outside the project, so they're never committed to git.
190
+
191
+ #### Project config (`.aia/config.yaml`)
192
+
193
+ Shared project settings:
194
+
195
+ ```yaml
196
+ # .aia/config.yaml
197
+ projectName: My Project
198
+ document_output_language: English
199
+ models:
200
+ # ...
201
+ ```
202
+
203
+ - **document_output_language**: Language for generated documents (specs, plans, etc.) - shared by the whole team
204
+
205
+ #### .gitignore recommendation
206
+
207
+ User preferences are stored in `~/.aia/config.yaml` (outside the project), so nothing extra is needed in `.gitignore`.
208
+
209
+ If you want to ignore local project overrides, add to your `.gitignore`:
210
+
211
+ ```gitignore
212
+ # AIA - ignore local overrides
213
+ .aia/local.yaml
214
+ ```
215
+
216
+ ### 7. Configure models (project config)
166
217
 
167
218
  In `config.yaml`, assign models to steps with probability weights:
168
219
 
@@ -210,7 +261,7 @@ Use aliases to delegate to the CLI's default model:
210
261
  | `gpt-*`, `o[0-9]*` | `codex exec` | `gpt-4.1`, `o3`, `o4-mini` |
211
262
  | `gemini-*` | `gemini` | `gemini-2.5-pro`, `gemini-2.5-flash` |
212
263
 
213
- ### 7. Run the feature pipeline
264
+ ### 8. Run the feature pipeline
214
265
 
215
266
  #### Step by step
216
267
 
@@ -315,7 +366,7 @@ The `init.md` file serves as the sole input context for the dev-plan step. Verbo
315
366
  aia quick add-rate-limit "Add rate limiting to the /api/upload endpoint" -v
316
367
  ```
317
368
 
318
- ### 8. Print mode vs Agent mode
369
+ ### 9. Print mode vs Agent mode
319
370
 
320
371
  By default, AIA runs in **print mode** -- the AI generates text (specs, plans, reviews) saved to `.md` files.
321
372
 
@@ -341,7 +392,7 @@ The `implement` step always runs in agent mode automatically.
341
392
 
342
393
  Idle timeout resets every time the CLI produces output, so long-running steps that stream continuously won't time out.
343
394
 
344
- ### 9. Scan your repo
395
+ ### 10. Scan your repo
345
396
 
346
397
  ```bash
347
398
  aia repo scan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamptee/aia-code",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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",
@@ -33,9 +33,15 @@
33
33
  "start": "node bin/aia.js"
34
34
  },
35
35
  "dependencies": {
36
+ "@iarna/toml": "^2.2.5",
37
+ "busboy": "^1.6.0",
36
38
  "chalk": "^5.3.0",
37
39
  "commander": "^12.1.0",
38
40
  "fs-extra": "^11.2.0",
41
+ "node-pty": "^1.0.0",
42
+ "ws": "^8.18.0",
43
+ "xterm": "^5.3.0",
44
+ "xterm-addon-fit": "^0.8.0",
39
45
  "yaml": "^2.7.0"
40
46
  }
41
47
  }
@@ -1,8 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import { createAiaStructure } from '../services/scaffold.js';
3
- import { writeDefaultConfig } from '../services/config.js';
3
+ import { writeDefaultConfig, isGlobalConfigured, updateGlobalConfig } from '../services/config.js';
4
4
  import { writeDefaultPrompts } from '../services/prompts.js';
5
5
  import { AIA_DIR } from '../constants.js';
6
+ import { ask, askRequired, isInteractive } from '../utils/prompt.js';
6
7
 
7
8
  export function registerInitCommand(program) {
8
9
  program
@@ -10,6 +11,15 @@ export function registerInitCommand(program) {
10
11
  .description('Initialize .aia folder structure, default config, and prompt templates')
11
12
  .action(async () => {
12
13
  try {
14
+ // First-time user setup
15
+ if (!(await isGlobalConfigured()) && isInteractive()) {
16
+ console.log(chalk.cyan('First time setup - let\'s configure your preferences.\n'));
17
+ const name = await askRequired('Your name');
18
+ const lang = await ask('Communication language', 'English');
19
+ await updateGlobalConfig({ user_name: name, communication_language: lang });
20
+ console.log(chalk.green('\nUser preferences saved to ~/.aia/config.yaml\n'));
21
+ }
22
+
13
23
  await createAiaStructure();
14
24
  await writeDefaultConfig();
15
25
  await writeDefaultPrompts();
package/src/models.js CHANGED
@@ -3,6 +3,7 @@ import fs from 'fs-extra';
3
3
  import yaml from 'yaml';
4
4
  import chalk from 'chalk';
5
5
  import { AIA_DIR } from './constants.js';
6
+ import { loadGlobalConfig } from './services/config.js';
6
7
 
7
8
  export async function loadConfig(root = process.cwd()) {
8
9
  const configPath = path.join(root, AIA_DIR, 'config.yaml');
@@ -12,13 +13,29 @@ export async function loadConfig(root = process.cwd()) {
12
13
  }
13
14
 
14
15
  const raw = await fs.readFile(configPath, 'utf-8');
15
- const config = yaml.parse(raw);
16
+ const projectConfig = yaml.parse(raw);
16
17
 
17
- validateConfig(config, configPath);
18
+ validateConfig(projectConfig, configPath);
19
+
20
+ // Merge with global user config
21
+ const globalConfig = await loadGlobalConfig();
22
+ const config = { ...globalConfig, ...projectConfig };
18
23
 
19
24
  return config;
20
25
  }
21
26
 
27
+ // Load only project config (without global merge)
28
+ export async function loadProjectConfig(root = process.cwd()) {
29
+ const configPath = path.join(root, AIA_DIR, 'config.yaml');
30
+
31
+ if (!(await fs.pathExists(configPath))) {
32
+ throw new Error(`Config not found: ${configPath}`);
33
+ }
34
+
35
+ const raw = await fs.readFile(configPath, 'utf-8');
36
+ return yaml.parse(raw);
37
+ }
38
+
22
39
  function validateConfig(config, configPath) {
23
40
  if (!config || typeof config !== 'object') {
24
41
  throw new Error(`Invalid config: ${configPath} must be a YAML object.`);
@@ -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, instructions, root = process.cwd() } = {}) {
80
+ export async function buildPrompt(feature, step, { description, instructions, history, attachments, root = process.cwd() } = {}) {
81
81
  const config = await loadConfig(root);
82
82
 
83
83
  const [context, knowledgeCategories, initSpecs, featureContent, previousOutput, task] = await Promise.all([
@@ -95,12 +95,41 @@ export async function buildPrompt(feature, step, { description, instructions, ro
95
95
 
96
96
  parts.push('IMPORTANT: You are working on a feature development pipeline. Everything you need is provided below in this prompt. Do NOT attempt to read, search for, or reference any external files. Do NOT say files are missing. Work exclusively with the content given below.\n');
97
97
 
98
+ // Inject user preferences - document language is the ONLY thing that matters for output
99
+ const docLang = config.document_output_language || 'English';
100
+
101
+ parts.push('=== OUTPUT LANGUAGE ===\n');
102
+ parts.push(`Write ALL your output in ${docLang}. This is mandatory and non-negotiable.`);
103
+ parts.push(`Do NOT use any other language for the document content.\n`);
104
+
105
+ // Add conversation history if present (for multi-turn)
106
+ if (history && history.length > 0) {
107
+ parts.push('=== CONVERSATION HISTORY ===\n');
108
+ for (const msg of history) {
109
+ const prefix = msg.role === 'user' ? 'User' : 'Agent';
110
+ parts.push(`${prefix}: ${msg.content}`);
111
+ }
112
+ parts.push('');
113
+ }
114
+
98
115
  if (description) {
99
116
  parts.push('=== DESCRIPTION ===\n');
100
117
  parts.push(description);
101
118
  parts.push('');
102
119
  }
103
120
 
121
+ if (attachments && attachments.length > 0) {
122
+ parts.push('=== ATTACHMENTS ===\n');
123
+ parts.push('The user has attached the following files. Use the Read tool to view them:\n');
124
+ for (const a of attachments) {
125
+ // F11: Sanitize filename to prevent prompt injection
126
+ const safeFilename = String(a.filename || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 255);
127
+ const safePath = String(a.path || '').slice(0, 1000);
128
+ parts.push(`- ${safeFilename}: ${safePath}`);
129
+ }
130
+ parts.push('');
131
+ }
132
+
104
133
  if (context) {
105
134
  parts.push('=== CONTEXT ===\n');
106
135
  parts.push(context);
@@ -136,5 +165,8 @@ export async function buildPrompt(feature, step, { description, instructions, ro
136
165
  parts.push('\n\n=== TASK ===\n');
137
166
  parts.push(task);
138
167
 
168
+ // Add language reminder at the end (always, to reinforce)
169
+ parts.push(`\n\n---\nREMINDER: Your entire output MUST be written in ${docLang}.`);
170
+
139
171
  return parts.join('\n');
140
172
  }
@@ -6,7 +6,7 @@ export async function generate(prompt, model, { verbose = false, apply = false,
6
6
  args.push('--model', model);
7
7
  }
8
8
  if (apply) {
9
- args.push('--allowedTools', 'Edit', 'Write', 'Bash', 'Read', 'Glob', 'Grep');
9
+ args.push('--allowedTools', 'Edit,Write,Bash,Read,Glob,Grep');
10
10
  }
11
11
  if (verbose || apply) {
12
12
  args.push('--verbose');
@@ -9,9 +9,10 @@ export function runCli(command, args, { stdin: stdinData, verbose = false, apply
9
9
  idleTimeoutMs = apply ? AGENT_IDLE_TIMEOUT_MS : DEFAULT_IDLE_TIMEOUT_MS;
10
10
  }
11
11
  return new Promise((resolve, reject) => {
12
+ const { CLAUDECODE, ...cleanEnv } = process.env;
12
13
  const child = spawn(command, args, {
13
14
  stdio: ['pipe', 'pipe', 'pipe'],
14
- env: { ...process.env, FORCE_COLOR: '0' },
15
+ env: { ...cleanEnv, FORCE_COLOR: '0' },
15
16
  });
16
17
 
17
18
  const chunks = [];
@@ -1,10 +1,17 @@
1
1
  import path from 'node:path';
2
+ import os from 'node:os';
2
3
  import fs from 'fs-extra';
3
4
  import yaml from 'yaml';
4
5
  import { AIA_DIR } from '../constants.js';
5
6
 
6
- const DEFAULT_CONFIG = {
7
+ // Global user config directory
8
+ const GLOBAL_AIA_DIR = path.join(os.homedir(), '.aia');
9
+ const GLOBAL_CONFIG_PATH = path.join(GLOBAL_AIA_DIR, 'config.yaml');
10
+
11
+ // Default config for PROJECT (.aia/config.yaml)
12
+ const DEFAULT_PROJECT_CONFIG = {
7
13
  projectName: 'My Project',
14
+ document_output_language: 'English',
8
15
  models: {
9
16
  brief: [
10
17
  { model: 'claude-default', weight: 1 },
@@ -42,6 +49,12 @@ const DEFAULT_CONFIG = {
42
49
  ],
43
50
  };
44
51
 
52
+ // Default config for USER (~/.aia/config.yaml)
53
+ const DEFAULT_USER_CONFIG = {
54
+ user_name: '',
55
+ communication_language: 'English',
56
+ };
57
+
45
58
  export async function writeDefaultConfig(root = process.cwd()) {
46
59
  const configPath = path.join(root, AIA_DIR, 'config.yaml');
47
60
 
@@ -49,6 +62,44 @@ export async function writeDefaultConfig(root = process.cwd()) {
49
62
  return;
50
63
  }
51
64
 
52
- const content = yaml.stringify(DEFAULT_CONFIG);
65
+ const content = yaml.stringify(DEFAULT_PROJECT_CONFIG);
53
66
  await fs.writeFile(configPath, content, 'utf-8');
54
67
  }
68
+
69
+ export async function ensureGlobalConfig() {
70
+ await fs.ensureDir(GLOBAL_AIA_DIR);
71
+
72
+ if (!(await fs.pathExists(GLOBAL_CONFIG_PATH))) {
73
+ const content = yaml.stringify(DEFAULT_USER_CONFIG);
74
+ await fs.writeFile(GLOBAL_CONFIG_PATH, content, 'utf-8');
75
+ }
76
+ }
77
+
78
+ export async function loadGlobalConfig() {
79
+ await ensureGlobalConfig();
80
+
81
+ const raw = await fs.readFile(GLOBAL_CONFIG_PATH, 'utf-8');
82
+ return yaml.parse(raw) || {};
83
+ }
84
+
85
+ export async function saveGlobalConfig(config) {
86
+ await fs.ensureDir(GLOBAL_AIA_DIR);
87
+ const content = yaml.stringify(config);
88
+ await fs.writeFile(GLOBAL_CONFIG_PATH, content, 'utf-8');
89
+ }
90
+
91
+ export async function isGlobalConfigured() {
92
+ const config = await loadGlobalConfig();
93
+ return config && config.user_name && config.user_name.trim() !== '';
94
+ }
95
+
96
+ export async function updateGlobalConfig(updates) {
97
+ const existing = await loadGlobalConfig();
98
+ const merged = { ...existing, ...updates };
99
+ await saveGlobalConfig(merged);
100
+ return merged;
101
+ }
102
+
103
+ export function getGlobalConfigPath() {
104
+ return GLOBAL_CONFIG_PATH;
105
+ }
@@ -0,0 +1,32 @@
1
+ const COMPLEXITY_INDICATORS = {
2
+ simple: ['fix', 'bug', 'typo', 'update', 'small', 'quick', 'minor', 'patch', 'hotfix', 'tweak'],
3
+ complex: ['refactor', 'architecture', 'migration', 'redesign', 'integration', 'new feature', 'complex', 'rewrite', 'overhaul', 'system']
4
+ };
5
+
6
+ export function suggestFlowType(description) {
7
+ if (!description || description.length < 50) {
8
+ return 'quick';
9
+ }
10
+
11
+ const lower = description.toLowerCase();
12
+ let simpleScore = 0;
13
+ let complexScore = 0;
14
+
15
+ COMPLEXITY_INDICATORS.simple.forEach(word => {
16
+ if (lower.includes(word)) simpleScore++;
17
+ });
18
+
19
+ COMPLEXITY_INDICATORS.complex.forEach(word => {
20
+ if (lower.includes(word)) complexScore++;
21
+ });
22
+
23
+ if (complexScore > simpleScore) {
24
+ return 'full';
25
+ }
26
+
27
+ if (simpleScore > 0 || description.length < 200) {
28
+ return 'quick';
29
+ }
30
+
31
+ return 'full';
32
+ }
@@ -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, instructions, model: modelOverride, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
11
+ export async function runStep(step, feature, { description, instructions, history, attachments, 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
  }
@@ -27,7 +27,7 @@ export async function runStep(step, feature, { description, instructions, model:
27
27
 
28
28
  try {
29
29
  const model = modelOverride || await resolveModel(step, root);
30
- const prompt = await buildPrompt(feature, step, { description, instructions, root });
30
+ const prompt = await buildPrompt(feature, step, { description, instructions, history, attachments, root });
31
31
 
32
32
  const start = performance.now();
33
33
  const output = await callModel(model, prompt, { verbose, apply: shouldApply, onData });
@@ -46,6 +46,20 @@ export async function updateStepStatus(feature, step, value, root = process.cwd(
46
46
  await fs.writeFile(statusPath(feature, root), content, 'utf-8');
47
47
  }
48
48
 
49
+ export async function updateFlowType(feature, flowType, root = process.cwd()) {
50
+ const status = await loadStatus(feature, root);
51
+ status.flow = flowType; // 'quick' or 'full'
52
+
53
+ // Set current_step based on flow type if not already started
54
+ const allPending = Object.values(status.steps).every(s => s === STEP_STATUS.PENDING);
55
+ if (allPending) {
56
+ status.current_step = flowType === 'quick' ? 'dev-plan' : 'brief';
57
+ }
58
+
59
+ const content = yaml.stringify(status);
60
+ await fs.writeFile(statusPath(feature, root), content, 'utf-8');
61
+ }
62
+
49
63
  export async function resetStep(feature, step, root = process.cwd()) {
50
64
  if (!FEATURE_STEPS.includes(step)) {
51
65
  throw new Error(`Unknown step "${step}". Valid steps: ${FEATURE_STEPS.join(', ')}`);
@@ -0,0 +1,166 @@
1
+ const STEP_GUIDANCE = {
2
+ en: {
3
+ brief: {
4
+ summary: 'Product brief completed',
5
+ next: 'ba-spec',
6
+ actions: [
7
+ 'Review the brief for completeness',
8
+ 'Run: aia next <feature>',
9
+ ],
10
+ tips: ['If too vague, iterate with more context'],
11
+ },
12
+ 'ba-spec': {
13
+ summary: 'Business analysis completed',
14
+ next: 'questions',
15
+ actions: [
16
+ 'Check requirements are measurable',
17
+ 'Run: aia next <feature>',
18
+ ],
19
+ tips: ['Missing acceptance criteria? Iterate.'],
20
+ },
21
+ questions: {
22
+ summary: 'Questions identified',
23
+ next: 'tech-spec',
24
+ actions: [
25
+ 'Answer key questions in init.md or as iteration instructions',
26
+ 'Run: aia next <feature>',
27
+ ],
28
+ tips: ['Unanswered questions will affect quality of next steps'],
29
+ },
30
+ 'tech-spec': {
31
+ summary: 'Technical specification ready',
32
+ next: 'challenge',
33
+ actions: [
34
+ 'Review technical choices and constraints',
35
+ 'Run: aia next <feature>',
36
+ ],
37
+ tips: ['Consider performance and scalability implications'],
38
+ },
39
+ challenge: {
40
+ summary: 'Challenges identified',
41
+ next: 'dev-plan',
42
+ actions: [
43
+ 'Address critical challenges before proceeding',
44
+ 'Run: aia next <feature>',
45
+ ],
46
+ tips: ['Unresolved challenges may cause issues during implementation'],
47
+ },
48
+ 'dev-plan': {
49
+ summary: 'Development plan ready',
50
+ next: 'implement',
51
+ actions: [
52
+ 'Review task breakdown',
53
+ 'Run: aia next <feature> -a (agent mode)',
54
+ ],
55
+ tips: ['Implementation will modify your codebase'],
56
+ },
57
+ implement: {
58
+ summary: 'Implementation completed',
59
+ next: 'review',
60
+ actions: [
61
+ 'Run tests',
62
+ 'Manual verification',
63
+ 'Run: aia next <feature>',
64
+ ],
65
+ tips: ['Check files created/modified'],
66
+ },
67
+ review: {
68
+ summary: 'Feature complete!',
69
+ next: null,
70
+ actions: [
71
+ 'Address review comments',
72
+ 'Merge to main branch',
73
+ ],
74
+ tips: [],
75
+ },
76
+ },
77
+ fr: {
78
+ brief: {
79
+ summary: 'Brief produit terminé',
80
+ next: 'ba-spec',
81
+ actions: [
82
+ 'Vérifier que le brief est complet',
83
+ 'Lancer : aia next <feature>',
84
+ ],
85
+ tips: ['Si trop vague, itérer avec plus de contexte'],
86
+ },
87
+ 'ba-spec': {
88
+ summary: 'Analyse métier terminée',
89
+ next: 'questions',
90
+ actions: [
91
+ 'Vérifier que les exigences sont mesurables',
92
+ 'Lancer : aia next <feature>',
93
+ ],
94
+ tips: ['Critères d\'acceptation manquants ? Itérer.'],
95
+ },
96
+ questions: {
97
+ summary: 'Questions identifiées',
98
+ next: 'tech-spec',
99
+ actions: [
100
+ 'Répondre aux questions clés dans init.md ou via instructions d\'itération',
101
+ 'Lancer : aia next <feature>',
102
+ ],
103
+ tips: ['Les questions sans réponse affecteront la qualité des étapes suivantes'],
104
+ },
105
+ 'tech-spec': {
106
+ summary: 'Spécification technique prête',
107
+ next: 'challenge',
108
+ actions: [
109
+ 'Revoir les choix techniques et contraintes',
110
+ 'Lancer : aia next <feature>',
111
+ ],
112
+ tips: ['Considérer les implications de performance et scalabilité'],
113
+ },
114
+ challenge: {
115
+ summary: 'Défis identifiés',
116
+ next: 'dev-plan',
117
+ actions: [
118
+ 'Traiter les défis critiques avant de continuer',
119
+ 'Lancer : aia next <feature>',
120
+ ],
121
+ tips: ['Les défis non résolus peuvent causer des problèmes lors de l\'implémentation'],
122
+ },
123
+ 'dev-plan': {
124
+ summary: 'Plan de développement prêt',
125
+ next: 'implement',
126
+ actions: [
127
+ 'Revoir le découpage des tâches',
128
+ 'Lancer : aia next <feature> -a (mode agent)',
129
+ ],
130
+ tips: ['L\'implémentation va modifier votre code'],
131
+ },
132
+ implement: {
133
+ summary: 'Implémentation terminée',
134
+ next: 'review',
135
+ actions: [
136
+ 'Lancer les tests',
137
+ 'Vérification manuelle',
138
+ 'Lancer : aia next <feature>',
139
+ ],
140
+ tips: ['Vérifier les fichiers créés/modifiés'],
141
+ },
142
+ review: {
143
+ summary: 'Feature terminée !',
144
+ next: null,
145
+ actions: [
146
+ 'Traiter les commentaires de review',
147
+ 'Merger sur la branche principale',
148
+ ],
149
+ tips: [],
150
+ },
151
+ },
152
+ };
153
+
154
+ function getLanguageCode(language) {
155
+ if (!language) return 'en';
156
+ const lower = language.toLowerCase();
157
+ if (lower.includes('french') || lower.includes('français')) return 'fr';
158
+ if (lower.includes('english') || lower.includes('anglais')) return 'en';
159
+ return 'en';
160
+ }
161
+
162
+ export function getGuidance(step, language = 'English') {
163
+ const langCode = getLanguageCode(language);
164
+ const guidance = STEP_GUIDANCE[langCode]?.[step] || STEP_GUIDANCE.en[step];
165
+ return guidance || null;
166
+ }