@agile-vibe-coding/avc 0.1.0 → 0.1.1

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.
@@ -2,7 +2,7 @@ import dotenv from 'dotenv';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import readline from 'readline';
5
- import Anthropic from '@anthropic-ai/sdk';
5
+ import { LLMProvider } from './llm-provider.js';
6
6
  import { fileURLToPath } from 'url';
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
@@ -20,7 +20,7 @@ const __dirname = path.dirname(__filename);
20
20
  * 6. Write to .avc/project/doc.md
21
21
  */
22
22
  class TemplateProcessor {
23
- constructor(progressPath = null) {
23
+ constructor(progressPath = null, nonInteractive = false) {
24
24
  // Load environment variables from project .env
25
25
  dotenv.config({ path: path.join(process.cwd(), '.env') });
26
26
 
@@ -29,11 +29,13 @@ class TemplateProcessor {
29
29
  this.outputPath = path.join(this.outputDir, 'doc.md');
30
30
  this.avcConfigPath = path.join(process.cwd(), '.avc/avc.json');
31
31
  this.progressPath = progressPath;
32
+ this.nonInteractive = nonInteractive;
32
33
 
33
34
  // Read model configuration from avc.json
34
- this.model = this.readModelConfig();
35
- this.apiKey = process.env.ANTHROPIC_API_KEY;
36
- this.claudeClient = null;
35
+ const { provider, model } = this.readModelConfig();
36
+ this._providerName = provider;
37
+ this._modelName = model;
38
+ this.llmProvider = null;
37
39
  }
38
40
 
39
41
  /**
@@ -42,10 +44,73 @@ class TemplateProcessor {
42
44
  readModelConfig() {
43
45
  try {
44
46
  const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
45
- return config.settings?.model || 'claude-sonnet-4-5-20250929';
47
+ const ceremony = config.settings?.ceremonies?.[0];
48
+ if (ceremony) {
49
+ return { provider: ceremony.provider || 'claude', model: ceremony.defaultModel || 'claude-sonnet-4-5-20250929' };
50
+ }
51
+ // Legacy fallback: settings.model without ceremonies array
52
+ return { provider: 'claude', model: config.settings?.model || 'claude-sonnet-4-5-20250929' };
46
53
  } catch (error) {
47
54
  console.warn('⚠️ Could not read model config, using default');
48
- return 'claude-sonnet-4-5-20250929';
55
+ return { provider: 'claude', model: 'claude-sonnet-4-5-20250929' };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Read guidelines from avc.json ceremony configuration
61
+ */
62
+ readGuidelines() {
63
+ try {
64
+ const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
65
+ const ceremony = config.settings?.ceremonies?.[0];
66
+ return ceremony?.guidelines || {};
67
+ } catch (error) {
68
+ return {};
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Load agent instructions from markdown file
74
+ * @param {string} agentFileName - Filename in src/cli/agents/
75
+ * @returns {string|null} - Agent instructions content or null if not found
76
+ */
77
+ loadAgentInstructions(agentFileName) {
78
+ try {
79
+ const agentPath = path.join(__dirname, 'agents', agentFileName);
80
+ if (!fs.existsSync(agentPath)) {
81
+ console.warn(`⚠️ Agent instruction file not found: ${agentFileName}`);
82
+ return null;
83
+ }
84
+ return fs.readFileSync(agentPath, 'utf8');
85
+ } catch (error) {
86
+ console.warn(`⚠️ Could not load agent instructions: ${error.message}`);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get agent instructions for a specific ceremony stage
93
+ * @param {string} stage - Ceremony stage (e.g., 'enhancement', 'suggestion', 'validation')
94
+ * @returns {string|null} - Agent instructions or null if not configured/found
95
+ */
96
+ getAgentForStage(stage) {
97
+ try {
98
+ const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
99
+ const ceremony = config.settings?.ceremonies?.[0];
100
+
101
+ if (!ceremony?.agents || ceremony.agents.length === 0) {
102
+ return null;
103
+ }
104
+
105
+ const agent = ceremony.agents.find(a => a.stage === stage);
106
+ if (!agent) {
107
+ return null;
108
+ }
109
+
110
+ return this.loadAgentInstructions(agent.instruction);
111
+ } catch (error) {
112
+ console.warn(`⚠️ Could not get agent for stage ${stage}: ${error.message}`);
113
+ return null;
49
114
  }
50
115
  }
51
116
 
@@ -219,18 +284,17 @@ class TemplateProcessor {
219
284
  }
220
285
 
221
286
  /**
222
- * Initialize Claude client
287
+ * Initialize LLM provider
223
288
  */
224
- initializeClaudeClient() {
225
- if (!process.env.ANTHROPIC_API_KEY) {
226
- console.log('⚠️ ANTHROPIC_API_KEY not found - AI suggestions will be skipped');
289
+ async initializeLLMProvider() {
290
+ try {
291
+ this.llmProvider = await LLMProvider.create(this._providerName, this._modelName);
292
+ return this.llmProvider;
293
+ } catch (error) {
294
+ console.log(`⚠️ Could not initialize ${this._providerName} provider - AI suggestions will be skipped`);
295
+ console.log(` ${error.message}`);
227
296
  return null;
228
297
  }
229
-
230
- this.claudeClient = new Anthropic({
231
- apiKey: process.env.ANTHROPIC_API_KEY
232
- });
233
- return this.claudeClient;
234
298
  }
235
299
 
236
300
  /**
@@ -264,11 +328,12 @@ class TemplateProcessor {
264
328
  /**
265
329
  * Parse Claude's response into structured format
266
330
  */
267
- parseClaudeResponse(response, isPlural) {
331
+ parseLLMResponse(response, isPlural) {
268
332
  if (isPlural) {
269
333
  return response.split('\n')
270
334
  .map(line => line.trim())
271
- .filter(line => line.length > 0 && !line.match(/^[0-9\-*.]+\s/));
335
+ .filter(line => line.length > 0)
336
+ .map(line => line.replace(/^[0-9\-*.]+\s+/, '')); // Remove list prefixes
272
337
  }
273
338
  return response.trim();
274
339
  }
@@ -277,20 +342,14 @@ class TemplateProcessor {
277
342
  * Generate AI suggestions for a variable
278
343
  */
279
344
  async generateSuggestions(variableName, isPlural, context) {
280
- if (!this.claudeClient && !this.initializeClaudeClient()) {
345
+ if (!this.llmProvider && !(await this.initializeLLMProvider())) {
281
346
  return null;
282
347
  }
283
348
 
284
349
  try {
285
350
  const prompt = this.buildPrompt(variableName, isPlural, context);
286
-
287
- const response = await this.claudeClient.messages.create({
288
- model: this.model,
289
- max_tokens: isPlural ? 512 : 256,
290
- messages: [{ role: "user", content: prompt }]
291
- });
292
-
293
- return this.parseClaudeResponse(response.content[0].text, isPlural);
351
+ const text = await this.llmProvider.generate(prompt, isPlural ? 512 : 256);
352
+ return this.parseLLMResponse(text, isPlural);
294
353
  } catch (error) {
295
354
  console.warn(`⚠️ Could not generate suggestions: ${error.message}`);
296
355
  return null;
@@ -304,14 +363,45 @@ class TemplateProcessor {
304
363
  async promptUser(variable, context) {
305
364
  let value;
306
365
 
307
- if (variable.isPlural) {
308
- value = await this.promptPlural(variable.displayName, variable.guidance);
366
+ // In non-interactive mode, skip readline prompts and use guidelines/AI
367
+ if (this.nonInteractive) {
368
+ console.log(`\n📝 ${variable.displayName}`);
369
+ if (variable.guidance) {
370
+ console.log(` ${variable.guidance}`);
371
+ }
372
+ console.log(' Generating AI response...');
373
+ value = null; // Force AI generation
309
374
  } else {
310
- value = await this.promptSingular(variable.displayName, variable.guidance);
375
+ // Interactive mode - use readline prompts
376
+ if (variable.isPlural) {
377
+ value = await this.promptPlural(variable.displayName, variable.guidance);
378
+ } else {
379
+ value = await this.promptSingular(variable.displayName, variable.guidance);
380
+ }
311
381
  }
312
382
 
313
- // If user skipped, try to generate AI suggestions
383
+ // If user skipped (or non-interactive mode), try to use guideline or generate AI suggestions
314
384
  if (value === null) {
385
+ // Check if there's a guideline for this variable
386
+ const guidelines = this.readGuidelines();
387
+ const guidelineKey = variable.name.toLowerCase().replace(/_/g, '');
388
+
389
+ if (guidelines[guidelineKey]) {
390
+ console.log(' 📋 Using default guideline...');
391
+ value = variable.isPlural
392
+ ? [guidelines[guidelineKey]] // Wrap in array for plural variables
393
+ : guidelines[guidelineKey];
394
+
395
+ console.log(' ✅ Guideline applied:');
396
+ if (Array.isArray(value)) {
397
+ value.forEach((item, idx) => console.log(` ${idx + 1}. ${item}`));
398
+ } else {
399
+ console.log(` ${value}`);
400
+ }
401
+ return { variable: variable.name, value, source: 'guideline', skipped: true };
402
+ }
403
+
404
+ // No guideline available, try AI suggestions
315
405
  console.log(' ✨ Generating AI suggestion...');
316
406
  value = await this.generateSuggestions(variable.name, variable.isPlural, context);
317
407
 
@@ -361,20 +451,34 @@ class TemplateProcessor {
361
451
  * Generate final document with LLM enhancement
362
452
  */
363
453
  async generateFinalDocument(templateWithValues) {
364
- if (!this.claudeClient && !this.initializeClaudeClient()) {
365
- // No API key - save template as-is
454
+ if (!this.llmProvider && !(await this.initializeLLMProvider())) {
455
+ // No provider available - save template as-is
456
+ console.log('\n⚠️ AI enhancement skipped (no LLM provider available)');
366
457
  return templateWithValues;
367
458
  }
368
459
 
369
460
  console.log('\n🤖 Enhancing document with AI...');
370
461
 
371
462
  try {
372
- const response = await this.claudeClient.messages.create({
373
- model: this.model,
374
- max_tokens: 4096,
375
- messages: [{
376
- role: "user",
377
- content: `You are creating a project definition document for an Agile Vibe Coding (AVC) project.
463
+ // Try to load agent instructions for enhancement stage
464
+ const agentInstructions = this.getAgentForStage('enhancement');
465
+
466
+ if (agentInstructions) {
467
+ console.log(' Using custom agent instructions for enhancement');
468
+ // Use agent instructions as system context
469
+ const userPrompt = `Here is the project information with all variables filled in:
470
+
471
+ ${templateWithValues}
472
+
473
+ Please review and enhance this document according to your role.`;
474
+
475
+ const enhanced = await this.llmProvider.generate(userPrompt, 4096, agentInstructions);
476
+ console.log(' ✓ Document enhanced successfully');
477
+ return enhanced;
478
+ } else {
479
+ console.log(' Using default enhancement instructions');
480
+ // Fallback to legacy hardcoded prompt for backward compatibility
481
+ const legacyPrompt = `You are creating a project definition document for an Agile Vibe Coding (AVC) project.
378
482
 
379
483
  Here is the project information with all variables filled in:
380
484
 
@@ -386,13 +490,15 @@ Please review and enhance this document to ensure:
386
490
  3. Sections flow logically
387
491
  4. Any incomplete sections are identified
388
492
 
389
- Return the enhanced markdown document.`
390
- }]
391
- });
493
+ Return the enhanced markdown document.`;
392
494
 
393
- return response.content[0].text;
495
+ const enhanced = await this.llmProvider.generate(legacyPrompt, 4096);
496
+ console.log(' ✓ Document enhanced successfully');
497
+ return enhanced;
498
+ }
394
499
  } catch (error) {
395
- console.warn(`⚠️ Could not enhance document: ${error.message}`);
500
+ console.warn(`\n⚠️ Could not enhance document: ${error.message}`);
501
+ console.log(' Using template without AI enhancement');
396
502
  return templateWithValues;
397
503
  }
398
504
  }
@@ -406,6 +512,67 @@ Return the enhanced markdown document.`
406
512
  }
407
513
  }
408
514
 
515
+ /**
516
+ * Sync project documentation to VitePress documentation folder
517
+ */
518
+ syncToVitePress(content) {
519
+ try {
520
+ const docsDir = path.join(process.cwd(), '.avc/documentation');
521
+ const indexPath = path.join(docsDir, 'index.md');
522
+
523
+ // Check if documentation folder exists
524
+ if (!fs.existsSync(docsDir)) {
525
+ console.log(' ℹ️ VitePress documentation folder not found, skipping sync');
526
+ return false;
527
+ }
528
+
529
+ // Write to .avc/documentation/index.md
530
+ fs.writeFileSync(indexPath, content, 'utf8');
531
+ console.log(` ✓ Synced to .avc/documentation/index.md`);
532
+ return true;
533
+ } catch (error) {
534
+ console.warn(` ⚠️ Could not sync to VitePress: ${error.message}`);
535
+ return false;
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Build VitePress documentation site
541
+ */
542
+ async buildVitePress() {
543
+ try {
544
+ const docsDir = path.join(process.cwd(), '.avc/documentation');
545
+ const packagePath = path.join(process.cwd(), 'package.json');
546
+
547
+ // Check if VitePress is configured
548
+ if (!fs.existsSync(docsDir) || !fs.existsSync(packagePath)) {
549
+ return false;
550
+ }
551
+
552
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
553
+ if (!packageJson.scripts?.['docs:build']) {
554
+ return false;
555
+ }
556
+
557
+ console.log('\n📚 Building VitePress documentation...');
558
+
559
+ // Import execSync for running build command
560
+ const { execSync } = await import('child_process');
561
+
562
+ // Run VitePress build
563
+ execSync('npm run docs:build', {
564
+ cwd: process.cwd(),
565
+ stdio: 'inherit'
566
+ });
567
+
568
+ console.log('✓ VitePress build completed');
569
+ return true;
570
+ } catch (error) {
571
+ console.warn(`⚠️ VitePress build failed: ${error.message}`);
572
+ return false;
573
+ }
574
+ }
575
+
409
576
  /**
410
577
  * Write document to file
411
578
  */
@@ -420,6 +587,14 @@ Return the enhanced markdown document.`
420
587
 
421
588
  console.log(`\n✅ Project document generated!`);
422
589
  console.log(` Location: ${this.outputPath}`);
590
+
591
+ // Sync to VitePress if configured
592
+ const synced = this.syncToVitePress(content);
593
+
594
+ // Optionally build VitePress (commented out by default to avoid slow builds during dev)
595
+ // if (synced) {
596
+ // await this.buildVitePress();
597
+ // }
423
598
  }
424
599
 
425
600
  /**
@@ -441,38 +616,64 @@ Return the enhanced markdown document.`
441
616
  if (initialProgress && initialProgress.collectedValues) {
442
617
  collectedValues = { ...initialProgress.collectedValues };
443
618
  answeredCount = Object.keys(collectedValues).length;
444
- console.log(`Resuming with ${answeredCount}/${variables.length} questions already answered.\n`);
445
- } else {
446
- console.log(`Found ${variables.length} sections to complete.\n`);
447
- }
448
619
 
449
- // 4. Collect values with context accumulation
450
- for (const variable of variables) {
451
- // Skip already answered questions when resuming
452
- if (collectedValues[variable.name] !== undefined) {
453
- console.log(`\n✓ ${variable.displayName}`);
454
- console.log(` Using previous answer: ${
455
- Array.isArray(collectedValues[variable.name])
456
- ? `${collectedValues[variable.name].length} items`
457
- : `"${collectedValues[variable.name].substring(0, 60)}${collectedValues[variable.name].length > 60 ? '...' : ''}"`
458
- }`);
459
- continue;
620
+ // Check if ALL answers are pre-filled (from REPL questionnaire)
621
+ if (answeredCount === variables.length) {
622
+ console.log(`✅ Using ${answeredCount} pre-filled answers from questionnaire.\n`);
623
+
624
+ // Use pre-filled answers, but still allow AI enhancement for skipped (null) answers
625
+ for (const variable of variables) {
626
+ if (collectedValues[variable.name] === null) {
627
+ console.log(`\n📝 ${variable.displayName}`);
628
+ console.log(' Generating AI suggestion...');
629
+ const aiValue = await this.generateSuggestions(variable.name, variable.isPlural, collectedValues);
630
+ collectedValues[variable.name] = aiValue || '';
631
+ }
632
+ }
633
+ } else {
634
+ console.log(`Resuming with ${answeredCount}/${variables.length} questions already answered.\n`);
635
+
636
+ // Continue with normal interactive flow for remaining questions
637
+ for (const variable of variables) {
638
+ if (collectedValues[variable.name] === undefined) {
639
+ const result = await this.promptUser(variable, collectedValues);
640
+ collectedValues[result.variable] = result.value;
641
+ answeredCount++;
642
+
643
+ // Save progress after each question
644
+ if (this.progressPath) {
645
+ const progress = {
646
+ stage: 'questionnaire',
647
+ totalQuestions: variables.length,
648
+ answeredQuestions: answeredCount,
649
+ collectedValues: collectedValues,
650
+ lastUpdate: new Date().toISOString()
651
+ };
652
+ this.saveProgress(progress);
653
+ }
654
+ }
655
+ }
460
656
  }
657
+ } else {
658
+ console.log(`Found ${variables.length} sections to complete.\n`);
461
659
 
462
- const result = await this.promptUser(variable, collectedValues);
463
- collectedValues[result.variable] = result.value;
464
- answeredCount++;
465
-
466
- // Save progress after each question
467
- if (this.progressPath) {
468
- const progress = {
469
- stage: 'questionnaire',
470
- totalQuestions: variables.length,
471
- answeredQuestions: answeredCount,
472
- collectedValues: collectedValues,
473
- lastUpdate: new Date().toISOString()
474
- };
475
- this.saveProgress(progress);
660
+ // 4. Collect values with context accumulation
661
+ for (const variable of variables) {
662
+ const result = await this.promptUser(variable, collectedValues);
663
+ collectedValues[result.variable] = result.value;
664
+ answeredCount++;
665
+
666
+ // Save progress after each question
667
+ if (this.progressPath) {
668
+ const progress = {
669
+ stage: 'questionnaire',
670
+ totalQuestions: variables.length,
671
+ answeredQuestions: answeredCount,
672
+ collectedValues: collectedValues,
673
+ lastUpdate: new Date().toISOString()
674
+ };
675
+ this.saveProgress(progress);
676
+ }
476
677
  }
477
678
  }
478
679
 
@@ -0,0 +1,33 @@
1
+ import { defineConfig } from 'vitepress'
2
+
3
+ export default defineConfig({
4
+ title: '{{PROJECT_NAME}}',
5
+ description: 'Project documentation powered by Agile Vibe Coding',
6
+ base: '/',
7
+
8
+ // Custom head tags for AVC documentation identification
9
+ head: [
10
+ ['meta', { name: 'avc-documentation', content: 'true' }],
11
+ ['meta', { name: 'generator', content: 'Agile Vibe Coding' }]
12
+ ],
13
+
14
+ themeConfig: {
15
+ nav: [
16
+ { text: 'Home', link: '/' },
17
+ { text: 'Epics', link: '/epics' }
18
+ ],
19
+
20
+ sidebar: [
21
+ {
22
+ text: 'Project',
23
+ items: [
24
+ { text: 'Overview', link: '/' }
25
+ ]
26
+ }
27
+ ],
28
+
29
+ socialLinks: [
30
+ { icon: 'github', link: 'https://github.com/yourusername/yourproject' }
31
+ ]
32
+ }
33
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-vibe-coding/avc",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Agile Vibe Coding (AVC) - Framework for managing AI agent-based software development projects",
5
5
  "type": "module",
6
6
  "main": "cli/index.js",
@@ -16,7 +16,12 @@
16
16
  "access": "public"
17
17
  },
18
18
  "scripts": {
19
- "test": "echo \"No tests specified yet\" && exit 0",
19
+ "test": "vitest run",
20
+ "test:unit": "vitest run tests/unit",
21
+ "test:integration": "vitest run tests/integration",
22
+ "test:watch": "vitest watch",
23
+ "test:coverage": "vitest run --coverage",
24
+ "test:ui": "vitest --ui",
20
25
  "prepublishOnly": "echo \"Running pre-publish checks...\" && npm test"
21
26
  },
22
27
  "keywords": [
@@ -49,10 +54,19 @@
49
54
  },
50
55
  "dependencies": {
51
56
  "@anthropic-ai/sdk": "^0.20.0",
57
+ "@google/genai": "^1.37.0",
52
58
  "dotenv": "^16.4.0",
53
59
  "ink": "^5.0.1",
54
60
  "ink-select-input": "^6.0.0",
55
61
  "ink-spinner": "^5.0.0",
56
62
  "react": "^18.3.1"
63
+ },
64
+ "devDependencies": {
65
+ "@sinonjs/fake-timers": "^12.0.0",
66
+ "@vitest/coverage-v8": "^2.1.9",
67
+ "@vitest/ui": "^2.1.9",
68
+ "memfs": "^4.56.10",
69
+ "sinon": "^19.0.5",
70
+ "vitest": "^2.1.9"
57
71
  }
58
- }
72
+ }