@agile-vibe-coding/avc 0.1.0 → 0.2.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 +2 -0
- package/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +129 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/epic-story-decomposer.md +280 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +79 -0
- package/cli/agents/mission-scope-validator.md +112 -0
- package/cli/agents/project-context-extractor.md +107 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/solver-epic-api.json +15 -0
- package/cli/agents/solver-epic-api.md +39 -0
- package/cli/agents/solver-epic-backend.json +15 -0
- package/cli/agents/solver-epic-backend.md +39 -0
- package/cli/agents/solver-epic-cloud.json +15 -0
- package/cli/agents/solver-epic-cloud.md +39 -0
- package/cli/agents/solver-epic-data.json +15 -0
- package/cli/agents/solver-epic-data.md +39 -0
- package/cli/agents/solver-epic-database.json +15 -0
- package/cli/agents/solver-epic-database.md +39 -0
- package/cli/agents/solver-epic-developer.json +15 -0
- package/cli/agents/solver-epic-developer.md +39 -0
- package/cli/agents/solver-epic-devops.json +15 -0
- package/cli/agents/solver-epic-devops.md +39 -0
- package/cli/agents/solver-epic-frontend.json +15 -0
- package/cli/agents/solver-epic-frontend.md +39 -0
- package/cli/agents/solver-epic-mobile.json +15 -0
- package/cli/agents/solver-epic-mobile.md +39 -0
- package/cli/agents/solver-epic-qa.json +15 -0
- package/cli/agents/solver-epic-qa.md +39 -0
- package/cli/agents/solver-epic-security.json +15 -0
- package/cli/agents/solver-epic-security.md +39 -0
- package/cli/agents/solver-epic-solution-architect.json +15 -0
- package/cli/agents/solver-epic-solution-architect.md +39 -0
- package/cli/agents/solver-epic-test-architect.json +15 -0
- package/cli/agents/solver-epic-test-architect.md +39 -0
- package/cli/agents/solver-epic-ui.json +15 -0
- package/cli/agents/solver-epic-ui.md +39 -0
- package/cli/agents/solver-epic-ux.json +15 -0
- package/cli/agents/solver-epic-ux.md +39 -0
- package/cli/agents/solver-story-api.json +15 -0
- package/cli/agents/solver-story-api.md +39 -0
- package/cli/agents/solver-story-backend.json +15 -0
- package/cli/agents/solver-story-backend.md +39 -0
- package/cli/agents/solver-story-cloud.json +15 -0
- package/cli/agents/solver-story-cloud.md +39 -0
- package/cli/agents/solver-story-data.json +15 -0
- package/cli/agents/solver-story-data.md +39 -0
- package/cli/agents/solver-story-database.json +15 -0
- package/cli/agents/solver-story-database.md +39 -0
- package/cli/agents/solver-story-developer.json +15 -0
- package/cli/agents/solver-story-developer.md +39 -0
- package/cli/agents/solver-story-devops.json +15 -0
- package/cli/agents/solver-story-devops.md +39 -0
- package/cli/agents/solver-story-frontend.json +15 -0
- package/cli/agents/solver-story-frontend.md +39 -0
- package/cli/agents/solver-story-mobile.json +15 -0
- package/cli/agents/solver-story-mobile.md +39 -0
- package/cli/agents/solver-story-qa.json +15 -0
- package/cli/agents/solver-story-qa.md +39 -0
- package/cli/agents/solver-story-security.json +15 -0
- package/cli/agents/solver-story-security.md +39 -0
- package/cli/agents/solver-story-solution-architect.json +15 -0
- package/cli/agents/solver-story-solution-architect.md +39 -0
- package/cli/agents/solver-story-test-architect.json +15 -0
- package/cli/agents/solver-story-test-architect.md +39 -0
- package/cli/agents/solver-story-ui.json +15 -0
- package/cli/agents/solver-story-ui.md +39 -0
- package/cli/agents/solver-story-ux.json +15 -0
- package/cli/agents/solver-story-ux.md +39 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +152 -0
- package/cli/agents/validator-documentation.md +453 -0
- package/cli/agents/validator-epic-api.json +93 -0
- package/cli/agents/validator-epic-api.md +137 -0
- package/cli/agents/validator-epic-backend.json +93 -0
- package/cli/agents/validator-epic-backend.md +130 -0
- package/cli/agents/validator-epic-cloud.json +93 -0
- package/cli/agents/validator-epic-cloud.md +137 -0
- package/cli/agents/validator-epic-data.json +93 -0
- package/cli/agents/validator-epic-data.md +130 -0
- package/cli/agents/validator-epic-database.json +93 -0
- package/cli/agents/validator-epic-database.md +137 -0
- package/cli/agents/validator-epic-developer.json +74 -0
- package/cli/agents/validator-epic-developer.md +153 -0
- package/cli/agents/validator-epic-devops.json +74 -0
- package/cli/agents/validator-epic-devops.md +153 -0
- package/cli/agents/validator-epic-frontend.json +74 -0
- package/cli/agents/validator-epic-frontend.md +153 -0
- package/cli/agents/validator-epic-mobile.json +93 -0
- package/cli/agents/validator-epic-mobile.md +130 -0
- package/cli/agents/validator-epic-qa.json +93 -0
- package/cli/agents/validator-epic-qa.md +130 -0
- package/cli/agents/validator-epic-security.json +74 -0
- package/cli/agents/validator-epic-security.md +154 -0
- package/cli/agents/validator-epic-solution-architect.json +74 -0
- package/cli/agents/validator-epic-solution-architect.md +156 -0
- package/cli/agents/validator-epic-test-architect.json +93 -0
- package/cli/agents/validator-epic-test-architect.md +130 -0
- package/cli/agents/validator-epic-ui.json +93 -0
- package/cli/agents/validator-epic-ui.md +130 -0
- package/cli/agents/validator-epic-ux.json +93 -0
- package/cli/agents/validator-epic-ux.md +130 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/agents/validator-story-api.json +104 -0
- package/cli/agents/validator-story-api.md +152 -0
- package/cli/agents/validator-story-backend.json +104 -0
- package/cli/agents/validator-story-backend.md +152 -0
- package/cli/agents/validator-story-cloud.json +104 -0
- package/cli/agents/validator-story-cloud.md +152 -0
- package/cli/agents/validator-story-data.json +104 -0
- package/cli/agents/validator-story-data.md +152 -0
- package/cli/agents/validator-story-database.json +104 -0
- package/cli/agents/validator-story-database.md +152 -0
- package/cli/agents/validator-story-developer.json +104 -0
- package/cli/agents/validator-story-developer.md +152 -0
- package/cli/agents/validator-story-devops.json +104 -0
- package/cli/agents/validator-story-devops.md +152 -0
- package/cli/agents/validator-story-frontend.json +104 -0
- package/cli/agents/validator-story-frontend.md +152 -0
- package/cli/agents/validator-story-mobile.json +104 -0
- package/cli/agents/validator-story-mobile.md +152 -0
- package/cli/agents/validator-story-qa.json +104 -0
- package/cli/agents/validator-story-qa.md +152 -0
- package/cli/agents/validator-story-security.json +104 -0
- package/cli/agents/validator-story-security.md +152 -0
- package/cli/agents/validator-story-solution-architect.json +104 -0
- package/cli/agents/validator-story-solution-architect.md +152 -0
- package/cli/agents/validator-story-test-architect.json +104 -0
- package/cli/agents/validator-story-test-architect.md +152 -0
- package/cli/agents/validator-story-ui.json +104 -0
- package/cli/agents/validator-story-ui.md +152 -0
- package/cli/agents/validator-story-ux.json +104 -0
- package/cli/agents/validator-story-ux.md +152 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/build-docs.js +298 -0
- package/cli/ceremony-history.js +369 -0
- package/cli/command-logger.js +245 -0
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +1174 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/index.js +3 -25
- package/cli/init-model-config.js +697 -0
- package/cli/init.js +1765 -100
- package/cli/kanban-server-manager.js +228 -0
- package/cli/llm-claude.js +109 -0
- package/cli/llm-gemini.js +115 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +233 -0
- package/cli/llm-provider.js +300 -0
- package/cli/llm-token-limits.js +102 -0
- package/cli/llm-verifier.js +454 -0
- package/cli/logger.js +32 -5
- package/cli/message-constants.js +58 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +297 -0
- package/cli/model-pricing.js +169 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +269 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +332 -0
- package/cli/repl-ink.js +5840 -504
- package/cli/repl-old.js +4 -4
- package/cli/seed-processor.js +792 -0
- package/cli/sprint-planning-processor.js +1813 -0
- package/cli/template-processor.js +2306 -108
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +34 -0
- package/cli/token-tracker.js +520 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +605 -0
- package/cli/verification-tracker.js +563 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
- package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +622 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +353 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +118 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +401 -0
- package/kanban/client/src/lib/status-grouping.js +144 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +115 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +516 -0
- package/kanban/server/routes/ceremony.js +305 -0
- package/kanban/server/routes/costs.js +157 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +303 -0
- package/kanban/server/routes/websocket.js +276 -0
- package/kanban/server/routes/work-items.js +347 -0
- package/kanban/server/services/CeremonyService.js +1190 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/sponsor-call-worker.js +84 -0
- package/kanban/server/workers/sprint-planning-worker.js +130 -0
- package/package.json +34 -7
package/cli/init.js
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import dotenv from 'dotenv';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
6
7
|
import { execSync } from 'child_process';
|
|
7
8
|
import { TemplateProcessor } from './template-processor.js';
|
|
9
|
+
import { ModelConfigurator } from './init-model-config.js';
|
|
10
|
+
import { MESSAGES, getCeremonyHeader } from './message-constants.js';
|
|
11
|
+
import { sendError, sendWarning, sendSuccess, sendInfo, sendOutput, sendIndented, sendSectionHeader } from './messaging-api.js';
|
|
12
|
+
import { boldCyan, yellow, green, cyan } from './ansi-colors.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Write a structured entry to the active command log file only.
|
|
16
|
+
* Uses [DEBUG] prefix so ConsoleOutputManager routes to file, never terminal.
|
|
17
|
+
*/
|
|
18
|
+
function fileLog(level, message, data = null) {
|
|
19
|
+
const ts = new Date().toISOString();
|
|
20
|
+
if (data !== null) {
|
|
21
|
+
console.log(`[DEBUG] [${level}] [${ts}] ${message}`, JSON.stringify(data, null, 2));
|
|
22
|
+
} else {
|
|
23
|
+
console.log(`[DEBUG] [${level}] [${ts}] ${message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
8
26
|
|
|
9
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
28
|
const __dirname = path.dirname(__filename);
|
|
@@ -19,11 +37,25 @@ const __dirname = path.dirname(__filename);
|
|
|
19
37
|
*/
|
|
20
38
|
|
|
21
39
|
class ProjectInitiator {
|
|
22
|
-
constructor() {
|
|
23
|
-
this.projectRoot = process.cwd();
|
|
40
|
+
constructor(projectRoot = null) {
|
|
41
|
+
this.projectRoot = projectRoot || process.cwd();
|
|
24
42
|
this.avcDir = path.join(this.projectRoot, '.avc');
|
|
43
|
+
this.srcDir = path.join(this.projectRoot, 'src');
|
|
44
|
+
this.worktreesDir = path.join(this.avcDir, 'worktrees');
|
|
25
45
|
this.avcConfigPath = path.join(this.avcDir, 'avc.json');
|
|
26
|
-
|
|
46
|
+
// Progress files are ceremony-specific
|
|
47
|
+
this.initProgressPath = path.join(this.avcDir, 'init-progress.json');
|
|
48
|
+
this.sponsorCallProgressPath = path.join(this.avcDir, 'sponsor-call-progress.json');
|
|
49
|
+
|
|
50
|
+
// Template processor for token usage tracking
|
|
51
|
+
this._lastTemplateProcessor = null;
|
|
52
|
+
|
|
53
|
+
// Load environment variables from project .env file
|
|
54
|
+
// Use override: true to reload even if already set (user may have edited .env)
|
|
55
|
+
dotenv.config({
|
|
56
|
+
path: path.join(this.projectRoot, '.env'),
|
|
57
|
+
override: true
|
|
58
|
+
});
|
|
27
59
|
}
|
|
28
60
|
|
|
29
61
|
/**
|
|
@@ -33,6 +65,47 @@ class ProjectInitiator {
|
|
|
33
65
|
return path.basename(this.projectRoot);
|
|
34
66
|
}
|
|
35
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Get the current AVC package version
|
|
70
|
+
*/
|
|
71
|
+
getAvcVersion() {
|
|
72
|
+
try {
|
|
73
|
+
const packagePath = path.join(__dirname, '../package.json');
|
|
74
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
75
|
+
return packageJson.version;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return 'unknown';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Deep merge objects - adds new keys, preserves existing values
|
|
83
|
+
* @param {Object} target - The target object (user's config)
|
|
84
|
+
* @param {Object} source - The source object (default config)
|
|
85
|
+
* @returns {Object} Merged object
|
|
86
|
+
*/
|
|
87
|
+
deepMerge(target, source) {
|
|
88
|
+
const result = { ...target };
|
|
89
|
+
|
|
90
|
+
for (const key in source) {
|
|
91
|
+
if (source.hasOwnProperty(key)) {
|
|
92
|
+
if (key in result) {
|
|
93
|
+
// Key exists in target
|
|
94
|
+
if (typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key] !== null) {
|
|
95
|
+
// Recursively merge objects
|
|
96
|
+
result[key] = this.deepMerge(result[key], source[key]);
|
|
97
|
+
}
|
|
98
|
+
// else: Keep existing value (don't overwrite)
|
|
99
|
+
} else {
|
|
100
|
+
// New key - add it
|
|
101
|
+
result[key] = source[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
36
109
|
/**
|
|
37
110
|
* Check if .avc folder exists
|
|
38
111
|
*/
|
|
@@ -53,41 +126,454 @@ class ProjectInitiator {
|
|
|
53
126
|
createAvcFolder() {
|
|
54
127
|
if (!this.hasAvcFolder()) {
|
|
55
128
|
fs.mkdirSync(this.avcDir, { recursive: true });
|
|
56
|
-
console.log('✓ Created .avc/ folder');
|
|
57
129
|
return true;
|
|
58
130
|
}
|
|
59
|
-
console.log('✓ .avc/ folder already exists');
|
|
60
131
|
return false;
|
|
61
132
|
}
|
|
62
133
|
|
|
63
134
|
/**
|
|
64
|
-
*
|
|
135
|
+
* Check if src folder exists
|
|
136
|
+
*/
|
|
137
|
+
hasSrcFolder() {
|
|
138
|
+
return fs.existsSync(this.srcDir);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create src folder for AVC-managed code
|
|
143
|
+
*/
|
|
144
|
+
createSrcFolder() {
|
|
145
|
+
if (!this.hasSrcFolder()) {
|
|
146
|
+
fs.mkdirSync(this.srcDir, { recursive: true });
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if worktrees folder exists
|
|
154
|
+
*/
|
|
155
|
+
hasWorktreesFolder() {
|
|
156
|
+
return fs.existsSync(this.worktreesDir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create worktrees folder for git worktree management
|
|
161
|
+
*/
|
|
162
|
+
createWorktreesFolder() {
|
|
163
|
+
if (!this.hasWorktreesFolder()) {
|
|
164
|
+
fs.mkdirSync(this.worktreesDir, { recursive: true });
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create or update avc.json with default settings
|
|
172
|
+
* Merges new attributes from version updates while preserving existing values
|
|
65
173
|
*/
|
|
66
174
|
createAvcConfig() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
175
|
+
const defaultConfig = {
|
|
176
|
+
version: '1.0.0',
|
|
177
|
+
avcVersion: this.getAvcVersion(),
|
|
178
|
+
projectName: this.getProjectName(),
|
|
179
|
+
framework: 'avc',
|
|
180
|
+
created: new Date().toISOString(),
|
|
181
|
+
settings: {
|
|
182
|
+
workItemStatuses: ['ready', 'pending', 'implementing', 'implemented', 'testing', 'completed', 'blocked', 'feedback'],
|
|
183
|
+
agentTypes: ['product-owner', 'server', 'client', 'infrastructure', 'testing'],
|
|
184
|
+
documentation: {
|
|
185
|
+
port: 4173
|
|
186
|
+
},
|
|
187
|
+
ceremonies: [
|
|
188
|
+
{
|
|
189
|
+
name: 'sponsor-call',
|
|
190
|
+
provider: 'claude',
|
|
191
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
192
|
+
stages: {
|
|
193
|
+
suggestions: {
|
|
194
|
+
provider: 'claude',
|
|
195
|
+
model: 'claude-sonnet-4-6'
|
|
196
|
+
},
|
|
197
|
+
documentation: {
|
|
198
|
+
provider: 'claude',
|
|
199
|
+
model: 'claude-sonnet-4-6'
|
|
200
|
+
},
|
|
201
|
+
'architecture-recommendation': {
|
|
202
|
+
provider: 'claude',
|
|
203
|
+
model: 'claude-sonnet-4-6'
|
|
204
|
+
},
|
|
205
|
+
'question-prefilling': {
|
|
206
|
+
provider: 'claude',
|
|
207
|
+
model: 'claude-sonnet-4-6'
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
agents: [
|
|
211
|
+
{
|
|
212
|
+
name: 'project-documentation-creator',
|
|
213
|
+
instruction: 'project-documentation-creator.md',
|
|
214
|
+
stage: 'documentation-generation'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'validator-documentation',
|
|
218
|
+
instruction: 'validator-documentation.md',
|
|
219
|
+
stage: 'documentation-validation',
|
|
220
|
+
group: 'validators'
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
validation: {
|
|
224
|
+
enabled: true,
|
|
225
|
+
maxIterations: 100,
|
|
226
|
+
acceptanceThreshold: 95,
|
|
227
|
+
skipOnCriticalIssues: false,
|
|
228
|
+
provider: 'claude',
|
|
229
|
+
model: 'claude-haiku-4-5-20251001',
|
|
230
|
+
documentation: {
|
|
231
|
+
provider: 'claude',
|
|
232
|
+
model: 'claude-haiku-4-5-20251001'
|
|
233
|
+
},
|
|
234
|
+
refinement: {
|
|
235
|
+
provider: 'claude',
|
|
236
|
+
model: 'claude-sonnet-4-6'
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
crossValidation: {
|
|
240
|
+
enabled: true,
|
|
241
|
+
maxIterations: 3
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'sprint-planning',
|
|
246
|
+
provider: 'claude',
|
|
247
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
248
|
+
stages: {
|
|
249
|
+
decomposition: {
|
|
250
|
+
provider: 'claude',
|
|
251
|
+
model: 'claude-opus-4-6'
|
|
252
|
+
},
|
|
253
|
+
validation: {
|
|
254
|
+
provider: 'claude',
|
|
255
|
+
model: 'claude-sonnet-4-6',
|
|
256
|
+
useContextualSelection: true
|
|
257
|
+
},
|
|
258
|
+
'doc-distribution': {
|
|
259
|
+
provider: 'claude',
|
|
260
|
+
model: 'claude-sonnet-4-6'
|
|
261
|
+
},
|
|
262
|
+
enrichment: {
|
|
263
|
+
provider: 'claude',
|
|
264
|
+
model: 'claude-sonnet-4-6'
|
|
265
|
+
},
|
|
266
|
+
solver: {
|
|
267
|
+
provider: 'claude',
|
|
268
|
+
model: 'claude-haiku-4-5-20251001',
|
|
269
|
+
maxIterations: 3,
|
|
270
|
+
acceptanceThreshold: 95
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
agents: [
|
|
274
|
+
{
|
|
275
|
+
name: 'epic-story-decomposer',
|
|
276
|
+
instruction: 'epic-story-decomposer.md',
|
|
277
|
+
stage: 'decomposition'
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'doc-distributor',
|
|
281
|
+
instruction: 'doc-distributor.md',
|
|
282
|
+
stage: 'doc-distribution'
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'project-context-extractor',
|
|
286
|
+
instruction: 'project-context-extractor.md',
|
|
287
|
+
stage: 'validation'
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: 'agent-selector',
|
|
291
|
+
instruction: 'agent-selector.md',
|
|
292
|
+
stage: 'validation'
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: 'story-doc-enricher',
|
|
296
|
+
instruction: 'story-doc-enricher.md',
|
|
297
|
+
stage: 'enrichment'
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'seed',
|
|
303
|
+
provider: 'claude',
|
|
304
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
305
|
+
stages: {
|
|
306
|
+
decomposition: {
|
|
307
|
+
provider: 'claude',
|
|
308
|
+
model: 'claude-opus-4-6'
|
|
309
|
+
},
|
|
310
|
+
'doc-distribution': {
|
|
311
|
+
provider: 'claude',
|
|
312
|
+
model: 'claude-sonnet-4-6'
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
agents: [
|
|
316
|
+
{
|
|
317
|
+
name: 'task-subtask-decomposer',
|
|
318
|
+
instruction: 'task-subtask-decomposer.md',
|
|
319
|
+
stage: 'decomposition'
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'doc-distributor',
|
|
323
|
+
instruction: 'doc-distributor.md',
|
|
324
|
+
stage: 'doc-distribution'
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'feature-context-generator',
|
|
328
|
+
instruction: 'feature-context-generator.md',
|
|
329
|
+
stage: 'decomposition'
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
missionGenerator: {
|
|
335
|
+
validation: {
|
|
336
|
+
maxIterations: 3,
|
|
337
|
+
acceptanceThreshold: 95
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
costThresholds: {
|
|
341
|
+
'sponsor-call': 2,
|
|
342
|
+
'sprint-planning': 2,
|
|
343
|
+
'seed': 2
|
|
344
|
+
},
|
|
345
|
+
questionnaire: {
|
|
346
|
+
defaults: {
|
|
347
|
+
MISSION_STATEMENT: null,
|
|
348
|
+
TARGET_USERS: null,
|
|
349
|
+
INITIAL_SCOPE: null,
|
|
350
|
+
DEPLOYMENT_TARGET: null,
|
|
351
|
+
TECHNICAL_CONSIDERATIONS: null,
|
|
352
|
+
SECURITY_AND_COMPLIANCE_REQUIREMENTS: null
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
models: {
|
|
356
|
+
// Anthropic Claude models (prices per 1M tokens in USD)
|
|
357
|
+
// Source: https://www.anthropic.com/pricing
|
|
358
|
+
'claude-opus-4-6': {
|
|
359
|
+
provider: 'claude',
|
|
360
|
+
displayName: 'Claude Opus 4.6',
|
|
361
|
+
pricing: {
|
|
362
|
+
input: 5.00,
|
|
363
|
+
output: 25.00,
|
|
364
|
+
unit: 'million',
|
|
365
|
+
source: 'https://www.anthropic.com/pricing',
|
|
366
|
+
lastUpdated: '2026-02-24'
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
'claude-sonnet-4-6': {
|
|
370
|
+
provider: 'claude',
|
|
371
|
+
displayName: 'Claude Sonnet 4.6',
|
|
372
|
+
pricing: {
|
|
373
|
+
input: 3.00,
|
|
374
|
+
output: 15.00,
|
|
375
|
+
unit: 'million',
|
|
376
|
+
source: 'https://www.anthropic.com/pricing',
|
|
377
|
+
lastUpdated: '2026-02-24'
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
'claude-haiku-4-5-20251001': {
|
|
381
|
+
provider: 'claude',
|
|
382
|
+
displayName: 'Claude Haiku 4.5',
|
|
383
|
+
pricing: {
|
|
384
|
+
input: 1.00,
|
|
385
|
+
output: 5.00,
|
|
386
|
+
unit: 'million',
|
|
387
|
+
source: 'https://www.anthropic.com/pricing',
|
|
388
|
+
lastUpdated: '2026-02-24'
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
// Google Gemini models (prices per 1M tokens in USD)
|
|
393
|
+
// Source: https://ai.google.dev/pricing
|
|
394
|
+
'gemini-3.1-pro-preview': {
|
|
395
|
+
provider: 'gemini',
|
|
396
|
+
displayName: 'Gemini 3.1 Pro Preview',
|
|
397
|
+
pricing: {
|
|
398
|
+
input: 2.00,
|
|
399
|
+
output: 12.00,
|
|
400
|
+
unit: 'million',
|
|
401
|
+
source: 'https://ai.google.dev/pricing',
|
|
402
|
+
lastUpdated: '2026-03-05'
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
'gemini-3-flash-preview': {
|
|
406
|
+
provider: 'gemini',
|
|
407
|
+
displayName: 'Gemini 3 Flash Preview',
|
|
408
|
+
pricing: {
|
|
409
|
+
input: 0.50,
|
|
410
|
+
output: 3.00,
|
|
411
|
+
unit: 'million',
|
|
412
|
+
source: 'https://ai.google.dev/pricing',
|
|
413
|
+
lastUpdated: '2026-03-05'
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
'gemini-2.5-pro': {
|
|
417
|
+
provider: 'gemini',
|
|
418
|
+
displayName: 'Gemini 2.5 Pro',
|
|
419
|
+
pricing: {
|
|
420
|
+
input: 1.25,
|
|
421
|
+
output: 10.00,
|
|
422
|
+
unit: 'million',
|
|
423
|
+
source: 'https://ai.google.dev/pricing',
|
|
424
|
+
lastUpdated: '2026-02-24'
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
'gemini-2.5-flash': {
|
|
428
|
+
provider: 'gemini',
|
|
429
|
+
displayName: 'Gemini 2.5 Flash',
|
|
430
|
+
pricing: {
|
|
431
|
+
input: 0.30,
|
|
432
|
+
output: 2.50,
|
|
433
|
+
unit: 'million',
|
|
434
|
+
source: 'https://ai.google.dev/pricing',
|
|
435
|
+
lastUpdated: '2026-02-24'
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
'gemini-2.5-flash-lite': {
|
|
439
|
+
provider: 'gemini',
|
|
440
|
+
displayName: 'Gemini 2.5 Flash-Lite',
|
|
441
|
+
pricing: {
|
|
442
|
+
input: 0.10,
|
|
443
|
+
output: 0.40,
|
|
444
|
+
unit: 'million',
|
|
445
|
+
source: 'https://ai.google.dev/pricing',
|
|
446
|
+
lastUpdated: '2026-02-24'
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
// OpenAI models (prices per 1M tokens in USD)
|
|
451
|
+
// Source: https://openai.com/api/pricing
|
|
452
|
+
'gpt-5.2': {
|
|
453
|
+
provider: 'openai',
|
|
454
|
+
displayName: 'GPT-5.2',
|
|
455
|
+
pricing: {
|
|
456
|
+
input: 1.75,
|
|
457
|
+
output: 14.00,
|
|
458
|
+
unit: 'million',
|
|
459
|
+
source: 'https://openai.com/api/pricing',
|
|
460
|
+
lastUpdated: '2026-02-24'
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
'gpt-5.1': {
|
|
464
|
+
provider: 'openai',
|
|
465
|
+
displayName: 'GPT-5.1',
|
|
466
|
+
pricing: {
|
|
467
|
+
input: 1.25,
|
|
468
|
+
output: 10.00,
|
|
469
|
+
unit: 'million',
|
|
470
|
+
source: 'https://openai.com/api/pricing',
|
|
471
|
+
lastUpdated: '2026-02-24'
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
'gpt-5-mini': {
|
|
475
|
+
provider: 'openai',
|
|
476
|
+
displayName: 'GPT-5 mini',
|
|
477
|
+
pricing: {
|
|
478
|
+
input: 0.25,
|
|
479
|
+
output: 2.00,
|
|
480
|
+
unit: 'million',
|
|
481
|
+
source: 'https://openai.com/api/pricing',
|
|
482
|
+
lastUpdated: '2026-02-24'
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
'o4-mini': {
|
|
486
|
+
provider: 'openai',
|
|
487
|
+
displayName: 'o4-mini',
|
|
488
|
+
pricing: {
|
|
489
|
+
input: 1.10,
|
|
490
|
+
output: 4.40,
|
|
491
|
+
unit: 'million',
|
|
492
|
+
source: 'https://openai.com/api/pricing',
|
|
493
|
+
lastUpdated: '2026-02-24'
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
'o3': {
|
|
497
|
+
provider: 'openai',
|
|
498
|
+
displayName: 'o3',
|
|
499
|
+
pricing: {
|
|
500
|
+
input: 2.00,
|
|
501
|
+
output: 8.00,
|
|
502
|
+
unit: 'million',
|
|
503
|
+
source: 'https://openai.com/api/pricing',
|
|
504
|
+
lastUpdated: '2026-02-24'
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
'o3-mini': {
|
|
508
|
+
provider: 'openai',
|
|
509
|
+
displayName: 'o3-mini',
|
|
510
|
+
pricing: {
|
|
511
|
+
input: 0.50,
|
|
512
|
+
output: 2.00,
|
|
513
|
+
unit: 'million',
|
|
514
|
+
source: 'https://openai.com/api/pricing',
|
|
515
|
+
lastUpdated: '2026-02-24'
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
'gpt-5.2-codex': {
|
|
519
|
+
provider: 'openai',
|
|
520
|
+
displayName: 'GPT-5.2-Codex',
|
|
521
|
+
pricing: {
|
|
522
|
+
input: 1.75,
|
|
523
|
+
output: 14.00,
|
|
524
|
+
unit: 'million',
|
|
525
|
+
source: 'https://openai.com/api/pricing',
|
|
526
|
+
lastUpdated: '2026-02-24'
|
|
527
|
+
}
|
|
528
|
+
}
|
|
78
529
|
}
|
|
79
|
-
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
80
532
|
|
|
533
|
+
if (!this.hasAvcConfig()) {
|
|
534
|
+
// Create new config
|
|
81
535
|
fs.writeFileSync(
|
|
82
536
|
this.avcConfigPath,
|
|
83
537
|
JSON.stringify(defaultConfig, null, 2),
|
|
84
538
|
'utf8'
|
|
85
539
|
);
|
|
86
|
-
console.log('✓ Created .avc/avc.json configuration file');
|
|
87
540
|
return true;
|
|
88
541
|
}
|
|
89
|
-
|
|
90
|
-
|
|
542
|
+
|
|
543
|
+
// Config exists - check for merge
|
|
544
|
+
try {
|
|
545
|
+
const existingConfig = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
546
|
+
|
|
547
|
+
// Merge: add new keys, keep existing values
|
|
548
|
+
const mergedConfig = this.deepMerge(existingConfig, defaultConfig);
|
|
549
|
+
|
|
550
|
+
// Upgrade null cost thresholds to default (2 USD)
|
|
551
|
+
if (mergedConfig.settings?.costThresholds) {
|
|
552
|
+
for (const ceremony of ['sponsor-call', 'sprint-planning', 'seed']) {
|
|
553
|
+
if (mergedConfig.settings.costThresholds[ceremony] === null) {
|
|
554
|
+
mergedConfig.settings.costThresholds[ceremony] = 2;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Update avcVersion to track CLI version
|
|
560
|
+
mergedConfig.avcVersion = this.getAvcVersion();
|
|
561
|
+
mergedConfig.updated = new Date().toISOString();
|
|
562
|
+
|
|
563
|
+
// Check if anything changed
|
|
564
|
+
const existingJson = JSON.stringify(existingConfig, null, 2);
|
|
565
|
+
const mergedJson = JSON.stringify(mergedConfig, null, 2);
|
|
566
|
+
|
|
567
|
+
if (existingJson !== mergedJson) {
|
|
568
|
+
fs.writeFileSync(this.avcConfigPath, mergedJson, 'utf8');
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return false;
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error(`Warning: Could not merge avc.json: ${error.message}`);
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
91
577
|
}
|
|
92
578
|
|
|
93
579
|
/**
|
|
@@ -104,22 +590,70 @@ class ProjectInitiator {
|
|
|
104
590
|
|
|
105
591
|
/**
|
|
106
592
|
* Create .env file for API keys
|
|
593
|
+
* If .env exists, check and add any missing API key variables
|
|
107
594
|
*/
|
|
108
595
|
createEnvFile() {
|
|
109
596
|
const envPath = path.join(this.projectRoot, '.env');
|
|
110
597
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
ANTHROPIC_API_KEY
|
|
598
|
+
// Define required API key variables with metadata
|
|
599
|
+
const requiredApiKeys = [
|
|
600
|
+
{
|
|
601
|
+
key: 'ANTHROPIC_API_KEY',
|
|
602
|
+
comment: 'Anthropic API Key for AI-powered Sponsor Call ceremony',
|
|
603
|
+
url: 'https://console.anthropic.com/settings/keys'
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
key: 'GEMINI_API_KEY',
|
|
607
|
+
comment: 'Google Gemini API Key (alternative LLM provider)',
|
|
608
|
+
url: 'https://aistudio.google.com/app/apikey'
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
key: 'OPENAI_API_KEY',
|
|
612
|
+
comment: 'OpenAI API Key (alternative LLM provider)',
|
|
613
|
+
url: 'https://platform.openai.com/api-keys'
|
|
614
|
+
}
|
|
615
|
+
];
|
|
115
616
|
|
|
116
|
-
|
|
117
|
-
|
|
617
|
+
if (!fs.existsSync(envPath)) {
|
|
618
|
+
// Create new .env file with all API keys
|
|
619
|
+
let envContent = '';
|
|
620
|
+
requiredApiKeys.forEach(({ key, comment, url }, index) => {
|
|
621
|
+
if (index > 0) envContent += '\n';
|
|
622
|
+
envContent += `# ${comment}\n`;
|
|
623
|
+
envContent += `# Get your key at: ${url}\n`;
|
|
624
|
+
envContent += `${key}=\n`;
|
|
625
|
+
});
|
|
118
626
|
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
119
|
-
console.log('✓ Created .env file for API keys');
|
|
120
627
|
return true;
|
|
121
628
|
}
|
|
122
|
-
|
|
629
|
+
|
|
630
|
+
// .env exists - check for missing API keys
|
|
631
|
+
const existingContent = fs.readFileSync(envPath, 'utf8');
|
|
632
|
+
const missingKeys = [];
|
|
633
|
+
|
|
634
|
+
// Check which API keys are missing
|
|
635
|
+
requiredApiKeys.forEach(({ key }) => {
|
|
636
|
+
const keyPattern = new RegExp(`^${key}=`, 'm');
|
|
637
|
+
if (!keyPattern.test(existingContent)) {
|
|
638
|
+
missingKeys.push(key);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
if (missingKeys.length > 0) {
|
|
643
|
+
// Add missing API keys to .env file
|
|
644
|
+
let appendContent = '\n';
|
|
645
|
+
requiredApiKeys.forEach(({ key, comment, url }) => {
|
|
646
|
+
if (missingKeys.includes(key)) {
|
|
647
|
+
appendContent += `\n# ${comment}\n`;
|
|
648
|
+
appendContent += `# Get your key at: ${url}\n`;
|
|
649
|
+
appendContent += `${key}=\n`;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
fs.appendFileSync(envPath, appendContent, 'utf8');
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
|
|
123
657
|
return false;
|
|
124
658
|
}
|
|
125
659
|
|
|
@@ -138,34 +672,88 @@ ANTHROPIC_API_KEY=
|
|
|
138
672
|
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
139
673
|
}
|
|
140
674
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
675
|
+
// Items to add to gitignore
|
|
676
|
+
const itemsToIgnore = [
|
|
677
|
+
{ pattern: '.env', comment: 'Environment variables' },
|
|
678
|
+
{ pattern: '.avc/documentation/.vitepress/dist', comment: 'VitePress build output' },
|
|
679
|
+
{ pattern: '.avc/documentation/.vitepress/cache', comment: 'VitePress cache' },
|
|
680
|
+
{ pattern: '.avc/logs', comment: 'Command execution logs' },
|
|
681
|
+
{ pattern: '.avc/token-history.json', comment: 'Token usage tracking' },
|
|
682
|
+
{ pattern: '.avc/ceremonies-history.json', comment: 'Ceremony execution history' }
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
let newContent = gitignoreContent;
|
|
686
|
+
let addedItems = [];
|
|
687
|
+
|
|
688
|
+
for (const item of itemsToIgnore) {
|
|
689
|
+
if (!newContent.includes(item.pattern)) {
|
|
690
|
+
if (!newContent.endsWith('\n') && newContent.length > 0) {
|
|
691
|
+
newContent += '\n';
|
|
692
|
+
}
|
|
693
|
+
if (!newContent.includes(`# ${item.comment}`)) {
|
|
694
|
+
newContent += `\n# ${item.comment}\n`;
|
|
695
|
+
}
|
|
696
|
+
newContent += `${item.pattern}\n`;
|
|
697
|
+
addedItems.push(item.pattern);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (addedItems.length > 0) {
|
|
702
|
+
fs.writeFileSync(gitignorePath, newContent, 'utf8');
|
|
145
703
|
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Create VitePress documentation structure (folders and config files)
|
|
708
|
+
* Note: VitePress is bundled with AVC, no need to modify user's package.json
|
|
709
|
+
*/
|
|
710
|
+
createVitePressStructure() {
|
|
711
|
+
const docsDir = path.join(this.avcDir, 'documentation');
|
|
712
|
+
const vitepressDir = path.join(docsDir, '.vitepress');
|
|
713
|
+
const publicDir = path.join(docsDir, 'public');
|
|
146
714
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
715
|
+
// Create directory structure
|
|
716
|
+
if (!fs.existsSync(vitepressDir)) {
|
|
717
|
+
fs.mkdirSync(vitepressDir, { recursive: true });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!fs.existsSync(publicDir)) {
|
|
721
|
+
fs.mkdirSync(publicDir, { recursive: true });
|
|
722
|
+
}
|
|
151
723
|
|
|
152
|
-
|
|
153
|
-
|
|
724
|
+
// Create VitePress config
|
|
725
|
+
const configPath = path.join(vitepressDir, 'config.mts');
|
|
726
|
+
if (!fs.existsSync(configPath)) {
|
|
727
|
+
const templatePath = path.join(__dirname, 'templates/vitepress-config.mts.template');
|
|
728
|
+
let configContent = fs.readFileSync(templatePath, 'utf8');
|
|
729
|
+
configContent = configContent.replace('{{PROJECT_NAME}}', this.getProjectName());
|
|
730
|
+
fs.writeFileSync(configPath, configContent, 'utf8');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Create initial index.md
|
|
734
|
+
const indexPath = path.join(docsDir, 'index.md');
|
|
735
|
+
if (!fs.existsSync(indexPath)) {
|
|
736
|
+
const indexContent = `# ${this.getProjectName()}
|
|
737
|
+
|
|
738
|
+
Documentation for this project will be generated automatically once the project is defined via the Sponsor Call ceremony. Use the **Start Project** button in the [Kanban board](http://localhost:4174) to get started.
|
|
739
|
+
`;
|
|
740
|
+
fs.writeFileSync(indexPath, indexContent, 'utf8');
|
|
741
|
+
}
|
|
154
742
|
}
|
|
155
743
|
|
|
156
744
|
/**
|
|
157
|
-
* Check if there's an incomplete
|
|
745
|
+
* Check if there's an incomplete ceremony in progress
|
|
158
746
|
*/
|
|
159
|
-
|
|
160
|
-
return fs.existsSync(
|
|
747
|
+
hasIncompleteProgress(progressPath) {
|
|
748
|
+
return fs.existsSync(progressPath);
|
|
161
749
|
}
|
|
162
750
|
|
|
163
751
|
/**
|
|
164
752
|
* Read progress from file
|
|
165
753
|
*/
|
|
166
|
-
readProgress() {
|
|
754
|
+
readProgress(progressPath) {
|
|
167
755
|
try {
|
|
168
|
-
const content = fs.readFileSync(
|
|
756
|
+
const content = fs.readFileSync(progressPath, 'utf8');
|
|
169
757
|
return JSON.parse(content);
|
|
170
758
|
} catch (error) {
|
|
171
759
|
return null;
|
|
@@ -175,29 +763,261 @@ ANTHROPIC_API_KEY=
|
|
|
175
763
|
/**
|
|
176
764
|
* Write progress to file
|
|
177
765
|
*/
|
|
178
|
-
writeProgress(progress) {
|
|
766
|
+
writeProgress(progress, progressPath) {
|
|
179
767
|
if (!fs.existsSync(this.avcDir)) {
|
|
180
768
|
fs.mkdirSync(this.avcDir, { recursive: true });
|
|
181
769
|
}
|
|
182
|
-
fs.writeFileSync(
|
|
770
|
+
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf8');
|
|
183
771
|
}
|
|
184
772
|
|
|
185
773
|
/**
|
|
186
|
-
* Clear progress file (
|
|
774
|
+
* Clear progress file (ceremony completed successfully)
|
|
187
775
|
*/
|
|
188
|
-
clearProgress() {
|
|
189
|
-
if (fs.existsSync(
|
|
190
|
-
fs.unlinkSync(
|
|
776
|
+
clearProgress(progressPath) {
|
|
777
|
+
if (fs.existsSync(progressPath)) {
|
|
778
|
+
fs.unlinkSync(progressPath);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Parse and simplify API error messages for better UX
|
|
785
|
+
* @param {string|object} error - Error from API call
|
|
786
|
+
* @returns {string} - Human-readable error message
|
|
787
|
+
*/
|
|
788
|
+
parseApiError(error) {
|
|
789
|
+
let errorStr = typeof error === 'string' ? error : JSON.stringify(error);
|
|
790
|
+
|
|
791
|
+
// Try to parse as JSON to extract meaningful information
|
|
792
|
+
let errorObj = null;
|
|
793
|
+
try {
|
|
794
|
+
errorObj = typeof error === 'object' ? error : JSON.parse(error);
|
|
795
|
+
} catch (e) {
|
|
796
|
+
// Not JSON, use as-is
|
|
191
797
|
}
|
|
798
|
+
|
|
799
|
+
// Check for common error patterns
|
|
800
|
+
if (errorStr.includes('quota') || errorStr.includes('RESOURCE_EXHAUSTED')) {
|
|
801
|
+
return 'API quota exceeded. You have reached your free tier limit.\n\n Please check your usage at the provider dashboard or upgrade your plan.';
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (errorStr.includes('rate limit') || errorStr.includes('429')) {
|
|
805
|
+
const retryMatch = errorStr.match(/retry.*?(\d+)\.?\d*s/i);
|
|
806
|
+
const retryTime = retryMatch ? ` Try again in ${Math.ceil(parseFloat(retryMatch[1]))} seconds.` : '';
|
|
807
|
+
return `Rate limit exceeded.${retryTime}\n\n Please wait before making more requests.`;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (errorStr.includes('401') || errorStr.includes('authentication') || errorStr.includes('unauthorized')) {
|
|
811
|
+
return 'Invalid API key or authentication failed.\n\n Please verify your API key is correct.';
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (errorStr.includes('403') || errorStr.includes('forbidden')) {
|
|
815
|
+
return 'Access forbidden. Your API key may not have permission for this operation.\n\n Check your API key permissions or contact support.';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (errorStr.includes('404') || errorStr.includes('not found')) {
|
|
819
|
+
return 'API endpoint not found. The model or API version may not be available.\n\n Check that you\'re using a valid model name.';
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (errorStr.includes('timeout') || errorStr.includes('ETIMEDOUT')) {
|
|
823
|
+
return 'Request timed out.\n\n Check your internet connection and try again.';
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (errorStr.includes('ENOTFOUND') || errorStr.includes('DNS')) {
|
|
827
|
+
return 'Network error: Could not reach API server.\n\n Check your internet connection.';
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Extract just the error message if it's an object with a message field
|
|
831
|
+
if (errorObj) {
|
|
832
|
+
if (errorObj.error?.message) {
|
|
833
|
+
// Take first line or first 150 characters of the message
|
|
834
|
+
const msg = errorObj.error.message.split('\n')[0];
|
|
835
|
+
return msg.length > 150 ? msg.substring(0, 150) + '...' : msg;
|
|
836
|
+
}
|
|
837
|
+
if (errorObj.message) {
|
|
838
|
+
const msg = errorObj.message.split('\n')[0];
|
|
839
|
+
return msg.length > 150 ? msg.substring(0, 150) + '...' : msg;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Fallback: truncate the error if it's too long
|
|
844
|
+
if (errorStr.length > 200) {
|
|
845
|
+
return errorStr.substring(0, 200) + '...\n\n (Full error logged to console)';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return errorStr;
|
|
192
849
|
}
|
|
193
850
|
|
|
851
|
+
/**
|
|
852
|
+
* Validate that the configured provider's API key is present and working
|
|
853
|
+
*/
|
|
854
|
+
async validateProviderApiKey() {
|
|
855
|
+
// Import LLMProvider dynamically to avoid circular dependencies
|
|
856
|
+
const { LLMProvider } = await import('./llm-provider.js');
|
|
857
|
+
|
|
858
|
+
// Check if config file exists
|
|
859
|
+
if (!fs.existsSync(this.avcConfigPath)) {
|
|
860
|
+
return {
|
|
861
|
+
valid: false,
|
|
862
|
+
message: 'Configuration file not found at .avc/avc.json.\n Please run /init first to set up your project.'
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Read ceremony config
|
|
867
|
+
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
868
|
+
const ceremony = config.settings?.ceremonies?.[0];
|
|
869
|
+
|
|
870
|
+
if (!ceremony) {
|
|
871
|
+
return {
|
|
872
|
+
valid: false,
|
|
873
|
+
message: 'No ceremonies configured in .avc/avc.json.\n Please check your configuration.'
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const mainProvider = ceremony.provider || 'claude';
|
|
878
|
+
const mainModel = ceremony.defaultModel || 'claude-sonnet-4-6';
|
|
879
|
+
|
|
880
|
+
// Check validation provider if validation is enabled
|
|
881
|
+
const validationEnabled = ceremony.validation?.enabled !== false;
|
|
882
|
+
const validationProvider = ceremony.validation?.provider || null;
|
|
883
|
+
const validationModel = ceremony.validation?.model || null;
|
|
884
|
+
|
|
885
|
+
const envVarMap = {
|
|
886
|
+
'claude': 'ANTHROPIC_API_KEY',
|
|
887
|
+
'gemini': 'GEMINI_API_KEY',
|
|
888
|
+
'openai': 'OPENAI_API_KEY'
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const urlMap = {
|
|
892
|
+
'claude': 'https://console.anthropic.com/settings/keys',
|
|
893
|
+
'gemini': 'https://aistudio.google.com/app/apikey',
|
|
894
|
+
'openai': 'https://platform.openai.com/api-keys'
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// Validate main provider
|
|
898
|
+
const mainEnvVar = envVarMap[mainProvider];
|
|
899
|
+
if (!mainEnvVar) {
|
|
900
|
+
return {
|
|
901
|
+
valid: false,
|
|
902
|
+
message: `Unknown provider "${mainProvider}".\n Supported providers: claude, gemini, openai`
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (!process.env[mainEnvVar]) {
|
|
907
|
+
return {
|
|
908
|
+
valid: false,
|
|
909
|
+
message: `${mainEnvVar} not found in .env file.\n\n Steps to fix:\n 1. Open .env file in the current directory\n 2. Add your API key: ${mainEnvVar}=your-key-here\n 3. Save the file and run /sponsor-call again\n\n Get your API key:\n • ${urlMap[mainProvider]}`
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Test the API key with a minimal call
|
|
914
|
+
let result;
|
|
915
|
+
try {
|
|
916
|
+
result = await LLMProvider.validate(mainProvider, mainModel);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
const parsedError = this.parseApiError(error.message || error);
|
|
919
|
+
return {
|
|
920
|
+
valid: false,
|
|
921
|
+
message: `${mainEnvVar} validation failed.\n\n ${parsedError}\n\n Get a new API key if needed:\n • ${urlMap[mainProvider]}`
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (!result.valid) {
|
|
926
|
+
const parsedError = this.parseApiError(result.error || 'Unknown error');
|
|
927
|
+
return {
|
|
928
|
+
valid: false,
|
|
929
|
+
message: `${mainEnvVar} is set but API call failed.\n\n ${parsedError}\n\n Get a new API key if needed:\n • ${urlMap[mainProvider]}`
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Validate validation provider if enabled and different from main
|
|
934
|
+
if (validationEnabled && validationProvider && validationProvider !== mainProvider) {
|
|
935
|
+
const validationEnvVar = envVarMap[validationProvider];
|
|
936
|
+
|
|
937
|
+
if (!validationEnvVar) {
|
|
938
|
+
return {
|
|
939
|
+
valid: false,
|
|
940
|
+
message: `Unknown validation provider "${validationProvider}".\n Supported providers: claude, gemini, openai`
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (!process.env[validationEnvVar]) {
|
|
945
|
+
// Enhanced error message with 3 options
|
|
946
|
+
return {
|
|
947
|
+
valid: false,
|
|
948
|
+
validationProviderMissing: true,
|
|
949
|
+
ceremonyConfig: {
|
|
950
|
+
mainProvider,
|
|
951
|
+
mainModel,
|
|
952
|
+
validationProvider,
|
|
953
|
+
validationModel
|
|
954
|
+
},
|
|
955
|
+
message: `Validation Provider API Key Missing\n\nYour ceremony is configured to use:\n • Generation: ${mainProvider} (${mainModel}) ✓\n • Validation: ${validationProvider} (${validationModel}) ✗ (${validationEnvVar} not found)\n\nYou have 3 options:\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nOption 1: Add the missing API key\n\n 1. Get API key: ${urlMap[validationProvider]}\n 2. Add to .env file: ${validationEnvVar}=your-key-here\n 3. Run /sponsor-call again\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nOption 2: Disable validation (faster, lower quality)\n\n 1. Edit .avc/avc.json\n 2. Find "sponsor-call" ceremony config\n 3. Set:\n "validation": {\n "enabled": false\n }\n 4. Run /sponsor-call again\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nOption 3: Use same provider for validation (simpler setup)\n\n 1. Edit .avc/avc.json\n 2. Find "sponsor-call" ceremony config\n 3. Change:\n "validation": {\n "enabled": true,\n "provider": "${mainProvider}",\n "model": "${mainModel}"\n }\n 4. Run /sponsor-call again\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
result = await LLMProvider.validate(validationProvider, validationModel);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
const parsedError = this.parseApiError(error.message || error);
|
|
963
|
+
return {
|
|
964
|
+
valid: false,
|
|
965
|
+
message: `${validationEnvVar} validation failed.\n\n ${parsedError}\n\n Get a new API key if needed:\n • ${urlMap[validationProvider]}`
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (!result.valid) {
|
|
970
|
+
const parsedError = this.parseApiError(result.error || 'Unknown error');
|
|
971
|
+
return {
|
|
972
|
+
valid: false,
|
|
973
|
+
message: `${validationEnvVar} is set but API call failed.\n\n ${parsedError}\n\n Get a new API key if needed:\n • ${urlMap[validationProvider]}`
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return { valid: true };
|
|
980
|
+
}
|
|
194
981
|
|
|
195
982
|
/**
|
|
196
983
|
* Generate project document via Sponsor Call ceremony
|
|
197
984
|
*/
|
|
198
|
-
async generateProjectDocument(progress = null) {
|
|
199
|
-
const processor = new TemplateProcessor(this.
|
|
200
|
-
|
|
985
|
+
async generateProjectDocument(progress = null, progressPath = null, nonInteractive = false, progressCallback = null, options = {}) {
|
|
986
|
+
const processor = new TemplateProcessor('sponsor-call', progressPath || this.sponsorCallProgressPath, nonInteractive, options);
|
|
987
|
+
|
|
988
|
+
// Set before await so processor is reachable even if processTemplate throws
|
|
989
|
+
this._lastTemplateProcessor = processor;
|
|
990
|
+
|
|
991
|
+
if (progressCallback) {
|
|
992
|
+
processor.setProgressCallback(progressCallback);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return await processor.processTemplate(progress);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Get token usage from last template processor execution
|
|
1000
|
+
* @returns {Object|null} Token usage object or null
|
|
1001
|
+
*/
|
|
1002
|
+
getLastTokenUsage() {
|
|
1003
|
+
if (this._lastTemplateProcessor) {
|
|
1004
|
+
return this._lastTemplateProcessor.getLastTokenUsage();
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Read ceremony configuration from avc.json
|
|
1011
|
+
* @param {string} ceremonyName - Name of the ceremony
|
|
1012
|
+
* @returns {Object|null} Ceremony config or null
|
|
1013
|
+
*/
|
|
1014
|
+
readCeremonyConfig(ceremonyName) {
|
|
1015
|
+
try {
|
|
1016
|
+
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
1017
|
+
return config.settings?.ceremonies?.find(c => c.name === ceremonyName);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
201
1021
|
}
|
|
202
1022
|
|
|
203
1023
|
/**
|
|
@@ -208,46 +1028,539 @@ ANTHROPIC_API_KEY=
|
|
|
208
1028
|
}
|
|
209
1029
|
|
|
210
1030
|
/**
|
|
211
|
-
* Initialize the AVC project
|
|
1031
|
+
* Initialize the AVC project structure (no API keys required)
|
|
1032
|
+
* Creates .avc folder, avc.json config, .env file, and gitignore entry
|
|
212
1033
|
*/
|
|
213
1034
|
async init() {
|
|
214
|
-
|
|
215
|
-
|
|
1035
|
+
const startTime = Date.now();
|
|
1036
|
+
fileLog('INFO', 'init() started', { projectRoot: this.projectRoot });
|
|
216
1037
|
|
|
217
|
-
|
|
1038
|
+
if (this.isAvcProject()) {
|
|
1039
|
+
// Project already initialized
|
|
1040
|
+
fileLog('INFO', 'Project already initialized — skipping structure creation');
|
|
1041
|
+
sendOutput('Project already initialized.');
|
|
1042
|
+
sendOutput('');
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
218
1045
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
progress = this.readProgress();
|
|
1046
|
+
fileLog('INFO', 'New project — creating structure');
|
|
1047
|
+
fileLog('DEBUG', 'Creating components: .avc/, src/, worktrees/, avc.json, .env, .gitignore, VitePress');
|
|
222
1048
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1049
|
+
// Suppress all console output during initialization
|
|
1050
|
+
const originalLog = console.log;
|
|
1051
|
+
console.log = () => { };
|
|
1052
|
+
|
|
1053
|
+
let initError = null;
|
|
1054
|
+
try {
|
|
1055
|
+
// Create project structure silently
|
|
1056
|
+
this.createAvcFolder();
|
|
1057
|
+
this.createSrcFolder();
|
|
1058
|
+
this.createWorktreesFolder();
|
|
1059
|
+
this.createAvcConfig();
|
|
1060
|
+
this.createEnvFile();
|
|
1061
|
+
this.addToGitignore();
|
|
1062
|
+
this.createVitePressStructure();
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
initError = err;
|
|
1065
|
+
} finally {
|
|
1066
|
+
console.log = originalLog;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (initError) {
|
|
1070
|
+
fileLog('ERROR', 'Structure creation failed', { error: initError.message, stack: initError.stack });
|
|
1071
|
+
throw initError;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const duration = Date.now() - startTime;
|
|
1075
|
+
fileLog('INFO', 'Structure creation complete', {
|
|
1076
|
+
duration: `${duration}ms`,
|
|
1077
|
+
avcFolder: this.hasAvcFolder(),
|
|
1078
|
+
srcFolder: this.hasSrcFolder(),
|
|
1079
|
+
worktreesFolder: this.hasWorktreesFolder(),
|
|
1080
|
+
avcConfig: this.hasAvcConfig(),
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
sendOutput('Project initialized — set your API keys in .env (Anthropic, Gemini, OpenAI) then open the Kanban board to get started.');
|
|
1084
|
+
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Configure models command
|
|
1090
|
+
* Shows current model configuration and offers interactive editing
|
|
1091
|
+
*/
|
|
1092
|
+
async models() {
|
|
1093
|
+
sendOutput('Model Configuration\n');
|
|
1094
|
+
sendOutput('Ceremonies are structured workflows (sponsor-call, sprint-planning, seed) that guide your project through key decisions. Each ceremony runs multiple stages in sequence, and you can assign a different LLM model to each stage.\n');
|
|
1095
|
+
|
|
1096
|
+
// Check if project is initialized
|
|
1097
|
+
if (!this.isAvcProject()) {
|
|
1098
|
+
sendError(MESSAGES.PROJECT_NOT_INITIALIZED.error);
|
|
1099
|
+
sendOutput('');
|
|
1100
|
+
sendOutput(MESSAGES.PROJECT_NOT_INITIALIZED.help);
|
|
1101
|
+
sendOutput('');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Use the shared configuration method
|
|
1106
|
+
return this.configureModelsInteractively();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Interactive model configuration flow
|
|
1111
|
+
* Shared by both /init and /models commands
|
|
1112
|
+
*/
|
|
1113
|
+
configureModelsInteractively() {
|
|
1114
|
+
fileLog('INFO', 'configureModels() called', { projectRoot: this.projectRoot });
|
|
1115
|
+
|
|
1116
|
+
const configurator = new ModelConfigurator(this.projectRoot);
|
|
1117
|
+
|
|
1118
|
+
// Detect available providers (used for model indicators)
|
|
1119
|
+
configurator.availableProviders = configurator.detectAvailableProviders();
|
|
1120
|
+
configurator.readConfig();
|
|
1121
|
+
|
|
1122
|
+
fileLog('DEBUG', 'Model configurator loaded', {
|
|
1123
|
+
availableProviders: configurator.availableProviders,
|
|
1124
|
+
configPath: this.avcConfigPath,
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// Show current configuration
|
|
1128
|
+
const ceremonies = configurator.getCeremonies();
|
|
1129
|
+
fileLog('INFO', 'Ceremony model configs', {
|
|
1130
|
+
count: ceremonies.length,
|
|
1131
|
+
names: ceremonies.map(c => c.name),
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
ceremonies.forEach(c => {
|
|
1135
|
+
const ceremonyUrl = `https://agilevibecoding.org/ceremonies/${c.name}.html`;
|
|
1136
|
+
|
|
1137
|
+
const hasMainKey = configurator.availableProviders.includes(c.mainProvider);
|
|
1138
|
+
const stageDetails = {};
|
|
1139
|
+
Object.keys(c.stages).forEach(stageName => {
|
|
1140
|
+
const stage = c.stages[stageName];
|
|
1141
|
+
stageDetails[stageName] = {
|
|
1142
|
+
model: stage.model,
|
|
1143
|
+
provider: stage.provider,
|
|
1144
|
+
hasApiKey: configurator.availableProviders.includes(stage.provider),
|
|
1145
|
+
};
|
|
1146
|
+
});
|
|
1147
|
+
fileLog('DEBUG', `Ceremony: ${c.name}`, {
|
|
1148
|
+
mainModel: c.mainModel,
|
|
1149
|
+
mainProvider: c.mainProvider,
|
|
1150
|
+
hasMainKey,
|
|
1151
|
+
validationModel: c.validationModel || null,
|
|
1152
|
+
validationProvider: c.validationProvider || null,
|
|
1153
|
+
hasValidationKey: c.validationProvider ? configurator.availableProviders.includes(c.validationProvider) : null,
|
|
1154
|
+
stages: stageDetails,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
sendOutput(boldCyan(c.name));
|
|
1158
|
+
sendOutput(`${yellow('default')}: ${green(c.mainModel)} (${c.mainProvider})`);
|
|
1159
|
+
if (c.validationProvider) {
|
|
1160
|
+
const hasValidationKey = configurator.availableProviders.includes(c.validationProvider);
|
|
1161
|
+
const keyWarning = hasValidationKey ? '' : ' [no API key]';
|
|
1162
|
+
sendOutput(`${yellow('validation')}: ${green(c.validationModel)} (${c.validationProvider})${keyWarning}`);
|
|
229
1163
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
1164
|
+
Object.keys(c.stages).forEach(stageName => {
|
|
1165
|
+
const stage = c.stages[stageName];
|
|
1166
|
+
const hasStageKey = configurator.availableProviders.includes(stage.provider);
|
|
1167
|
+
const keyWarning = hasStageKey ? '' : ' [no API key]';
|
|
1168
|
+
sendOutput(`${yellow(stageName)}: ${green(stage.model)} (${stage.provider})${keyWarning}`);
|
|
1169
|
+
});
|
|
1170
|
+
sendOutput('');
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
fileLog('INFO', 'configureModels() complete');
|
|
1174
|
+
// Return configurator for REPL to use
|
|
1175
|
+
return {
|
|
1176
|
+
shouldConfigure: true,
|
|
1177
|
+
configurator,
|
|
1178
|
+
ceremonies: ceremonies.map(c => c.name) // List of ceremony names for selection
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Run Sponsor Call ceremony with pre-filled answers from REPL questionnaire
|
|
1184
|
+
* Used when all answers are collected via REPL UI
|
|
1185
|
+
*/
|
|
1186
|
+
async sponsorCallWithAnswers(answers, progressCallback = null, options = {}) {
|
|
1187
|
+
const startTime = Date.now();
|
|
1188
|
+
fileLog('INFO', 'sponsorCallWithAnswers() called', {
|
|
1189
|
+
answerKeys: Object.keys(answers || {}),
|
|
1190
|
+
answeredCount: Object.values(answers || {}).filter(v => v !== null && v !== '').length,
|
|
1191
|
+
hasProgressCallback: !!progressCallback,
|
|
1192
|
+
projectRoot: this.projectRoot,
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// Remove initial ceremony banner - will be shown in summary
|
|
1196
|
+
|
|
1197
|
+
// Check if project is initialized
|
|
1198
|
+
if (!this.isAvcProject()) {
|
|
1199
|
+
fileLog('ERROR', 'Project not initialized — aborting sponsor call');
|
|
1200
|
+
sendError(MESSAGES.PROJECT_NOT_INITIALIZED.error);
|
|
234
1201
|
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const progressPath = this.sponsorCallProgressPath;
|
|
1205
|
+
|
|
1206
|
+
// Initialize ceremony history
|
|
1207
|
+
const { CeremonyHistory } = await import('./ceremony-history.js');
|
|
1208
|
+
const history = new CeremonyHistory(this.avcDir);
|
|
1209
|
+
history.init();
|
|
1210
|
+
|
|
1211
|
+
// Get or create execution record
|
|
1212
|
+
let executionId;
|
|
1213
|
+
const lastExecution = history.getLastExecution('sponsor-call');
|
|
1214
|
+
|
|
1215
|
+
if (lastExecution && lastExecution.status === 'in-progress' && lastExecution.stage === 'llm-generation') {
|
|
1216
|
+
// Resume existing execution (from REPL flow)
|
|
1217
|
+
executionId = lastExecution.id;
|
|
235
1218
|
} else {
|
|
236
|
-
//
|
|
237
|
-
|
|
1219
|
+
// This shouldn't normally happen if called from REPL, but handle it
|
|
1220
|
+
executionId = history.startExecution('sponsor-call', 'llm-generation');
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Count answers provided (for logging)
|
|
1224
|
+
const answeredCount = Object.values(answers).filter(v => v !== null && v !== '').length;
|
|
1225
|
+
fileLog('DEBUG', 'Ceremony history state', {
|
|
1226
|
+
executionId,
|
|
1227
|
+
lastExecutionStatus: lastExecution?.status,
|
|
1228
|
+
lastExecutionStage: lastExecution?.stage,
|
|
1229
|
+
answeredCount,
|
|
1230
|
+
totalQuestions: Object.keys(answers).length,
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
// Validate API key before starting ceremony
|
|
1234
|
+
fileLog('INFO', 'Validating API key before ceremony start');
|
|
1235
|
+
const validationResult = await this.validateProviderApiKey();
|
|
1236
|
+
fileLog(validationResult.valid ? 'INFO' : 'ERROR', 'API key validation result', {
|
|
1237
|
+
valid: validationResult.valid,
|
|
1238
|
+
message: validationResult.message || 'OK',
|
|
1239
|
+
});
|
|
1240
|
+
if (!validationResult.valid) {
|
|
1241
|
+
// Mark execution as aborted
|
|
1242
|
+
history.completeExecution('sponsor-call', executionId, 'abrupt-termination', {
|
|
1243
|
+
answers,
|
|
1244
|
+
stage: 'llm-generation',
|
|
1245
|
+
error: 'API key validation failed'
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// Return error for REPL to display
|
|
1249
|
+
return {
|
|
1250
|
+
error: true,
|
|
1251
|
+
message: `API Key Validation Failed: ${validationResult.message}`
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Create progress with pre-filled answers
|
|
1256
|
+
const progress = {
|
|
1257
|
+
stage: 'questionnaire',
|
|
1258
|
+
totalQuestions: 5,
|
|
1259
|
+
answeredQuestions: 5,
|
|
1260
|
+
collectedValues: answers,
|
|
1261
|
+
lastUpdate: new Date().toISOString()
|
|
1262
|
+
};
|
|
1263
|
+
this.writeProgress(progress, progressPath);
|
|
1264
|
+
|
|
1265
|
+
try {
|
|
1266
|
+
// Generate project document with pre-filled answers
|
|
1267
|
+
const result = await this.generateProjectDocument(progress, progressPath, true, progressCallback, options);
|
|
1268
|
+
|
|
1269
|
+
fileLog('INFO', 'generateProjectDocument() complete', { resultKeys: result ? Object.keys(result) : [] });
|
|
1270
|
+
|
|
1271
|
+
// Notify progress during cleanup
|
|
1272
|
+
if (progressCallback) await progressCallback(null, 'Calculating token usage costs...');
|
|
1273
|
+
|
|
1274
|
+
// Get token usage from template processor
|
|
1275
|
+
const tokenUsage = this.getLastTokenUsage();
|
|
1276
|
+
|
|
1277
|
+
// Get model ID from ceremony config
|
|
1278
|
+
const ceremony = this.readCeremonyConfig('sponsor-call');
|
|
1279
|
+
const modelId = ceremony?.defaultModel || 'claude-sonnet-4-6';
|
|
1280
|
+
|
|
1281
|
+
// Calculate cost using token tracker
|
|
1282
|
+
const { TokenTracker } = await import('./token-tracker.js');
|
|
1283
|
+
const tracker = new TokenTracker(this.avcDir);
|
|
1284
|
+
const cost = tracker.calculateCost(
|
|
1285
|
+
tokenUsage?.inputTokens || 0,
|
|
1286
|
+
tokenUsage?.outputTokens || 0,
|
|
1287
|
+
modelId
|
|
1288
|
+
);
|
|
1289
|
+
|
|
1290
|
+
// Mark execution as completed with metadata
|
|
1291
|
+
if (progressCallback) await progressCallback(null, 'Saving ceremony history...');
|
|
1292
|
+
history.completeExecution('sponsor-call', executionId, 'success', {
|
|
1293
|
+
answers,
|
|
1294
|
+
filesGenerated: [
|
|
1295
|
+
path.join(this.avcDir, 'project/doc.md')
|
|
1296
|
+
],
|
|
1297
|
+
tokenUsage: {
|
|
1298
|
+
input: tokenUsage?.inputTokens || 0,
|
|
1299
|
+
output: tokenUsage?.outputTokens || 0,
|
|
1300
|
+
total: tokenUsage?.totalTokens || 0
|
|
1301
|
+
},
|
|
1302
|
+
model: modelId,
|
|
1303
|
+
cost: cost,
|
|
1304
|
+
stage: 'completed'
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// Mark progress as completed and clean up
|
|
1308
|
+
if (progressCallback) await progressCallback(null, 'Finalizing ceremony...');
|
|
1309
|
+
progress.stage = 'completed';
|
|
1310
|
+
progress.lastUpdate = new Date().toISOString();
|
|
1311
|
+
this.writeProgress(progress, progressPath);
|
|
1312
|
+
this.clearProgress(progressPath);
|
|
1313
|
+
|
|
1314
|
+
// Emit a final main progress message so the UI log clearly shows completion
|
|
1315
|
+
if (progressCallback) await progressCallback('✓ Documentation generated successfully!');
|
|
1316
|
+
|
|
1317
|
+
fileLog('INFO', 'sponsorCallWithAnswers() complete', {
|
|
1318
|
+
duration: `${Date.now() - startTime}ms`,
|
|
1319
|
+
outputPath: result?.outputPath,
|
|
1320
|
+
tokenInput: result?.tokenUsage?.input,
|
|
1321
|
+
tokenOutput: result?.tokenUsage?.output,
|
|
1322
|
+
estimatedCost: result?.cost?.total,
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
// Persist answers and generate Q&A documentation page (non-fatal)
|
|
1326
|
+
this.saveProjectBriefAnswers(answers);
|
|
1327
|
+
|
|
1328
|
+
// Return result for display in REPL
|
|
1329
|
+
return result;
|
|
1330
|
+
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
const isCancelled = error.message === 'CEREMONY_CANCELLED';
|
|
1333
|
+
|
|
1334
|
+
// Save any tokens spent before cancellation/error
|
|
1335
|
+
try {
|
|
1336
|
+
this._lastTemplateProcessor?.saveCurrentTokenTracking();
|
|
1337
|
+
} catch (trackErr) {
|
|
1338
|
+
fileLog('WARN', 'Could not save partial token tracking', { error: trackErr.message });
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (!isCancelled) {
|
|
1342
|
+
fileLog('ERROR', 'sponsorCallWithAnswers() failed', {
|
|
1343
|
+
error: error.message,
|
|
1344
|
+
stack: error.stack,
|
|
1345
|
+
duration: `${Date.now() - startTime}ms`,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Mark execution as aborted on error/cancel
|
|
1350
|
+
history.completeExecution('sponsor-call', executionId, 'abrupt-termination', {
|
|
1351
|
+
answers,
|
|
1352
|
+
stage: isCancelled ? 'cancelled' : 'llm-generation',
|
|
1353
|
+
error: error.message
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Display token usage statistics and costs
|
|
1362
|
+
*/
|
|
1363
|
+
async showTokenStats() {
|
|
1364
|
+
fileLog('INFO', 'showTokenStats() called', { avcDir: this.avcDir });
|
|
1365
|
+
|
|
1366
|
+
const { TokenTracker } = await import('./token-tracker.js');
|
|
1367
|
+
const tracker = new TokenTracker(this.avcDir);
|
|
1368
|
+
tracker.init();
|
|
1369
|
+
tracker.load();
|
|
1370
|
+
|
|
1371
|
+
const data = tracker.data;
|
|
1372
|
+
fileLog('DEBUG', 'Token history loaded', {
|
|
1373
|
+
version: data.version,
|
|
1374
|
+
lastUpdated: data.lastUpdated,
|
|
1375
|
+
ceremonyTypes: Object.keys(data).filter(k => !['version', 'lastUpdated', 'totals'].includes(k)),
|
|
1376
|
+
allTimeTotal: data.totals?.allTime?.total,
|
|
1377
|
+
allTimeExecutions: data.totals?.allTime?.executions,
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
const allTime = data.totals.allTime;
|
|
1381
|
+
const costStr = (allTime.cost && allTime.cost.total > 0) ? ` / $${allTime.cost.total.toFixed(4)}` : '';
|
|
1382
|
+
sendOutput(`All-time: ${allTime.total.toLocaleString()} tokens / ${allTime.executions} executions${costStr}`);
|
|
1383
|
+
|
|
1384
|
+
const ceremonyTypes = Object.keys(data).filter(k => !['version', 'lastUpdated', 'totals'].includes(k));
|
|
1385
|
+
for (const ceremonyType of ceremonyTypes) {
|
|
1386
|
+
const ceremony = data[ceremonyType];
|
|
1387
|
+
if (ceremony.allTime && ceremony.allTime.executions > 0) {
|
|
1388
|
+
const cCostStr = (ceremony.allTime.cost && ceremony.allTime.cost.total > 0) ? ` / $${ceremony.allTime.cost.total.toFixed(4)}` : '';
|
|
1389
|
+
sendIndented(`${ceremonyType}: ${ceremony.allTime.total.toLocaleString()} tokens / ${ceremony.allTime.executions} runs${cCostStr}`, 1);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
fileLog('INFO', 'showTokenStats() complete', {
|
|
1394
|
+
allTimeInput: data.totals?.allTime?.input,
|
|
1395
|
+
allTimeOutput: data.totals?.allTime?.output,
|
|
1396
|
+
allTimeTotal: data.totals?.allTime?.total,
|
|
1397
|
+
estimatedCost: data.totals?.allTime?.cost?.total,
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Run Sprint Planning ceremony to create/expand Epics and Stories
|
|
1403
|
+
*/
|
|
1404
|
+
async sprintPlanning() {
|
|
1405
|
+
const startTime = Date.now();
|
|
1406
|
+
fileLog('INFO', 'sprintPlanning() called', { projectRoot: this.projectRoot });
|
|
1407
|
+
|
|
1408
|
+
if (!this.isAvcProject()) {
|
|
1409
|
+
fileLog('ERROR', 'Project not initialized — aborting sprint planning');
|
|
1410
|
+
sendError('Project not initialized. Run /init first.');
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
fileLog('INFO', 'Loading SprintPlanningProcessor');
|
|
1415
|
+
const { SprintPlanningProcessor } = await import('./sprint-planning-processor.js');
|
|
1416
|
+
const processor = new SprintPlanningProcessor();
|
|
1417
|
+
fileLog('DEBUG', 'SprintPlanningProcessor created', {
|
|
1418
|
+
projectPath: processor.projectPath,
|
|
1419
|
+
provider: processor._providerName,
|
|
1420
|
+
model: processor._modelName,
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
await processor.execute();
|
|
1424
|
+
|
|
1425
|
+
// Non-fatal docs sync after sprint planning
|
|
1426
|
+
try {
|
|
1427
|
+
const { DocsSyncProcessor } = await import('./docs-sync.js');
|
|
1428
|
+
const syncer = new DocsSyncProcessor(process.cwd());
|
|
1429
|
+
if (fs.existsSync(syncer.docsDir)) {
|
|
1430
|
+
await syncer.sync();
|
|
1431
|
+
sendInfo('Documentation synced.');
|
|
1432
|
+
}
|
|
1433
|
+
} catch (_) { /* non-fatal */ }
|
|
1434
|
+
|
|
1435
|
+
fileLog('INFO', 'sprintPlanning() complete', { duration: `${Date.now() - startTime}ms` });
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Run Sprint Planning ceremony with a progress callback (used by kanban board)
|
|
1440
|
+
* @param {Function|null} progressCallback - Called with (msg, substep, meta) on each stage
|
|
1441
|
+
* @returns {Promise<object>} Result with epicsCreated, storiesCreated, tokenUsage, model
|
|
1442
|
+
*/
|
|
1443
|
+
async sprintPlanningWithCallback(progressCallback = null, options = {}) {
|
|
1444
|
+
fileLog('INFO', 'sprintPlanningWithCallback() called', { projectRoot: this.projectRoot });
|
|
1445
|
+
if (!this.isAvcProject()) {
|
|
1446
|
+
throw new Error('Project not initialized. Run /init first.');
|
|
1447
|
+
}
|
|
1448
|
+
const { SprintPlanningProcessor } = await import('./sprint-planning-processor.js');
|
|
1449
|
+
const processor = new SprintPlanningProcessor(options);
|
|
1450
|
+
return await processor.execute(progressCallback);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Run Seed ceremony to decompose a Story into Tasks and Subtasks
|
|
1455
|
+
* @param {string} storyId - Story ID (e.g., context-0001-0001)
|
|
1456
|
+
*/
|
|
1457
|
+
async seed(storyId) {
|
|
1458
|
+
const startTime = Date.now();
|
|
1459
|
+
fileLog('INFO', 'seed() called', { storyId, projectRoot: this.projectRoot });
|
|
1460
|
+
|
|
1461
|
+
if (!this.isAvcProject()) {
|
|
1462
|
+
fileLog('ERROR', 'Project not initialized — aborting seed');
|
|
1463
|
+
sendError('Project not initialized. Run /init first.');
|
|
1464
|
+
return;
|
|
238
1465
|
}
|
|
239
1466
|
|
|
240
|
-
|
|
241
|
-
|
|
1467
|
+
if (!storyId) {
|
|
1468
|
+
fileLog('ERROR', 'No story ID provided — aborting seed');
|
|
1469
|
+
sendError('Story ID required. Usage: /seed <story-id>');
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
fileLog('INFO', 'Loading SeedProcessor', { storyId });
|
|
1474
|
+
const { SeedProcessor } = await import('./seed-processor.js');
|
|
1475
|
+
const processor = new SeedProcessor(storyId);
|
|
1476
|
+
fileLog('DEBUG', 'SeedProcessor created', {
|
|
1477
|
+
storyId,
|
|
1478
|
+
storyPath: processor.storyPath,
|
|
1479
|
+
provider: processor._providerName,
|
|
1480
|
+
model: processor._modelName,
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
await processor.execute();
|
|
1484
|
+
fileLog('INFO', 'seed() complete', { storyId, duration: `${Date.now() - startTime}ms` });
|
|
1485
|
+
}
|
|
242
1486
|
|
|
243
|
-
|
|
244
|
-
|
|
1487
|
+
/**
|
|
1488
|
+
* Run Sponsor Call ceremony to define project with AI assistance
|
|
1489
|
+
* Requires API keys to be configured in .env file
|
|
1490
|
+
*/
|
|
1491
|
+
async sponsorCall() {
|
|
1492
|
+
const header = getCeremonyHeader('sponsor-call');
|
|
1493
|
+
sendOutput('');
|
|
1494
|
+
sendOutput(header.title);
|
|
1495
|
+
sendOutput('');
|
|
1496
|
+
sendOutput(`Project directory: ${this.projectRoot}`);
|
|
1497
|
+
sendOutput('');
|
|
245
1498
|
|
|
246
|
-
//
|
|
247
|
-
|
|
1499
|
+
// Check if running in REPL mode
|
|
1500
|
+
const isReplMode = process.env.AVC_REPL_MODE === 'true';
|
|
1501
|
+
if (isReplMode) {
|
|
1502
|
+
// REPL mode is handled by repl-ink.js questionnaire display
|
|
1503
|
+
// This code path shouldn't be reached from REPL
|
|
1504
|
+
sendWarning('Unexpected: Ceremony called directly from REPL');
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Check if project is initialized
|
|
1509
|
+
if (!this.isAvcProject()) {
|
|
1510
|
+
sendError(MESSAGES.PROJECT_NOT_INITIALIZED.error);
|
|
1511
|
+
sendOutput('');
|
|
1512
|
+
sendOutput(MESSAGES.PROJECT_NOT_INITIALIZED.help);
|
|
1513
|
+
sendOutput('');
|
|
1514
|
+
return; // Don't exit in REPL mode
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Check if sponsor call has already completed successfully
|
|
1518
|
+
const { CeremonyHistory } = await import('./ceremony-history.js');
|
|
1519
|
+
const history = new CeremonyHistory(this.avcDir);
|
|
1520
|
+
|
|
1521
|
+
if (history.hasSuccessfulCompletion('sponsor-call')) {
|
|
1522
|
+
sendError('Sponsor Call has already completed successfully');
|
|
1523
|
+
sendOutput('');
|
|
1524
|
+
sendOutput('Project documentation already exists at .avc/project/doc.md');
|
|
1525
|
+
sendOutput('');
|
|
1526
|
+
sendOutput('To regenerate documentation, first run /remove to clear the project,');
|
|
1527
|
+
sendOutput('then run /init followed by /sponsor-call again.');
|
|
1528
|
+
sendOutput('');
|
|
1529
|
+
return; // Don't allow re-running
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
let progress = null;
|
|
1533
|
+
const progressPath = this.sponsorCallProgressPath;
|
|
1534
|
+
|
|
1535
|
+
// Check for incomplete ceremony
|
|
1536
|
+
if (this.hasIncompleteProgress(progressPath)) {
|
|
1537
|
+
progress = this.readProgress(progressPath);
|
|
1538
|
+
|
|
1539
|
+
if (progress && progress.stage !== 'completed') {
|
|
1540
|
+
sendWarning('Found incomplete ceremony from previous session');
|
|
1541
|
+
sendIndented(`Last activity: ${new Date(progress.lastUpdate).toLocaleString()}`, 1);
|
|
1542
|
+
sendIndented(`Stage: ${progress.stage}`, 1);
|
|
1543
|
+
sendIndented(`Progress: ${progress.answeredQuestions || 0}/${progress.totalQuestions || 0} questions answered`, 1);
|
|
1544
|
+
sendOutput('');
|
|
1545
|
+
sendInfo('Continuing from where you left off...');
|
|
1546
|
+
sendOutput('');
|
|
1547
|
+
}
|
|
1548
|
+
} else {
|
|
1549
|
+
// Fresh start
|
|
1550
|
+
sendOutput('Starting Sponsor Call ceremony...');
|
|
1551
|
+
sendOutput('');
|
|
1552
|
+
}
|
|
248
1553
|
|
|
249
|
-
//
|
|
250
|
-
this.
|
|
1554
|
+
// Validate API key before starting ceremony
|
|
1555
|
+
const validationResult = await this.validateProviderApiKey();
|
|
1556
|
+
if (!validationResult.valid) {
|
|
1557
|
+
sendOutput('');
|
|
1558
|
+
sendError('API Key Validation Failed');
|
|
1559
|
+
sendOutput('');
|
|
1560
|
+
sendIndented(validationResult.message, 1);
|
|
1561
|
+
sendOutput('');
|
|
1562
|
+
return; // Don't exit in REPL mode
|
|
1563
|
+
}
|
|
251
1564
|
|
|
252
1565
|
// Save initial progress
|
|
253
1566
|
if (!progress) {
|
|
@@ -258,44 +1571,387 @@ ANTHROPIC_API_KEY=
|
|
|
258
1571
|
collectedValues: {},
|
|
259
1572
|
lastUpdate: new Date().toISOString()
|
|
260
1573
|
};
|
|
261
|
-
this.writeProgress(progress);
|
|
1574
|
+
this.writeProgress(progress, progressPath);
|
|
262
1575
|
}
|
|
263
1576
|
|
|
264
1577
|
// Generate project document via Sponsor Call ceremony
|
|
265
|
-
await this.generateProjectDocument(progress);
|
|
1578
|
+
await this.generateProjectDocument(progress, progressPath, isReplMode);
|
|
266
1579
|
|
|
267
1580
|
// Mark as completed and clean up
|
|
268
1581
|
progress.stage = 'completed';
|
|
269
1582
|
progress.lastUpdate = new Date().toISOString();
|
|
270
|
-
this.writeProgress(progress);
|
|
271
|
-
this.clearProgress();
|
|
1583
|
+
this.writeProgress(progress, progressPath);
|
|
1584
|
+
this.clearProgress(progressPath);
|
|
272
1585
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
1586
|
+
sendOutput('');
|
|
1587
|
+
sendSuccess('Project defined successfully!');
|
|
1588
|
+
sendOutput('');
|
|
1589
|
+
sendOutput('Next steps:');
|
|
1590
|
+
sendIndented('1. Review .avc/project/doc.md for your project definition', 1);
|
|
1591
|
+
sendIndented('2. Review .avc/avc.json configuration', 1);
|
|
1592
|
+
sendIndented('3. Create your project documentation and work items', 1);
|
|
1593
|
+
sendIndented('4. Use AI agents to implement features', 1);
|
|
280
1594
|
}
|
|
281
1595
|
|
|
282
1596
|
/**
|
|
283
1597
|
* Display current project status
|
|
284
1598
|
*/
|
|
285
1599
|
status() {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1600
|
+
fileLog('INFO', 'status() called', { projectRoot: this.projectRoot });
|
|
1601
|
+
|
|
1602
|
+
const hasAvc = this.hasAvcFolder();
|
|
1603
|
+
const hasSrc = this.hasSrcFolder();
|
|
1604
|
+
const hasWorktrees = this.hasWorktreesFolder();
|
|
1605
|
+
const hasConfig = this.hasAvcConfig();
|
|
1606
|
+
const isInitialized = this.isAvcProject();
|
|
1607
|
+
const projectName = this.getProjectName();
|
|
289
1608
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
1609
|
+
fileLog('DEBUG', 'Component check results', {
|
|
1610
|
+
'.avc/': hasAvc,
|
|
1611
|
+
'src/': hasSrc,
|
|
1612
|
+
'worktrees/': hasWorktrees,
|
|
1613
|
+
'avc.json': hasConfig,
|
|
1614
|
+
isAvcProject: isInitialized,
|
|
1615
|
+
projectName,
|
|
1616
|
+
});
|
|
293
1617
|
|
|
294
|
-
|
|
1618
|
+
if (isInitialized) {
|
|
1619
|
+
sendOutput(`${projectName}: Initialized`);
|
|
1620
|
+
fileLog('INFO', 'status() complete — project is initialized');
|
|
1621
|
+
} else {
|
|
1622
|
+
fileLog('WARNING', 'Project not initialized — components missing', { hasAvc, hasConfig });
|
|
1623
|
+
sendOutput(`${projectName}: Not initialized. Run /init to start.`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Remove AVC project structure (destructive operation)
|
|
1629
|
+
* Requires confirmation by typing "delete all"
|
|
1630
|
+
*/
|
|
1631
|
+
async remove() {
|
|
1632
|
+
sendSectionHeader('Remove AVC Project Structure');
|
|
1633
|
+
sendOutput('');
|
|
1634
|
+
sendOutput(`Project directory: ${this.projectRoot}`);
|
|
1635
|
+
sendOutput('');
|
|
295
1636
|
|
|
1637
|
+
// Check if project is initialized
|
|
296
1638
|
if (!this.isAvcProject()) {
|
|
297
|
-
|
|
1639
|
+
sendWarning('No AVC project found in this directory.');
|
|
1640
|
+
sendOutput('Nothing to remove.');
|
|
1641
|
+
sendOutput('');
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Show what will be deleted
|
|
1646
|
+
sendWarning('WARNING: This is a DESTRUCTIVE operation!');
|
|
1647
|
+
sendOutput('');
|
|
1648
|
+
sendOutput('The following will be PERMANENTLY DELETED:');
|
|
1649
|
+
sendOutput('');
|
|
1650
|
+
|
|
1651
|
+
// List contents of .avc folder
|
|
1652
|
+
const avcContents = this.getAvcContents();
|
|
1653
|
+
if (avcContents.length > 0) {
|
|
1654
|
+
sendOutput('.avc/ folder contents:');
|
|
1655
|
+
sendOutput('');
|
|
1656
|
+
avcContents.forEach(item => {
|
|
1657
|
+
sendIndented(`• ${item}`, 1);
|
|
1658
|
+
});
|
|
1659
|
+
sendOutput('');
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
sendError('All project definitions, epics, stories, tasks, and documentation will be lost.');
|
|
1663
|
+
sendError('All VitePress documentation will be deleted.');
|
|
1664
|
+
sendError('This action CANNOT be undone.');
|
|
1665
|
+
sendOutput('');
|
|
1666
|
+
|
|
1667
|
+
// Check for .env file
|
|
1668
|
+
const envPath = path.join(this.projectRoot, '.env');
|
|
1669
|
+
const hasEnvFile = fs.existsSync(envPath);
|
|
1670
|
+
if (hasEnvFile) {
|
|
1671
|
+
sendInfo('Note: The .env file will NOT be deleted.');
|
|
1672
|
+
sendOutput('You may want to manually remove API keys if no longer needed.');
|
|
1673
|
+
sendOutput('');
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Check for src folder
|
|
1677
|
+
const hasSrcFolder = this.hasSrcFolder();
|
|
1678
|
+
if (hasSrcFolder) {
|
|
1679
|
+
sendSuccess('IMPORTANT: The src/ folder will NOT be deleted.');
|
|
1680
|
+
sendOutput('All your AVC-managed code will be preserved.');
|
|
1681
|
+
sendOutput('');
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Check for worktrees folder
|
|
1685
|
+
const hasWorktreesFolder = this.hasWorktreesFolder();
|
|
1686
|
+
if (hasWorktreesFolder) {
|
|
1687
|
+
sendWarning('The .avc/worktrees/ folder and all git worktrees inside it WILL be deleted.');
|
|
1688
|
+
sendOutput('');
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// Check if running in REPL mode
|
|
1692
|
+
const isReplMode = process.env.AVC_REPL_MODE === 'true';
|
|
1693
|
+
|
|
1694
|
+
if (isReplMode) {
|
|
1695
|
+
// In REPL mode, interactive confirmation is handled by repl-ink.js
|
|
1696
|
+
// This code path shouldn't be reached from REPL
|
|
1697
|
+
sendWarning('Unexpected: Remove called directly from REPL');
|
|
1698
|
+
sendOutput('Interactive confirmation should be handled by REPL interface.');
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
sendOutput('─'.repeat(60));
|
|
1703
|
+
sendOutput('To confirm deletion, type exactly: delete all');
|
|
1704
|
+
sendOutput('To cancel, type anything else or press Ctrl+C');
|
|
1705
|
+
sendOutput('─'.repeat(60));
|
|
1706
|
+
sendOutput('');
|
|
1707
|
+
|
|
1708
|
+
// Create readline interface for confirmation
|
|
1709
|
+
const readline = await import('readline');
|
|
1710
|
+
const rl = readline.createInterface({
|
|
1711
|
+
input: process.stdin,
|
|
1712
|
+
output: process.stdout
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
return new Promise((resolve) => {
|
|
1716
|
+
rl.question('Confirmation: ', (answer) => {
|
|
1717
|
+
rl.close();
|
|
1718
|
+
sendOutput('');
|
|
1719
|
+
|
|
1720
|
+
if (answer.trim() === 'delete all') {
|
|
1721
|
+
// Proceed with deletion
|
|
1722
|
+
sendOutput('Deleting AVC project structure...');
|
|
1723
|
+
sendOutput('');
|
|
1724
|
+
|
|
1725
|
+
try {
|
|
1726
|
+
// Get list of what's being deleted before deletion
|
|
1727
|
+
const deletedItems = this.getAvcContents();
|
|
1728
|
+
|
|
1729
|
+
// Delete .avc folder
|
|
1730
|
+
fs.rmSync(this.avcDir, { recursive: true, force: true });
|
|
1731
|
+
|
|
1732
|
+
sendSuccess('Successfully deleted:');
|
|
1733
|
+
sendOutput('');
|
|
1734
|
+
sendOutput('.avc/ folder and all contents:');
|
|
1735
|
+
sendOutput('');
|
|
1736
|
+
deletedItems.forEach(item => {
|
|
1737
|
+
sendIndented(`• ${item}`, 3);
|
|
1738
|
+
});
|
|
1739
|
+
sendOutput('');
|
|
1740
|
+
|
|
1741
|
+
// Reminder about preserved files
|
|
1742
|
+
if (hasEnvFile || hasSrcFolder) {
|
|
1743
|
+
sendInfo('Preserved files:');
|
|
1744
|
+
sendOutput('');
|
|
1745
|
+
|
|
1746
|
+
if (hasEnvFile) {
|
|
1747
|
+
sendOutput('The .env file was NOT deleted and still contains:');
|
|
1748
|
+
sendOutput('');
|
|
1749
|
+
sendIndented('• ANTHROPIC_API_KEY', 1);
|
|
1750
|
+
sendIndented('• GEMINI_API_KEY', 1);
|
|
1751
|
+
sendIndented('• OPENAI_API_KEY', 1);
|
|
1752
|
+
sendIndented('• (and any other API keys you added)', 1);
|
|
1753
|
+
sendOutput('If these API keys are not used elsewhere in your project,');
|
|
1754
|
+
sendOutput('you may want to manually delete the .env file or remove');
|
|
1755
|
+
sendOutput('the unused keys.');
|
|
1756
|
+
sendOutput('');
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
if (hasSrcFolder) {
|
|
1760
|
+
sendSuccess('The src/ folder was NOT deleted.');
|
|
1761
|
+
sendOutput('All your AVC-managed code has been preserved.');
|
|
1762
|
+
sendOutput('');
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
sendSuccess('AVC project structure has been completely removed.');
|
|
1768
|
+
sendOutput('You can re-initialize anytime by running /init');
|
|
1769
|
+
sendOutput('');
|
|
1770
|
+
|
|
1771
|
+
resolve();
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
sendError(`Error during deletion: ${error.message}`);
|
|
1774
|
+
sendOutput('');
|
|
1775
|
+
sendOutput('The .avc folder may be partially deleted.');
|
|
1776
|
+
sendOutput('You may need to manually remove it.');
|
|
1777
|
+
sendOutput('');
|
|
1778
|
+
resolve();
|
|
1779
|
+
}
|
|
1780
|
+
} else {
|
|
1781
|
+
// Cancellation
|
|
1782
|
+
sendError('Operation cancelled.');
|
|
1783
|
+
sendOutput('No files were deleted.');
|
|
1784
|
+
sendOutput('');
|
|
1785
|
+
resolve();
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Get list of contents in .avc folder for display
|
|
1793
|
+
*/
|
|
1794
|
+
getAvcContents() {
|
|
1795
|
+
const contents = [];
|
|
1796
|
+
|
|
1797
|
+
try {
|
|
1798
|
+
if (!fs.existsSync(this.avcDir)) {
|
|
1799
|
+
return contents;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// Read .avc directory
|
|
1803
|
+
const items = fs.readdirSync(this.avcDir);
|
|
1804
|
+
|
|
1805
|
+
items.forEach(item => {
|
|
1806
|
+
const itemPath = path.join(this.avcDir, item);
|
|
1807
|
+
const stat = fs.statSync(itemPath);
|
|
1808
|
+
|
|
1809
|
+
if (stat.isDirectory()) {
|
|
1810
|
+
// Count items in subdirectories
|
|
1811
|
+
const subItems = this.countItemsRecursive(itemPath);
|
|
1812
|
+
contents.push(`${item}/ (${subItems} items)`);
|
|
1813
|
+
} else {
|
|
1814
|
+
contents.push(item);
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
// Ignore errors, return what we have
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
return contents;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Save sponsor-call Q&A answers to avc.json and generate VitePress Q&A page.
|
|
1826
|
+
* Wrapped in try/catch — failures are non-fatal.
|
|
1827
|
+
*/
|
|
1828
|
+
saveProjectBriefAnswers(answers) {
|
|
1829
|
+
try {
|
|
1830
|
+
// Normalize: CLI path has top-level keys; Kanban path may nest under .requirements
|
|
1831
|
+
const qa = answers.MISSION_STATEMENT ? answers : (answers.requirements || answers);
|
|
1832
|
+
|
|
1833
|
+
// Read current avc.json and persist answers
|
|
1834
|
+
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
1835
|
+
if (!config.settings) config.settings = {};
|
|
1836
|
+
if (!config.settings.projectBrief) config.settings.projectBrief = {};
|
|
1837
|
+
config.settings.projectBrief.answers = {
|
|
1838
|
+
MISSION_STATEMENT: qa.MISSION_STATEMENT || null,
|
|
1839
|
+
TARGET_USERS: qa.TARGET_USERS || null,
|
|
1840
|
+
INITIAL_SCOPE: qa.INITIAL_SCOPE || null,
|
|
1841
|
+
DEPLOYMENT_TARGET: qa.DEPLOYMENT_TARGET || null,
|
|
1842
|
+
TECHNICAL_CONSIDERATIONS: qa.TECHNICAL_CONSIDERATIONS || null,
|
|
1843
|
+
TECHNICAL_EXCLUSIONS: qa.TECHNICAL_EXCLUSIONS || null,
|
|
1844
|
+
SECURITY_AND_COMPLIANCE_REQUIREMENTS: qa.SECURITY_AND_COMPLIANCE_REQUIREMENTS || null,
|
|
1845
|
+
};
|
|
1846
|
+
config.settings.projectBrief.savedAt = new Date().toISOString();
|
|
1847
|
+
fs.writeFileSync(this.avcConfigPath, JSON.stringify(config, null, 2), 'utf8');
|
|
1848
|
+
|
|
1849
|
+
fileLog('INFO', 'saveProjectBriefAnswers() — answers persisted to avc.json');
|
|
1850
|
+
|
|
1851
|
+
this.generateQADocumentationPage(qa);
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
fileLog('WARN', 'saveProjectBriefAnswers() failed (non-fatal)', { error: error.message });
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Generate .avc/documentation/questions-and-answers.md with the sponsor-call answers.
|
|
1859
|
+
* No-ops silently if the documentation directory doesn't exist yet.
|
|
1860
|
+
*/
|
|
1861
|
+
generateQADocumentationPage(qa) {
|
|
1862
|
+
const docDir = path.join(this.avcDir, 'documentation');
|
|
1863
|
+
if (!fs.existsSync(docDir)) {
|
|
1864
|
+
fileLog('INFO', 'generateQADocumentationPage() — documentation dir not found, skipping');
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const lines = ['# Questions & Answers', ''];
|
|
1869
|
+
lines.push('Sponsor-call questionnaire answers captured during project brief generation.', '');
|
|
1870
|
+
|
|
1871
|
+
lines.push('## Mission Statement', '');
|
|
1872
|
+
lines.push(qa.MISSION_STATEMENT || '_Not provided._', '');
|
|
1873
|
+
|
|
1874
|
+
lines.push('## Initial Scope', '');
|
|
1875
|
+
lines.push(qa.INITIAL_SCOPE || '_Not provided._', '');
|
|
1876
|
+
|
|
1877
|
+
lines.push('## Target Users', '');
|
|
1878
|
+
lines.push(qa.TARGET_USERS || '_Not provided._', '');
|
|
1879
|
+
|
|
1880
|
+
lines.push('## Deployment Target', '');
|
|
1881
|
+
lines.push(qa.DEPLOYMENT_TARGET || '_Not provided._', '');
|
|
1882
|
+
|
|
1883
|
+
lines.push('## Technical Considerations', '');
|
|
1884
|
+
lines.push(qa.TECHNICAL_CONSIDERATIONS || '_Not provided._', '');
|
|
1885
|
+
|
|
1886
|
+
if (qa.TECHNICAL_EXCLUSIONS) {
|
|
1887
|
+
lines.push('## Technical Exclusions', '');
|
|
1888
|
+
lines.push(qa.TECHNICAL_EXCLUSIONS, '');
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
lines.push('## Security & Compliance Requirements', '');
|
|
1892
|
+
lines.push(qa.SECURITY_AND_COMPLIANCE_REQUIREMENTS || '_Not provided._', '');
|
|
1893
|
+
|
|
1894
|
+
const qaPath = path.join(docDir, 'questions-and-answers.md');
|
|
1895
|
+
fs.writeFileSync(qaPath, lines.join('\n'), 'utf8');
|
|
1896
|
+
|
|
1897
|
+
fileLog('INFO', 'generateQADocumentationPage() — written', { qaPath });
|
|
1898
|
+
|
|
1899
|
+
this.addQAToVitePressConfig();
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Insert "Questions & Answers" into the VitePress sidebar, immediately after "Project Brief".
|
|
1904
|
+
* Idempotent — skips if already present.
|
|
1905
|
+
*/
|
|
1906
|
+
addQAToVitePressConfig() {
|
|
1907
|
+
const configPath = path.join(this.avcDir, 'documentation', '.vitepress', 'config.mts');
|
|
1908
|
+
if (!fs.existsSync(configPath)) {
|
|
1909
|
+
fileLog('INFO', 'addQAToVitePressConfig() — config.mts not found, skipping');
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
let content = fs.readFileSync(configPath, 'utf8');
|
|
1914
|
+
|
|
1915
|
+
if (content.includes('questions-and-answers')) {
|
|
1916
|
+
fileLog('INFO', 'addQAToVitePressConfig() — already present, skipping');
|
|
1917
|
+
return;
|
|
298
1918
|
}
|
|
1919
|
+
|
|
1920
|
+
// Insert after the Project Brief link line
|
|
1921
|
+
const projectBriefLine = "{ text: 'Project Brief', link: '/' }";
|
|
1922
|
+
const qaLine = "{ text: 'Questions & Answers', link: '/questions-and-answers' }";
|
|
1923
|
+
content = content.replace(
|
|
1924
|
+
projectBriefLine,
|
|
1925
|
+
`${projectBriefLine},\n ${qaLine}`
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
1929
|
+
fileLog('INFO', 'addQAToVitePressConfig() — sidebar updated', { configPath });
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Recursively count items in a directory
|
|
1934
|
+
*/
|
|
1935
|
+
countItemsRecursive(dirPath) {
|
|
1936
|
+
let count = 0;
|
|
1937
|
+
|
|
1938
|
+
try {
|
|
1939
|
+
const items = fs.readdirSync(dirPath);
|
|
1940
|
+
count += items.length;
|
|
1941
|
+
|
|
1942
|
+
items.forEach(item => {
|
|
1943
|
+
const itemPath = path.join(dirPath, item);
|
|
1944
|
+
const stat = fs.statSync(itemPath);
|
|
1945
|
+
|
|
1946
|
+
if (stat.isDirectory()) {
|
|
1947
|
+
count += this.countItemsRecursive(itemPath);
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
// Ignore errors
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
return count;
|
|
299
1955
|
}
|
|
300
1956
|
}
|
|
301
1957
|
|
|
@@ -311,11 +1967,20 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
311
1967
|
case 'init':
|
|
312
1968
|
initiator.init();
|
|
313
1969
|
break;
|
|
1970
|
+
case 'sponsor-call':
|
|
1971
|
+
initiator.sponsorCall();
|
|
1972
|
+
break;
|
|
314
1973
|
case 'status':
|
|
315
1974
|
initiator.status();
|
|
316
1975
|
break;
|
|
1976
|
+
case 'models':
|
|
1977
|
+
initiator.models();
|
|
1978
|
+
break;
|
|
1979
|
+
case 'remove':
|
|
1980
|
+
initiator.remove();
|
|
1981
|
+
break;
|
|
317
1982
|
default:
|
|
318
|
-
|
|
1983
|
+
sendError('Unknown command. Available commands: init, sponsor-call, status, models, remove');
|
|
319
1984
|
process.exit(1);
|
|
320
1985
|
}
|
|
321
1986
|
}
|