@agile-vibe-coding/avc 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +152 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -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/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +559 -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 +143 -0
- package/cli/agents/mission-scope-validator.md +146 -0
- package/cli/agents/project-context-extractor.md +122 -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/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -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 +183 -0
- package/cli/agents/validator-documentation.md +455 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/api-reference-tool.js +368 -0
- package/cli/build-docs.js +29 -8
- package/cli/ceremony-history.js +369 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/command-logger.js +49 -12
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +659 -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/init-model-config.js +704 -0
- package/cli/init.js +1737 -278
- package/cli/kanban-server-manager.js +227 -0
- package/cli/llm-claude.js +150 -1
- package/cli/llm-gemini.js +109 -0
- package/cli/llm-local.js +493 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +454 -0
- package/cli/llm-provider.js +379 -3
- package/cli/llm-token-limits.js +211 -0
- package/cli/llm-verifier.js +662 -0
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +49 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +291 -0
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +192 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +270 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +73 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +4625 -1094
- package/cli/repl-old.js +3 -4
- package/cli/seed-processor.js +962 -0
- package/cli/sprint-planning-processor.js +4162 -0
- package/cli/template-processor.js +2149 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- package/cli/token-tracker.js +547 -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 +667 -0
- package/cli/verification-tracker.js +563 -0
- package/cli/worktree-runner.js +654 -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-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -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 +651 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -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 +329 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -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 +63 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -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 +381 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -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 +384 -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 +177 -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 +515 -0
- package/kanban/client/src/lib/status-grouping.js +154 -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 +123 -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 +537 -0
- package/kanban/server/routes/ceremony.js +454 -0
- package/kanban/server/routes/costs.js +163 -0
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +736 -0
- package/kanban/server/routes/websocket.js +281 -0
- package/kanban/server/routes/work-items.js +487 -0
- package/kanban/server/services/CeremonyService.js +1441 -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/TaskRunnerService.js +261 -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/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +92 -0
- package/kanban/server/workers/sprint-planning-worker.js +212 -0
- package/package.json +19 -7
- package/cli/agents/documentation.md +0 -302
|
@@ -3,11 +3,38 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import readline from 'readline';
|
|
5
5
|
import { LLMProvider } from './llm-provider.js';
|
|
6
|
+
import { LLMVerifier } from './llm-verifier.js';
|
|
7
|
+
import { TokenTracker } from './token-tracker.js';
|
|
8
|
+
import { VerificationTracker } from './verification-tracker.js';
|
|
6
9
|
import { fileURLToPath } from 'url';
|
|
10
|
+
import { sendError, sendWarning, sendSuccess, sendInfo, sendOutput, sendIndented, sendSectionHeader, sendProgress } from './messaging-api.js';
|
|
11
|
+
import { loadAgent } from './agent-loader.js';
|
|
7
12
|
|
|
8
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
14
|
const __dirname = path.dirname(__filename);
|
|
10
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Module-level debug log file path.
|
|
18
|
+
* Set via TemplateProcessor.setDebugLogFile() from repl-ink.js before creating
|
|
19
|
+
* any TemplateProcessor instance. When null, debug() is silent (no terminal output).
|
|
20
|
+
* This ensures diagnostic output NEVER corrupts the Ink UI.
|
|
21
|
+
*/
|
|
22
|
+
let _debugLogFile = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Debug logging helper - writes directly to the log file, never to the terminal.
|
|
26
|
+
* @param {string} message - Log message
|
|
27
|
+
* @param {Object} data - Optional data to log
|
|
28
|
+
*/
|
|
29
|
+
function debug(message, data = null) {
|
|
30
|
+
if (!_debugLogFile) return;
|
|
31
|
+
const timestamp = new Date().toISOString();
|
|
32
|
+
const line = data !== null
|
|
33
|
+
? `[${timestamp}] [DEBUG] ${message}\n${JSON.stringify(data, null, 2)}\n`
|
|
34
|
+
: `[${timestamp}] [DEBUG] ${message}\n`;
|
|
35
|
+
try { fs.appendFileSync(_debugLogFile, line, 'utf8'); } catch (_) {}
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
/**
|
|
12
39
|
* TemplateProcessor - Handles interactive template processing with AI suggestions
|
|
13
40
|
*
|
|
@@ -20,55 +47,291 @@ const __dirname = path.dirname(__filename);
|
|
|
20
47
|
* 6. Write to .avc/project/doc.md
|
|
21
48
|
*/
|
|
22
49
|
class TemplateProcessor {
|
|
23
|
-
constructor(progressPath = null, nonInteractive = false) {
|
|
50
|
+
constructor(ceremonyName = 'sponsor-call', progressPath = null, nonInteractive = false, options = {}) {
|
|
24
51
|
// Load environment variables from project .env
|
|
25
52
|
dotenv.config({ path: path.join(process.cwd(), '.env') });
|
|
26
53
|
|
|
54
|
+
debug('TemplateProcessor constructor called', { ceremonyName, nonInteractive });
|
|
55
|
+
|
|
56
|
+
this.ceremonyName = ceremonyName;
|
|
57
|
+
|
|
58
|
+
// Initialize verification tracker
|
|
59
|
+
this.verificationTracker = new VerificationTracker(ceremonyName);
|
|
27
60
|
this.templatePath = path.join(__dirname, 'templates/project.md');
|
|
28
61
|
this.outputDir = path.join(process.cwd(), '.avc/project');
|
|
29
62
|
this.outputPath = path.join(this.outputDir, 'doc.md');
|
|
30
63
|
this.avcConfigPath = path.join(process.cwd(), '.avc/avc.json');
|
|
64
|
+
this.agentsPath = path.join(__dirname, 'agents');
|
|
65
|
+
this.avcPath = path.join(process.cwd(), '.avc');
|
|
31
66
|
this.progressPath = progressPath;
|
|
32
67
|
this.nonInteractive = nonInteractive;
|
|
33
68
|
|
|
34
|
-
//
|
|
35
|
-
|
|
69
|
+
// Track verification feedback for agent learning
|
|
70
|
+
this.verificationFeedback = {};
|
|
71
|
+
|
|
72
|
+
// Collect validation issues for final ceremony summary
|
|
73
|
+
this.validationIssues = [];
|
|
74
|
+
|
|
75
|
+
// Read ceremony-specific configuration
|
|
76
|
+
const { provider, model, validationConfig, stagesConfig } = this.readCeremonyConfig(ceremonyName);
|
|
36
77
|
this._providerName = provider;
|
|
37
78
|
this._modelName = model;
|
|
38
79
|
this.llmProvider = null;
|
|
80
|
+
this.stagesConfig = stagesConfig;
|
|
81
|
+
|
|
82
|
+
// Read validation provider config
|
|
83
|
+
this._validationProvider = null;
|
|
84
|
+
this._validationModel = null;
|
|
85
|
+
this.validationLLMProvider = null;
|
|
86
|
+
this.validationConfig = validationConfig;
|
|
87
|
+
|
|
88
|
+
if (validationConfig?.enabled) {
|
|
89
|
+
this._validationProvider = validationConfig.provider;
|
|
90
|
+
this._validationModel = validationConfig.model;
|
|
91
|
+
|
|
92
|
+
// Validate required fields
|
|
93
|
+
if (!this._validationProvider || !this._validationModel) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Validation is enabled for '${ceremonyName}' but validation.provider or validation.model is not configured in avc.json`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Read refinement provider config (separate from validator — used in improveDocument)
|
|
101
|
+
const refinementConfig = validationConfig?.refinement;
|
|
102
|
+
this._refinementProvider = refinementConfig?.provider || this._validationProvider;
|
|
103
|
+
this._refinementModel = refinementConfig?.model || this._validationModel;
|
|
104
|
+
this._refinementProviderInstance = null;
|
|
105
|
+
|
|
106
|
+
// Initialize token tracker
|
|
107
|
+
this.tokenTracker = new TokenTracker(this.avcPath);
|
|
108
|
+
this.tokenTracker.init();
|
|
109
|
+
|
|
110
|
+
// Track last token usage for ceremony history
|
|
111
|
+
this._lastTokenUsage = null;
|
|
112
|
+
|
|
113
|
+
// Cost threshold protection
|
|
114
|
+
this._costThreshold = options?.costThreshold ?? null;
|
|
115
|
+
this._costLimitReachedCallback = options?.costLimitReachedCallback ?? null;
|
|
116
|
+
this._runningCost = 0;
|
|
117
|
+
|
|
118
|
+
// Progress reporting
|
|
119
|
+
this.progressCallback = null;
|
|
120
|
+
this.activityLog = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Strip markdown code fences from content
|
|
125
|
+
* Handles: ```json ... ``` or ``` ... ```
|
|
126
|
+
*/
|
|
127
|
+
stripMarkdownCodeFences(content) {
|
|
128
|
+
if (typeof content !== 'string') return content;
|
|
129
|
+
|
|
130
|
+
// Remove opening fence (```json, ```JSON, or just ```)
|
|
131
|
+
let stripped = content.replace(/^```(?:json|JSON)?\s*\n?/, '');
|
|
132
|
+
|
|
133
|
+
// Remove closing fence (```)
|
|
134
|
+
stripped = stripped.replace(/\n?\s*```\s*$/, '');
|
|
135
|
+
|
|
136
|
+
return stripped.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set progress callback for updating UI during execution
|
|
141
|
+
*/
|
|
142
|
+
setProgressCallback(callback) {
|
|
143
|
+
this.progressCallback = callback;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Report progress to callback and log activity
|
|
148
|
+
*/
|
|
149
|
+
async reportProgress(message, activity = null) {
|
|
150
|
+
if (this.progressCallback) {
|
|
151
|
+
await this.progressCallback(message);
|
|
152
|
+
}
|
|
153
|
+
if (activity) {
|
|
154
|
+
this.activityLog.push(activity);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Report substep to callback (for detailed progress tracking)
|
|
160
|
+
* @param {string} substep - The substep message
|
|
161
|
+
* @param {Object} metadata - Additional metadata (tokensUsed, filesCreated)
|
|
162
|
+
*/
|
|
163
|
+
async reportSubstep(substep, metadata = {}) {
|
|
164
|
+
if (this.progressCallback) {
|
|
165
|
+
await this.progressCallback(null, substep, metadata);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Report a level-3 detail line to callback
|
|
171
|
+
* @param {string} detail - The detail message
|
|
172
|
+
*/
|
|
173
|
+
async reportDetail(detail) {
|
|
174
|
+
if (this.progressCallback) {
|
|
175
|
+
await this.progressCallback(null, null, { detail });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Report progress with small delay to ensure UI updates
|
|
181
|
+
* Adds async delay to force React state updates and re-renders between stages
|
|
182
|
+
* @param {string} message - Main progress message
|
|
183
|
+
* @param {string} activity - Activity to log
|
|
184
|
+
* @param {number} delayMs - Delay in milliseconds (default 50ms)
|
|
185
|
+
*/
|
|
186
|
+
async reportProgressWithDelay(message, activity = null, delayMs = 50) {
|
|
187
|
+
await this.reportProgress(message, activity);
|
|
188
|
+
// Only delay in non-interactive mode (REPL UI) to allow UI re-renders
|
|
189
|
+
if (!this.nonInteractive && this.progressCallback) {
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
191
|
+
}
|
|
39
192
|
}
|
|
40
193
|
|
|
41
194
|
/**
|
|
42
|
-
* Read
|
|
195
|
+
* Read ceremony-specific configuration from avc.json
|
|
43
196
|
*/
|
|
44
|
-
|
|
197
|
+
readCeremonyConfig(ceremonyName) {
|
|
45
198
|
try {
|
|
46
199
|
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
47
|
-
const ceremony = config.settings?.ceremonies?.
|
|
48
|
-
|
|
49
|
-
|
|
200
|
+
const ceremony = config.settings?.ceremonies?.find(c => c.name === ceremonyName);
|
|
201
|
+
|
|
202
|
+
if (!ceremony) {
|
|
203
|
+
sendWarning(`Ceremony '${ceremonyName}' not found in config, using defaults`);
|
|
204
|
+
return {
|
|
205
|
+
provider: 'claude',
|
|
206
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
207
|
+
validationConfig: null,
|
|
208
|
+
stagesConfig: null
|
|
209
|
+
};
|
|
50
210
|
}
|
|
51
|
-
|
|
52
|
-
return {
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
provider: ceremony.provider || 'claude',
|
|
214
|
+
model: ceremony.defaultModel || 'claude-sonnet-4-5-20250929',
|
|
215
|
+
validationConfig: ceremony.validation || null,
|
|
216
|
+
stagesConfig: ceremony.stages || null
|
|
217
|
+
};
|
|
53
218
|
} catch (error) {
|
|
54
|
-
|
|
55
|
-
return {
|
|
219
|
+
sendWarning(`Could not read ceremony config: ${error.message}`);
|
|
220
|
+
return {
|
|
221
|
+
provider: 'claude',
|
|
222
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
223
|
+
validationConfig: null,
|
|
224
|
+
stagesConfig: null
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get provider and model for a specific stage
|
|
231
|
+
* Falls back to ceremony-level config if stage-specific config not found
|
|
232
|
+
* @param {string} stageName - Stage name ('suggestions', 'documentation')
|
|
233
|
+
* @returns {Object} { provider, model }
|
|
234
|
+
*/
|
|
235
|
+
getProviderForStage(stageName) {
|
|
236
|
+
// Check if stage-specific config exists
|
|
237
|
+
if (this.stagesConfig && this.stagesConfig[stageName]) {
|
|
238
|
+
const stageConfig = this.stagesConfig[stageName];
|
|
239
|
+
return {
|
|
240
|
+
provider: stageConfig.provider || this._providerName,
|
|
241
|
+
model: stageConfig.model || this._modelName
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Fall back to ceremony-level config
|
|
246
|
+
return {
|
|
247
|
+
provider: this._providerName,
|
|
248
|
+
model: this._modelName
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get provider and model for validation of a specific content type
|
|
254
|
+
* Falls back to ceremony-level validation config if type-specific config not found
|
|
255
|
+
* @param {string} validationType - Validation type ('documentation')
|
|
256
|
+
* @returns {Object} { provider, model }
|
|
257
|
+
*/
|
|
258
|
+
getValidationProviderForType(validationType) {
|
|
259
|
+
// Check if type-specific validation config exists
|
|
260
|
+
if (this.validationConfig && this.validationConfig[validationType]) {
|
|
261
|
+
const typeConfig = this.validationConfig[validationType];
|
|
262
|
+
return {
|
|
263
|
+
provider: typeConfig.provider || this._validationProvider,
|
|
264
|
+
model: typeConfig.model || this._validationModel
|
|
265
|
+
};
|
|
56
266
|
}
|
|
267
|
+
|
|
268
|
+
// Fall back to ceremony-level validation config
|
|
269
|
+
return {
|
|
270
|
+
provider: this._validationProvider,
|
|
271
|
+
model: this._validationModel
|
|
272
|
+
};
|
|
57
273
|
}
|
|
58
274
|
|
|
59
275
|
/**
|
|
60
|
-
* Read
|
|
276
|
+
* Read defaults from avc.json questionnaire configuration
|
|
61
277
|
*/
|
|
62
|
-
|
|
278
|
+
readDefaults() {
|
|
63
279
|
try {
|
|
64
280
|
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
65
|
-
|
|
66
|
-
return ceremony?.guidelines || {};
|
|
281
|
+
return config.settings?.questionnaire?.defaults || {};
|
|
67
282
|
} catch (error) {
|
|
68
283
|
return {};
|
|
69
284
|
}
|
|
70
285
|
}
|
|
71
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Get human-readable feedback for a rule violation
|
|
289
|
+
* @param {string} ruleId - Rule ID
|
|
290
|
+
* @returns {string} - Feedback message
|
|
291
|
+
*/
|
|
292
|
+
getRuleFeedback(ruleId) {
|
|
293
|
+
const feedbackMap = {
|
|
294
|
+
'valid-json': 'You wrapped JSON in markdown code fences (```json). Output raw JSON only, no markdown formatting.',
|
|
295
|
+
'required-fields': 'You missed required JSON fields. Include all mandatory fields as specified in the output format.',
|
|
296
|
+
'array-fields': 'Field that should be an array was output as a different type. Check array field requirements.',
|
|
297
|
+
'required-validator-fields': 'Validator output missing required fields. Include validationStatus, overallScore, and issues.',
|
|
298
|
+
'score-range': 'Score must be between 0-100. Check your overallScore value.',
|
|
299
|
+
'valid-severity': 'Invalid severity value. Use only: critical, major, or minor.',
|
|
300
|
+
'valid-status': 'Invalid status value. Use only the allowed status values from the spec.'
|
|
301
|
+
};
|
|
302
|
+
return feedbackMap[ruleId] || `Rule "${ruleId}" was violated. Follow the output format exactly.`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Enhance prompt with verification feedback from previous runs
|
|
307
|
+
* @param {string} prompt - Original prompt
|
|
308
|
+
* @param {string} agentName - Agent name to get feedback for
|
|
309
|
+
* @returns {string} - Enhanced prompt with feedback
|
|
310
|
+
*/
|
|
311
|
+
enhancePromptWithFeedback(prompt, agentName) {
|
|
312
|
+
const feedback = this.verificationFeedback[agentName];
|
|
313
|
+
|
|
314
|
+
if (!feedback || feedback.length === 0) {
|
|
315
|
+
return prompt;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const feedbackText = `
|
|
319
|
+
WARNING: **LEARN FROM PREVIOUS MISTAKES**
|
|
320
|
+
|
|
321
|
+
In your last run, verification found these issues that you MUST avoid:
|
|
322
|
+
|
|
323
|
+
${feedback.map((rule, idx) => `${idx + 1}. **${rule.ruleName}** (${rule.severity}):
|
|
324
|
+
${this.getRuleFeedback(rule.ruleId)}`).join('\n\n')}
|
|
325
|
+
|
|
326
|
+
Please carefully follow the output format requirements to avoid these issues.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
return feedbackText + prompt;
|
|
333
|
+
}
|
|
334
|
+
|
|
72
335
|
/**
|
|
73
336
|
* Load agent instructions from markdown file
|
|
74
337
|
* @param {string} agentFileName - Filename in src/cli/agents/
|
|
@@ -78,12 +341,12 @@ class TemplateProcessor {
|
|
|
78
341
|
try {
|
|
79
342
|
const agentPath = path.join(__dirname, 'agents', agentFileName);
|
|
80
343
|
if (!fs.existsSync(agentPath)) {
|
|
81
|
-
|
|
344
|
+
sendWarning(`Agent instruction file not found: ${agentFileName}`);
|
|
82
345
|
return null;
|
|
83
346
|
}
|
|
84
347
|
return fs.readFileSync(agentPath, 'utf8');
|
|
85
348
|
} catch (error) {
|
|
86
|
-
|
|
349
|
+
sendWarning(`Could not load agent instructions: ${error.message}`);
|
|
87
350
|
return null;
|
|
88
351
|
}
|
|
89
352
|
}
|
|
@@ -109,7 +372,7 @@ class TemplateProcessor {
|
|
|
109
372
|
|
|
110
373
|
return this.loadAgentInstructions(agent.instruction);
|
|
111
374
|
} catch (error) {
|
|
112
|
-
|
|
375
|
+
sendWarning(`Could not get agent for stage ${stage}: ${error.message}`);
|
|
113
376
|
return null;
|
|
114
377
|
}
|
|
115
378
|
}
|
|
@@ -164,7 +427,9 @@ class TemplateProcessor {
|
|
|
164
427
|
|
|
165
428
|
'INITIAL_SCOPE': 'Describe the initial scope of your application: key features, main workflows, and core functionality.\n What will users be able to do? What are the essential capabilities?\n Example: "Users can create tasks, assign them to team members, track progress, set deadlines, and receive notifications."',
|
|
166
429
|
|
|
167
|
-
'
|
|
430
|
+
'DEPLOYMENT_TARGET': 'Where will this application be deployed and hosted?\n Consider: Cloud providers (AWS, Google Cloud, Azure, DigitalOcean, Vercel, Netlify), platform types (serverless, containerized, VM-based, static hosting), local deployment (desktop app, mobile app, browser extension), CMS platforms (WordPress plugin, Shopify app), hybrid approaches (local with cloud sync, PWA), infrastructure constraints (on-premises, air-gapped, specific regions)\n Example: "AWS cloud using Lambda and S3, with CloudFront CDN for global distribution"',
|
|
431
|
+
|
|
432
|
+
'TECHNICAL_CONSIDERATIONS': 'Technical stack and requirements for your application (backend AND frontend).\n Backend examples: "Node.js with Express API", "PostgreSQL database", "Real-time data sync with WebSockets"\n Frontend examples: "React SPA with TypeScript", "VitePress for documentation", "Next.js with SSR for e-commerce", "Material-UI design system"\n UI/UX examples: "Mobile-first responsive design", "WCAG 2.1 AA accessibility", "Must work offline", "Multi-language support"',
|
|
168
433
|
|
|
169
434
|
'SECURITY_AND_COMPLIANCE_REQUIREMENTS': 'Security, privacy, or regulatory requirements your application must meet.\n Examples: "GDPR compliance for EU users", "PCI DSS for payment data", "Two-factor authentication", "Data encryption at rest"'
|
|
170
435
|
};
|
|
@@ -196,11 +461,11 @@ class TemplateProcessor {
|
|
|
196
461
|
async promptSingular(name, guidance) {
|
|
197
462
|
const rl = this.createInterface();
|
|
198
463
|
|
|
199
|
-
|
|
464
|
+
sendSectionHeader(name);
|
|
200
465
|
if (guidance) {
|
|
201
|
-
console.log(
|
|
466
|
+
console.log(`${guidance}`);
|
|
202
467
|
}
|
|
203
|
-
console.log('
|
|
468
|
+
console.log('Enter response (press Enter twice when done, or Enter immediately to skip):\n');
|
|
204
469
|
|
|
205
470
|
const lines = [];
|
|
206
471
|
let emptyLineCount = 0;
|
|
@@ -245,11 +510,11 @@ class TemplateProcessor {
|
|
|
245
510
|
async promptPlural(name, guidance) {
|
|
246
511
|
const rl = this.createInterface();
|
|
247
512
|
|
|
248
|
-
|
|
513
|
+
sendSectionHeader(name);
|
|
249
514
|
if (guidance) {
|
|
250
|
-
console.log(
|
|
515
|
+
console.log(`${guidance}`);
|
|
251
516
|
}
|
|
252
|
-
console.log('
|
|
517
|
+
console.log('Enter items one per line (empty line to finish, or Enter immediately to skip):\n');
|
|
253
518
|
|
|
254
519
|
const items = [];
|
|
255
520
|
let itemNumber = 1;
|
|
@@ -283,16 +548,222 @@ class TemplateProcessor {
|
|
|
283
548
|
});
|
|
284
549
|
}
|
|
285
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Register a per-call token callback on a provider instance.
|
|
553
|
+
* Saves tokens to disk after every LLM API call for crash-safe accounting.
|
|
554
|
+
* @param {LLMProvider} provider - Provider to register on
|
|
555
|
+
* @param {string} ceremonyType - Ceremony type key (e.g., 'sponsor-call')
|
|
556
|
+
*/
|
|
557
|
+
_registerTokenCallback(provider, ceremonyType) {
|
|
558
|
+
if (!provider) return;
|
|
559
|
+
provider.onCall((delta) => {
|
|
560
|
+
this.tokenTracker.addIncremental(ceremonyType, delta);
|
|
561
|
+
if (delta.model) {
|
|
562
|
+
const cost = this.tokenTracker.calculateCost(delta.input, delta.output, delta.model);
|
|
563
|
+
this._runningCost += cost?.total ?? 0;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
286
568
|
/**
|
|
287
569
|
* Initialize LLM provider
|
|
288
570
|
*/
|
|
289
571
|
async initializeLLMProvider() {
|
|
290
572
|
try {
|
|
573
|
+
// Initialize main provider
|
|
291
574
|
this.llmProvider = await LLMProvider.create(this._providerName, this._modelName);
|
|
575
|
+
this._registerTokenCallback(this.llmProvider, this.ceremonyName);
|
|
576
|
+
debug(`Using ${this._providerName} (${this._modelName}) for generation`);
|
|
577
|
+
|
|
578
|
+
// Initialize validation provider if validation is enabled
|
|
579
|
+
if (this._validationProvider) {
|
|
580
|
+
debug(`Using ${this._validationProvider} (${this._validationModel}) for validation`);
|
|
581
|
+
this.validationLLMProvider = await LLMProvider.create(
|
|
582
|
+
this._validationProvider,
|
|
583
|
+
this._validationModel
|
|
584
|
+
);
|
|
585
|
+
this._registerTokenCallback(this.validationLLMProvider, `${this.ceremonyName}-validation`);
|
|
586
|
+
}
|
|
587
|
+
|
|
292
588
|
return this.llmProvider;
|
|
293
589
|
} catch (error) {
|
|
294
|
-
|
|
295
|
-
console.log(
|
|
590
|
+
sendWarning(`Could not initialize LLM provider`);
|
|
591
|
+
console.log(`${error.message}`);
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Get or create LLM provider for a specific stage
|
|
598
|
+
* @param {string} stageName - Stage name ('suggestions', 'documentation')
|
|
599
|
+
* @returns {Promise<LLMProvider>} LLM provider instance
|
|
600
|
+
*/
|
|
601
|
+
async getProviderForStageInstance(stageName) {
|
|
602
|
+
let { provider, model } = this.getProviderForStage(stageName);
|
|
603
|
+
|
|
604
|
+
// Resolve to an available provider if current one has no credentials
|
|
605
|
+
const resolved = await LLMProvider.resolveAvailableProvider(provider, model);
|
|
606
|
+
if (resolved.fellBack) {
|
|
607
|
+
debug(`Provider fallback for ${stageName}: ${provider}→${resolved.provider} (${resolved.model})`);
|
|
608
|
+
console.warn(`[WARN] ${provider} has no API key — falling back to ${resolved.provider} for stage "${stageName}"`);
|
|
609
|
+
provider = resolved.provider;
|
|
610
|
+
model = resolved.model;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Check if we already have a provider for this stage
|
|
614
|
+
const cacheKey = `${stageName}:${provider}:${model}`;
|
|
615
|
+
if (!this._stageProviders) {
|
|
616
|
+
this._stageProviders = {};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (this._stageProviders[cacheKey]) {
|
|
620
|
+
return this._stageProviders[cacheKey];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Create new provider
|
|
624
|
+
const providerInstance = await LLMProvider.create(provider, model);
|
|
625
|
+
this._registerTokenCallback(providerInstance, this.ceremonyName);
|
|
626
|
+
this._stageProviders[cacheKey] = providerInstance;
|
|
627
|
+
|
|
628
|
+
debug(`Using ${provider} (${model}) for ${stageName}`);
|
|
629
|
+
|
|
630
|
+
return providerInstance;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Get or create validation provider for a specific content type
|
|
635
|
+
* @param {string} validationType - Validation type ('documentation')
|
|
636
|
+
* @returns {Promise<LLMProvider>} LLM provider instance
|
|
637
|
+
*/
|
|
638
|
+
async getValidationProviderForTypeInstance(validationType) {
|
|
639
|
+
let { provider, model } = this.getValidationProviderForType(validationType);
|
|
640
|
+
|
|
641
|
+
// Resolve to an available provider if current one has no credentials
|
|
642
|
+
const resolved = await LLMProvider.resolveAvailableProvider(provider, model);
|
|
643
|
+
if (resolved.fellBack) {
|
|
644
|
+
debug(`Validation provider fallback for ${validationType}: ${provider}→${resolved.provider} (${resolved.model})`);
|
|
645
|
+
console.warn(`[WARN] ${provider} has no API key — falling back to ${resolved.provider} for validation "${validationType}"`);
|
|
646
|
+
provider = resolved.provider;
|
|
647
|
+
model = resolved.model;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Check if we already have a provider for this validation type
|
|
651
|
+
const cacheKey = `validation:${validationType}:${provider}:${model}`;
|
|
652
|
+
if (!this._validationProviders) {
|
|
653
|
+
this._validationProviders = {};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (this._validationProviders[cacheKey]) {
|
|
657
|
+
return this._validationProviders[cacheKey];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Create new provider
|
|
661
|
+
const providerInstance = await LLMProvider.create(provider, model);
|
|
662
|
+
this._registerTokenCallback(providerInstance, `${this.ceremonyName}-validation`);
|
|
663
|
+
this._validationProviders[cacheKey] = providerInstance;
|
|
664
|
+
|
|
665
|
+
debug(`Using ${provider} (${model}) for ${validationType} validation`);
|
|
666
|
+
|
|
667
|
+
return providerInstance;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Get or create LLM provider for document refinement (improvement after validation).
|
|
672
|
+
* Uses validation.refinement config if set, otherwise falls back to validation provider.
|
|
673
|
+
* @returns {Promise<LLMProvider>} LLM provider instance
|
|
674
|
+
*/
|
|
675
|
+
async getRefinementProviderInstance() {
|
|
676
|
+
if (this._refinementProviderInstance) {
|
|
677
|
+
return this._refinementProviderInstance;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const provider = this._refinementProvider;
|
|
681
|
+
const model = this._refinementModel;
|
|
682
|
+
|
|
683
|
+
if (!provider || !model) {
|
|
684
|
+
return this.getValidationProviderForTypeInstance('documentation');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const providerInstance = await LLMProvider.create(provider, model);
|
|
688
|
+
this._registerTokenCallback(providerInstance, `${this.ceremonyName}-refinement`);
|
|
689
|
+
this._refinementProviderInstance = providerInstance;
|
|
690
|
+
|
|
691
|
+
debug(`Using ${provider} (${model}) for document refinement`);
|
|
692
|
+
return providerInstance;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Run an async fn while emitting periodic heartbeat substep messages.
|
|
697
|
+
* Clears the interval once fn resolves or rejects.
|
|
698
|
+
* @param {Function} fn - Async function to run
|
|
699
|
+
* @param {Function} getMessageFn - Called with elapsed seconds, returns substep string
|
|
700
|
+
* @param {number} intervalMs - How often to emit (default 10s)
|
|
701
|
+
*/
|
|
702
|
+
async withHeartbeat(fn, getMessageFn, intervalMs = 10000) {
|
|
703
|
+
const startTime = Date.now();
|
|
704
|
+
const timer = setInterval(() => {
|
|
705
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
706
|
+
this.reportDetail(getMessageFn(elapsed)).catch(() => {});
|
|
707
|
+
}, intervalMs);
|
|
708
|
+
try {
|
|
709
|
+
return await fn();
|
|
710
|
+
} finally {
|
|
711
|
+
clearInterval(timer);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Retry wrapper for LLM calls with exponential backoff
|
|
717
|
+
* @param {Function} fn - Async function to retry
|
|
718
|
+
* @param {string} operation - Description of operation for logging
|
|
719
|
+
* @param {number} maxRetries - Maximum retry attempts (default: 3)
|
|
720
|
+
* @returns {Promise} Result from function call
|
|
721
|
+
*/
|
|
722
|
+
async retryWithBackoff(fn, operation, maxRetries = 3) {
|
|
723
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
724
|
+
try {
|
|
725
|
+
return await fn();
|
|
726
|
+
} catch (error) {
|
|
727
|
+
const isLastAttempt = attempt === maxRetries;
|
|
728
|
+
const isRetriable = error.message?.includes('rate limit') ||
|
|
729
|
+
error.message?.includes('timeout') ||
|
|
730
|
+
error.message?.includes('503') ||
|
|
731
|
+
error.message?.includes('network');
|
|
732
|
+
|
|
733
|
+
if (isLastAttempt || !isRetriable) {
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
|
|
738
|
+
sendWarning(`Retry ${attempt}/${maxRetries} in ${delay/1000}s: ${operation}`);
|
|
739
|
+
console.log(`Error: ${error.message}`);
|
|
740
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get domain-specific agent for a variable
|
|
747
|
+
* Returns agent instructions for generating suggestions
|
|
748
|
+
*/
|
|
749
|
+
getAgentForVariable(variableName) {
|
|
750
|
+
const agentMap = {
|
|
751
|
+
'TARGET_USERS': 'suggestion-ux-researcher.md',
|
|
752
|
+
'INITIAL_SCOPE': 'suggestion-product-manager.md',
|
|
753
|
+
'DEPLOYMENT_TARGET': 'suggestion-deployment-architect.md',
|
|
754
|
+
'TECHNICAL_CONSIDERATIONS': 'suggestion-technical-architect.md',
|
|
755
|
+
'SECURITY_AND_COMPLIANCE_REQUIREMENTS': 'suggestion-security-specialist.md'
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const agentFile = agentMap[variableName];
|
|
759
|
+
if (!agentFile) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
return loadAgent(agentFile, path.dirname(this.avcPath));
|
|
765
|
+
} catch {
|
|
766
|
+
sendWarning(`Agent file not found: ${agentFile}`);
|
|
296
767
|
return null;
|
|
297
768
|
}
|
|
298
769
|
}
|
|
@@ -318,11 +789,8 @@ class TemplateProcessor {
|
|
|
318
789
|
contextSection += '\n';
|
|
319
790
|
}
|
|
320
791
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
} else {
|
|
324
|
-
return `${contextSection}Suggest an appropriate value for "${displayName}".\n\nReturn only the suggestion text, concise (1-3 sentences).`;
|
|
325
|
-
}
|
|
792
|
+
// Create simple user prompt with context
|
|
793
|
+
return `${contextSection}Please provide your response for "${displayName}".`;
|
|
326
794
|
}
|
|
327
795
|
|
|
328
796
|
/**
|
|
@@ -339,19 +807,35 @@ class TemplateProcessor {
|
|
|
339
807
|
}
|
|
340
808
|
|
|
341
809
|
/**
|
|
342
|
-
* Generate AI suggestions for a variable
|
|
810
|
+
* Generate AI suggestions for a variable using domain-specific agent
|
|
343
811
|
*/
|
|
344
812
|
async generateSuggestions(variableName, isPlural, context) {
|
|
345
|
-
if (!this.llmProvider && !(await this.initializeLLMProvider())) {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
813
|
try {
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
814
|
+
// Get stage-specific provider for suggestions
|
|
815
|
+
const provider = await this.getProviderForStageInstance('suggestions');
|
|
816
|
+
|
|
817
|
+
// Get domain-specific agent for this variable
|
|
818
|
+
const agentInstructions = this.getAgentForVariable(variableName);
|
|
819
|
+
|
|
820
|
+
if (agentInstructions) {
|
|
821
|
+
// Use domain-specific agent with context
|
|
822
|
+
const prompt = this.buildPrompt(variableName, isPlural, context);
|
|
823
|
+
debug(`Using specialized agent: ${variableName.toLowerCase().replace(/_/g, '-')}`);
|
|
824
|
+
|
|
825
|
+
const text = await this.retryWithBackoff(
|
|
826
|
+
() => provider.generate(prompt, isPlural ? 512 : 1024, agentInstructions),
|
|
827
|
+
`${variableName} suggestion`
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
return this.parseLLMResponse(text, isPlural);
|
|
831
|
+
} else {
|
|
832
|
+
// Fallback to generic prompt if no agent available
|
|
833
|
+
const prompt = this.buildPrompt(variableName, isPlural, context);
|
|
834
|
+
const text = await provider.generate(prompt, isPlural ? 512 : 256);
|
|
835
|
+
return this.parseLLMResponse(text, isPlural);
|
|
836
|
+
}
|
|
353
837
|
} catch (error) {
|
|
354
|
-
|
|
838
|
+
sendWarning(`Could not generate suggestions: ${error.message}`);
|
|
355
839
|
return null;
|
|
356
840
|
}
|
|
357
841
|
}
|
|
@@ -363,13 +847,10 @@ class TemplateProcessor {
|
|
|
363
847
|
async promptUser(variable, context) {
|
|
364
848
|
let value;
|
|
365
849
|
|
|
366
|
-
// In non-interactive mode, skip readline prompts and use
|
|
850
|
+
// In non-interactive mode, skip readline prompts and use defaults/AI
|
|
367
851
|
if (this.nonInteractive) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
console.log(` ${variable.guidance}`);
|
|
371
|
-
}
|
|
372
|
-
console.log(' Generating AI response...');
|
|
852
|
+
// No section header output — silent AI generation to avoid polluting terminal output
|
|
853
|
+
sendProgress(`Generating AI suggestion for ${variable.displayName}...`);
|
|
373
854
|
value = null; // Force AI generation
|
|
374
855
|
} else {
|
|
375
856
|
// Interactive mode - use readline prompts
|
|
@@ -380,37 +861,37 @@ class TemplateProcessor {
|
|
|
380
861
|
}
|
|
381
862
|
}
|
|
382
863
|
|
|
383
|
-
// If user skipped (or non-interactive mode), try to use
|
|
864
|
+
// If user skipped (or non-interactive mode), try to use default or generate AI suggestions
|
|
384
865
|
if (value === null) {
|
|
385
|
-
// Check if there's a
|
|
386
|
-
const
|
|
387
|
-
const
|
|
866
|
+
// Check if there's a default for this variable
|
|
867
|
+
const defaults = this.readDefaults();
|
|
868
|
+
const defaultValue = defaults[variable.name];
|
|
388
869
|
|
|
389
|
-
if (
|
|
390
|
-
|
|
870
|
+
if (defaultValue) {
|
|
871
|
+
sendInfo('Using default from settings...');
|
|
391
872
|
value = variable.isPlural
|
|
392
|
-
?
|
|
393
|
-
:
|
|
873
|
+
? (Array.isArray(defaultValue) ? defaultValue : [defaultValue])
|
|
874
|
+
: defaultValue;
|
|
394
875
|
|
|
395
|
-
|
|
876
|
+
sendSuccess('Default applied:');
|
|
396
877
|
if (Array.isArray(value)) {
|
|
397
|
-
value.forEach((item, idx) => console.log(
|
|
878
|
+
value.forEach((item, idx) => console.log(`${idx + 1}. ${item}`));
|
|
398
879
|
} else {
|
|
399
|
-
console.log(
|
|
880
|
+
console.log(`${value}`);
|
|
400
881
|
}
|
|
401
|
-
return { variable: variable.name, value, source: '
|
|
882
|
+
return { variable: variable.name, value, source: 'default', skipped: false };
|
|
402
883
|
}
|
|
403
884
|
|
|
404
|
-
// No
|
|
405
|
-
|
|
885
|
+
// No default available, try AI suggestions
|
|
886
|
+
sendInfo('Generating AI suggestion...');
|
|
406
887
|
value = await this.generateSuggestions(variable.name, variable.isPlural, context);
|
|
407
888
|
|
|
408
889
|
if (value) {
|
|
409
|
-
|
|
890
|
+
sendSuccess('AI suggestion:');
|
|
410
891
|
if (Array.isArray(value)) {
|
|
411
|
-
value.forEach((item, idx) => console.log(
|
|
892
|
+
value.forEach((item, idx) => console.log(`${idx + 1}. ${item}`));
|
|
412
893
|
} else {
|
|
413
|
-
console.log(
|
|
894
|
+
console.log(`${value}`);
|
|
414
895
|
}
|
|
415
896
|
return { variable: variable.name, value, source: 'ai', skipped: true };
|
|
416
897
|
} else {
|
|
@@ -450,33 +931,121 @@ class TemplateProcessor {
|
|
|
450
931
|
/**
|
|
451
932
|
* Generate final document with LLM enhancement
|
|
452
933
|
*/
|
|
453
|
-
async generateFinalDocument(templateWithValues) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
console.log('\n🤖 Enhancing document with AI...');
|
|
461
|
-
|
|
934
|
+
async generateFinalDocument(templateWithValues, questionnaire = null) {
|
|
935
|
+
const t0 = Date.now();
|
|
936
|
+
debug('generateFinalDocument called', {
|
|
937
|
+
templateLength: templateWithValues?.length,
|
|
938
|
+
hasQuestionnaire: !!questionnaire,
|
|
939
|
+
ceremony: this.ceremonyName
|
|
940
|
+
});
|
|
462
941
|
try {
|
|
942
|
+
// Get stage-specific provider for documentation
|
|
943
|
+
const provider = await this.getProviderForStageInstance('documentation');
|
|
944
|
+
|
|
463
945
|
// Try to load agent instructions for enhancement stage
|
|
946
|
+
this.reportSubstep('Reading agent: project-documentation-creator.md');
|
|
464
947
|
const agentInstructions = this.getAgentForStage('enhancement');
|
|
465
948
|
|
|
466
949
|
if (agentInstructions) {
|
|
467
|
-
console.log(' Using custom agent instructions for enhancement');
|
|
468
950
|
// Use agent instructions as system context
|
|
469
|
-
|
|
951
|
+
let userPrompt = `Here is the project information with all variables filled in:
|
|
952
|
+
|
|
953
|
+
${templateWithValues}`;
|
|
954
|
+
|
|
955
|
+
// Inject explicit user choices so the LLM cannot override them with defaults
|
|
956
|
+
if (questionnaire) {
|
|
957
|
+
const deploymentTarget = questionnaire.DEPLOYMENT_TARGET || '';
|
|
958
|
+
const isLocal = /local|localhost|dev\s*machine|on.?prem/i.test(deploymentTarget);
|
|
959
|
+
|
|
960
|
+
userPrompt += `\n\n**User's Stated Choices — MUST be respected exactly as provided:**
|
|
961
|
+
- MISSION_STATEMENT: ${questionnaire.MISSION_STATEMENT || 'N/A'}
|
|
962
|
+
- TARGET_USERS: ${questionnaire.TARGET_USERS || 'N/A'}
|
|
963
|
+
- INITIAL_SCOPE: ${questionnaire.INITIAL_SCOPE || 'N/A'}
|
|
964
|
+
- TECHNICAL_CONSIDERATIONS: ${questionnaire.TECHNICAL_CONSIDERATIONS || 'N/A'}
|
|
965
|
+
- DEPLOYMENT_TARGET: ${deploymentTarget || 'N/A'}
|
|
966
|
+
- TECHNICAL_EXCLUSIONS: ${questionnaire.TECHNICAL_EXCLUSIONS || 'None'}
|
|
967
|
+
- SECURITY_AND_COMPLIANCE_REQUIREMENTS: ${questionnaire.SECURITY_AND_COMPLIANCE_REQUIREMENTS || 'N/A'}`;
|
|
968
|
+
|
|
969
|
+
if (isLocal) {
|
|
970
|
+
userPrompt += `\n\n**DEPLOYMENT CONSTRAINT — CRITICAL:**
|
|
971
|
+
The user has chosen LOCAL deployment ("${deploymentTarget}").
|
|
972
|
+
- The Deployment Environment section MUST describe local-first setup only (e.g. Docker Compose, localhost, local DB, npm run dev).
|
|
973
|
+
- Do NOT add cloud providers (AWS, GCP, Azure), managed services, Kubernetes, Terraform, CI/CD pipelines, or any cloud infrastructure.
|
|
974
|
+
- Do NOT suggest migration to cloud unless the user's INITIAL_SCOPE or TECHNICAL_CONSIDERATIONS explicitly requests it.
|
|
975
|
+
- Infrastructure means: local processes, local database, local file system.`;
|
|
976
|
+
}
|
|
470
977
|
|
|
471
|
-
|
|
978
|
+
if (questionnaire.TECHNICAL_EXCLUSIONS) {
|
|
979
|
+
userPrompt += `\n\n**TECHNOLOGY EXCLUSION CONSTRAINT — CRITICAL:**
|
|
980
|
+
The user has explicitly excluded the following from this project:
|
|
981
|
+
${questionnaire.TECHNICAL_EXCLUSIONS}
|
|
982
|
+
- These technologies MUST NOT appear as recommendations anywhere in the document.
|
|
983
|
+
- Add an "Explicitly Excluded Technologies" subsection in section 6 (Technical Architecture).
|
|
984
|
+
- Do NOT soften with "consider using" or "if needed" language.`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
472
987
|
|
|
473
|
-
|
|
988
|
+
userPrompt += `\n\nPlease review and enhance this document according to your role.`;
|
|
989
|
+
|
|
990
|
+
// Enhance prompt with verification feedback if available
|
|
991
|
+
userPrompt = this.enhancePromptWithFeedback(userPrompt, 'project-documentation-creator');
|
|
992
|
+
|
|
993
|
+
this.reportSubstep('Generating Project Brief (this may take 20-30 seconds)...');
|
|
994
|
+
await this.reportDetail(`Sending to ${provider.providerName || 'LLM'} (${provider.model || ''})…`);
|
|
995
|
+
const enhanced = await this.withHeartbeat(
|
|
996
|
+
() => this.retryWithBackoff(
|
|
997
|
+
() => provider.generate(userPrompt, 4096, agentInstructions),
|
|
998
|
+
'document enhancement'
|
|
999
|
+
),
|
|
1000
|
+
(elapsed) => {
|
|
1001
|
+
if (elapsed < 15) return 'Structuring project documentation…';
|
|
1002
|
+
if (elapsed < 30) return 'Writing mission and scope sections…';
|
|
1003
|
+
if (elapsed < 45) return 'Generating technical architecture…';
|
|
1004
|
+
if (elapsed < 60) return 'Finalizing project brief…';
|
|
1005
|
+
return `Generating Project Brief… (${elapsed}s)`;
|
|
1006
|
+
},
|
|
1007
|
+
15000
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
// Report token usage after generation
|
|
1011
|
+
if (provider && typeof provider.getTokenUsage === 'function') {
|
|
1012
|
+
const usage = provider.getTokenUsage();
|
|
1013
|
+
this.reportSubstep('Processing generated content...', {
|
|
1014
|
+
tokensUsed: {
|
|
1015
|
+
input: usage.inputTokens,
|
|
1016
|
+
output: usage.outputTokens
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
await this.reportDetail(`${usage.inputTokens.toLocaleString()} in · ${usage.outputTokens.toLocaleString()} out tokens`);
|
|
1020
|
+
}
|
|
474
1021
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1022
|
+
// Post-process verification
|
|
1023
|
+
this.reportSubstep('Verifying documentation quality...');
|
|
1024
|
+
const verifier = new LLMVerifier(provider, 'project-documentation-creator', this.verificationTracker);
|
|
1025
|
+
const verificationResult = await verifier.verify(
|
|
1026
|
+
enhanced,
|
|
1027
|
+
(mainMsg, substep) => this.reportSubstep(substep)
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
if (verificationResult.rulesApplied.length > 0) {
|
|
1031
|
+
this.reportSubstep(`Applied ${verificationResult.rulesApplied.length} fixes`);
|
|
1032
|
+
|
|
1033
|
+
// Store feedback for agent learning
|
|
1034
|
+
this.verificationFeedback['project-documentation-creator'] = verificationResult.rulesApplied.map(rule => ({
|
|
1035
|
+
ruleId: rule.id,
|
|
1036
|
+
ruleName: rule.name,
|
|
1037
|
+
severity: rule.severity
|
|
1038
|
+
}));
|
|
1039
|
+
|
|
1040
|
+
// Collect for final ceremony summary
|
|
1041
|
+
for (const rule of verificationResult.rulesApplied) {
|
|
1042
|
+
this.validationIssues.push({ stage: 'Project Documentation', ruleId: rule.id, name: rule.name, severity: rule.severity, description: rule.description });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
debug('generateFinalDocument complete (agent path)', { duration: `${Date.now() - t0}ms`, resultLength: verificationResult.content?.length });
|
|
1047
|
+
return verificationResult.content;
|
|
478
1048
|
} else {
|
|
479
|
-
console.log(' Using default enhancement instructions');
|
|
480
1049
|
// Fallback to legacy hardcoded prompt for backward compatibility
|
|
481
1050
|
const legacyPrompt = `You are creating a project definition document for an Agile Vibe Coding (AVC) project.
|
|
482
1051
|
|
|
@@ -492,13 +1061,30 @@ Please review and enhance this document to ensure:
|
|
|
492
1061
|
|
|
493
1062
|
Return the enhanced markdown document.`;
|
|
494
1063
|
|
|
495
|
-
|
|
496
|
-
|
|
1064
|
+
this.reportSubstep('Generating Project Brief (this may take 20-30 seconds)...');
|
|
1065
|
+
await this.reportDetail(`Sending to ${provider.providerName || 'LLM'} (${provider.model || ''})…`);
|
|
1066
|
+
const enhanced = await this.withHeartbeat(
|
|
1067
|
+
() => this.retryWithBackoff(
|
|
1068
|
+
() => provider.generate(legacyPrompt, 4096),
|
|
1069
|
+
'document enhancement (legacy)'
|
|
1070
|
+
),
|
|
1071
|
+
(elapsed) => {
|
|
1072
|
+
if (elapsed < 15) return 'Structuring project documentation…';
|
|
1073
|
+
if (elapsed < 30) return 'Writing mission and scope sections…';
|
|
1074
|
+
if (elapsed < 45) return 'Generating technical architecture…';
|
|
1075
|
+
return `Generating Project Brief… (${elapsed}s)`;
|
|
1076
|
+
},
|
|
1077
|
+
15000
|
|
1078
|
+
);
|
|
1079
|
+
if (provider && typeof provider.getTokenUsage === 'function') {
|
|
1080
|
+
const usage = provider.getTokenUsage();
|
|
1081
|
+
await this.reportDetail(`${usage.inputTokens.toLocaleString()} in · ${usage.outputTokens.toLocaleString()} out tokens`);
|
|
1082
|
+
}
|
|
1083
|
+
debug('generateFinalDocument complete (legacy path)', { duration: `${Date.now() - t0}ms`, resultLength: enhanced?.length });
|
|
497
1084
|
return enhanced;
|
|
498
1085
|
}
|
|
499
1086
|
} catch (error) {
|
|
500
|
-
|
|
501
|
-
console.log(' Using template without AI enhancement');
|
|
1087
|
+
debug('generateFinalDocument error - returning template as-is', { error: error.message, duration: `${Date.now() - t0}ms` });
|
|
502
1088
|
return templateWithValues;
|
|
503
1089
|
}
|
|
504
1090
|
}
|
|
@@ -522,16 +1108,16 @@ Return the enhanced markdown document.`;
|
|
|
522
1108
|
|
|
523
1109
|
// Check if documentation folder exists
|
|
524
1110
|
if (!fs.existsSync(docsDir)) {
|
|
525
|
-
|
|
1111
|
+
sendInfo('VitePress documentation folder not found, skipping sync');
|
|
526
1112
|
return false;
|
|
527
1113
|
}
|
|
528
1114
|
|
|
529
1115
|
// Write to .avc/documentation/index.md
|
|
530
1116
|
fs.writeFileSync(indexPath, content, 'utf8');
|
|
531
|
-
|
|
1117
|
+
debug('Synced to .avc/documentation/index.md');
|
|
532
1118
|
return true;
|
|
533
1119
|
} catch (error) {
|
|
534
|
-
|
|
1120
|
+
sendWarning(`Could not sync to VitePress: ${error.message}`);
|
|
535
1121
|
return false;
|
|
536
1122
|
}
|
|
537
1123
|
}
|
|
@@ -565,10 +1151,10 @@ Return the enhanced markdown document.`;
|
|
|
565
1151
|
stdio: 'inherit'
|
|
566
1152
|
});
|
|
567
1153
|
|
|
568
|
-
|
|
1154
|
+
sendSuccess('VitePress build completed');
|
|
569
1155
|
return true;
|
|
570
1156
|
} catch (error) {
|
|
571
|
-
|
|
1157
|
+
sendWarning(`VitePress build failed: ${error.message}`);
|
|
572
1158
|
return false;
|
|
573
1159
|
}
|
|
574
1160
|
}
|
|
@@ -577,18 +1163,23 @@ Return the enhanced markdown document.`;
|
|
|
577
1163
|
* Write document to file
|
|
578
1164
|
*/
|
|
579
1165
|
async writeDocument(content) {
|
|
1166
|
+
const fileSize = Math.ceil(Buffer.byteLength(content, 'utf8') / 1024);
|
|
1167
|
+
debug('writeDocument called', { outputPath: this.outputPath, sizeKB: fileSize });
|
|
1168
|
+
|
|
580
1169
|
// Create .avc/project/ directory
|
|
581
1170
|
if (!fs.existsSync(this.outputDir)) {
|
|
582
1171
|
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
583
1172
|
}
|
|
584
1173
|
|
|
585
1174
|
// Write doc.md
|
|
1175
|
+
this.reportSubstep(`Writing doc.md (${fileSize} KB)`);
|
|
586
1176
|
fs.writeFileSync(this.outputPath, content, 'utf8');
|
|
587
1177
|
|
|
588
|
-
|
|
589
|
-
|
|
1178
|
+
// Report files created
|
|
1179
|
+
const filesCreated = ['.avc/project/project/doc.md'];
|
|
1180
|
+
this.reportSubstep('Project Brief created successfully', { filesCreated });
|
|
590
1181
|
|
|
591
|
-
// Sync to VitePress if configured
|
|
1182
|
+
// Sync to VitePress if configured (silent for sponsor-call)
|
|
592
1183
|
const synced = this.syncToVitePress(content);
|
|
593
1184
|
|
|
594
1185
|
// Optionally build VitePress (commented out by default to avoid slow builds during dev)
|
|
@@ -601,37 +1192,73 @@ Return the enhanced markdown document.`;
|
|
|
601
1192
|
* Main workflow - process template and generate document
|
|
602
1193
|
*/
|
|
603
1194
|
async processTemplate(initialProgress = null) {
|
|
604
|
-
|
|
1195
|
+
debug('Starting processTemplate', { hasInitialProgress: !!initialProgress, ceremony: this.ceremonyName });
|
|
1196
|
+
debug('Project Setup Questionnaire');
|
|
1197
|
+
|
|
1198
|
+
// Cost threshold protection — wrap callback to check running cost before each progress call
|
|
1199
|
+
if (this._costThreshold != null && this.progressCallback) {
|
|
1200
|
+
const _origCallback = this.progressCallback;
|
|
1201
|
+
this.progressCallback = async (...args) => {
|
|
1202
|
+
if (this._costThreshold != null && this._runningCost >= this._costThreshold) {
|
|
1203
|
+
if (this._costLimitReachedCallback) {
|
|
1204
|
+
this._costThreshold = null; // disable re-triggering
|
|
1205
|
+
await this._costLimitReachedCallback(this._runningCost);
|
|
1206
|
+
// returns → ceremony continues with limit disabled
|
|
1207
|
+
} else {
|
|
1208
|
+
throw new Error(`COST_LIMIT_EXCEEDED:${this._runningCost.toFixed(6)}`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return _origCallback(...args);
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
605
1214
|
|
|
606
1215
|
// 1. Read template
|
|
1216
|
+
debug('Reading template file', { templatePath: this.templatePath });
|
|
607
1217
|
const templateContent = fs.readFileSync(this.templatePath, 'utf8');
|
|
1218
|
+
debug('Template loaded', { size: templateContent.length });
|
|
608
1219
|
|
|
609
1220
|
// 2. Extract variables
|
|
1221
|
+
debug('Extracting variables from template');
|
|
610
1222
|
const variables = this.extractVariables(templateContent);
|
|
1223
|
+
debug('Variables extracted', { count: variables.length, names: variables.map(v => v.name) });
|
|
611
1224
|
|
|
612
1225
|
// 3. Initialize or restore progress
|
|
613
1226
|
let collectedValues = {};
|
|
614
1227
|
let answeredCount = 0;
|
|
615
1228
|
|
|
616
1229
|
if (initialProgress && initialProgress.collectedValues) {
|
|
1230
|
+
debug('Restoring from initial progress', { answeredCount: Object.keys(initialProgress.collectedValues).length });
|
|
617
1231
|
collectedValues = { ...initialProgress.collectedValues };
|
|
618
1232
|
answeredCount = Object.keys(collectedValues).length;
|
|
619
1233
|
|
|
620
1234
|
// Check if ALL answers are pre-filled (from REPL questionnaire)
|
|
621
1235
|
if (answeredCount === variables.length) {
|
|
622
|
-
|
|
1236
|
+
debug(`Using ${answeredCount} pre-filled answers from questionnaire`);
|
|
623
1237
|
|
|
624
|
-
// Use pre-filled answers, but
|
|
1238
|
+
// Use pre-filled answers, but check defaults or AI for skipped (null) answers
|
|
625
1239
|
for (const variable of variables) {
|
|
626
1240
|
if (collectedValues[variable.name] === null) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1241
|
+
// No section header — silent fill to avoid polluting terminal output during ceremony
|
|
1242
|
+
|
|
1243
|
+
// First, check if there's a default for this question
|
|
1244
|
+
const defaults = this.readDefaults();
|
|
1245
|
+
const defaultValue = defaults[variable.name];
|
|
1246
|
+
|
|
1247
|
+
if (defaultValue) {
|
|
1248
|
+
sendSuccess('Using default from settings');
|
|
1249
|
+
collectedValues[variable.name] = variable.isPlural
|
|
1250
|
+
? (Array.isArray(defaultValue) ? defaultValue : [defaultValue])
|
|
1251
|
+
: defaultValue;
|
|
1252
|
+
} else {
|
|
1253
|
+
// No default found, generate AI suggestion
|
|
1254
|
+
sendProgress('Generating AI suggestion...');
|
|
1255
|
+
const aiValue = await this.generateSuggestions(variable.name, variable.isPlural, collectedValues);
|
|
1256
|
+
collectedValues[variable.name] = aiValue || '';
|
|
1257
|
+
}
|
|
631
1258
|
}
|
|
632
1259
|
}
|
|
633
1260
|
} else {
|
|
634
|
-
|
|
1261
|
+
sendOutput(`Resuming with ${answeredCount}/${variables.length} questions already answered.\n`);
|
|
635
1262
|
|
|
636
1263
|
// Continue with normal interactive flow for remaining questions
|
|
637
1264
|
for (const variable of variables) {
|
|
@@ -677,15 +1304,1432 @@ Return the enhanced markdown document.`;
|
|
|
677
1304
|
}
|
|
678
1305
|
}
|
|
679
1306
|
|
|
1307
|
+
// Compute total stages based on what will actually run for this ceremony
|
|
1308
|
+
const _scValidation = this.ceremonyName === 'sponsor-call' && this.isValidationEnabled();
|
|
1309
|
+
const _T = 5 + (_scValidation ? 1 : 0);
|
|
1310
|
+
let _s = 0;
|
|
1311
|
+
|
|
1312
|
+
// Report questionnaire completion (with delay for UI update)
|
|
1313
|
+
await this.reportProgressWithDelay(`Stage ${++_s}/${_T}: Processing questionnaire answers...`, 'Processed questionnaire responses');
|
|
1314
|
+
|
|
680
1315
|
// 5. Replace variables in template
|
|
681
|
-
|
|
1316
|
+
await this.reportProgressWithDelay(`Stage ${++_s}/${_T}: Preparing project template...`, 'Template preparation complete');
|
|
1317
|
+
this.reportSubstep('Reading template: project.md');
|
|
682
1318
|
const templateWithValues = this.replaceVariables(templateContent, collectedValues);
|
|
1319
|
+
this.reportSubstep('Replaced 6 template variables');
|
|
1320
|
+
|
|
1321
|
+
// Preparation complete
|
|
1322
|
+
await this.reportProgressWithDelay(`Stage ${++_s}/${_T}: Preparing for documentation generation...`, 'Ready to generate documentation');
|
|
683
1323
|
|
|
684
1324
|
// 6. Enhance document with LLM
|
|
685
|
-
|
|
1325
|
+
await this.reportProgressWithDelay(`Stage ${++_s}/${_T}: Creating project documentation...`, 'Created project documentation');
|
|
1326
|
+
let finalDocument = await this.generateFinalDocument(templateWithValues, collectedValues);
|
|
1327
|
+
|
|
1328
|
+
// 7. Validate and improve documentation (if validation enabled)
|
|
1329
|
+
if (this.ceremonyName === 'sponsor-call' && this.isValidationEnabled()) {
|
|
1330
|
+
++_s;
|
|
1331
|
+
finalDocument = await this.iterativeValidation(finalDocument, 'documentation', collectedValues, _s, _T);
|
|
1332
|
+
}
|
|
686
1333
|
|
|
687
|
-
//
|
|
1334
|
+
// 8. Write documentation to file
|
|
688
1335
|
await this.writeDocument(finalDocument);
|
|
1336
|
+
this.reportSubstep('Wrapping up...');
|
|
1337
|
+
|
|
1338
|
+
// 11. Track token usage (only for sponsor-call)
|
|
1339
|
+
let tokenUsage = null;
|
|
1340
|
+
let cost = null;
|
|
1341
|
+
if (this.ceremonyName === 'sponsor-call') {
|
|
1342
|
+
({ tokenUsage, cost } = this.saveCurrentTokenTracking());
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// 12. Return comprehensive result
|
|
1346
|
+
// Save verification tracking summary
|
|
1347
|
+
if (this.verificationTracker) {
|
|
1348
|
+
try {
|
|
1349
|
+
this.verificationTracker.logCeremonySummary();
|
|
1350
|
+
const { jsonPath, summaryPath } = this.verificationTracker.saveToFile();
|
|
1351
|
+
|
|
1352
|
+
if (jsonPath && summaryPath) {
|
|
1353
|
+
debug('Verification tracking saved', { json: jsonPath, summary: summaryPath });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Clean up old verification logs (keep last 10)
|
|
1357
|
+
VerificationTracker.cleanupOldLogs(this.ceremonyName);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
console.error('Error saving verification tracking:', error.message);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
return {
|
|
1364
|
+
outputPath: this.outputPath,
|
|
1365
|
+
activities: this.activityLog,
|
|
1366
|
+
tokenUsage: tokenUsage,
|
|
1367
|
+
cost: cost,
|
|
1368
|
+
model: this._modelName,
|
|
1369
|
+
validationIssues: this.validationIssues
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Aggregate token usage from all providers, write to token-history.json, and cache on this instance.
|
|
1375
|
+
* Safe to call at any point (success or partial/cancelled run).
|
|
1376
|
+
* @returns {{ tokenUsage: Object|null, cost: Object|null }}
|
|
1377
|
+
*/
|
|
1378
|
+
saveCurrentTokenTracking() {
|
|
1379
|
+
let totalInput = 0;
|
|
1380
|
+
let totalOutput = 0;
|
|
1381
|
+
let totalCalls = 0;
|
|
1382
|
+
const stageBreakdown = {};
|
|
1383
|
+
|
|
1384
|
+
if (this._stageProviders) {
|
|
1385
|
+
for (const [cacheKey, provider] of Object.entries(this._stageProviders)) {
|
|
1386
|
+
if (typeof provider.getTokenUsage === 'function') {
|
|
1387
|
+
const usage = provider.getTokenUsage();
|
|
1388
|
+
totalInput += usage.inputTokens || 0;
|
|
1389
|
+
totalOutput += usage.outputTokens || 0;
|
|
1390
|
+
totalCalls += usage.totalCalls || 0;
|
|
1391
|
+
const stageName = cacheKey.split(':')[0];
|
|
1392
|
+
stageBreakdown[stageName] = { input: usage.inputTokens, output: usage.outputTokens, calls: usage.totalCalls };
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
if (this._validationProviders) {
|
|
1398
|
+
for (const [cacheKey, provider] of Object.entries(this._validationProviders)) {
|
|
1399
|
+
if (typeof provider.getTokenUsage === 'function') {
|
|
1400
|
+
const usage = provider.getTokenUsage();
|
|
1401
|
+
totalInput += usage.inputTokens || 0;
|
|
1402
|
+
totalOutput += usage.outputTokens || 0;
|
|
1403
|
+
totalCalls += usage.totalCalls || 0;
|
|
1404
|
+
const validationType = cacheKey.split(':')[1];
|
|
1405
|
+
stageBreakdown[`validation:${validationType}`] = { input: usage.inputTokens, output: usage.outputTokens, calls: usage.totalCalls };
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (totalInput === 0 && totalOutput === 0 && this.llmProvider && typeof this.llmProvider.getTokenUsage === 'function') {
|
|
1411
|
+
const usage = this.llmProvider.getTokenUsage();
|
|
1412
|
+
totalInput = usage.inputTokens || 0;
|
|
1413
|
+
totalOutput = usage.outputTokens || 0;
|
|
1414
|
+
totalCalls = usage.totalCalls || 0;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (totalInput === 0 && totalOutput === 0) return { tokenUsage: null, cost: null };
|
|
1418
|
+
|
|
1419
|
+
const cost = this.tokenTracker.calculateCost(totalInput, totalOutput, this._modelName);
|
|
1420
|
+
|
|
1421
|
+
this._lastTokenUsage = {
|
|
1422
|
+
inputTokens: totalInput,
|
|
1423
|
+
outputTokens: totalOutput,
|
|
1424
|
+
totalTokens: totalInput + totalOutput,
|
|
1425
|
+
totalCalls: totalCalls
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
const tokenUsage = {
|
|
1429
|
+
input: totalInput,
|
|
1430
|
+
output: totalOutput,
|
|
1431
|
+
total: totalInput + totalOutput,
|
|
1432
|
+
calls: totalCalls,
|
|
1433
|
+
stageBreakdown: stageBreakdown
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
return { tokenUsage, cost };
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Get token usage from last LLM execution
|
|
1441
|
+
* @returns {Object|null} Token usage object or null
|
|
1442
|
+
*/
|
|
1443
|
+
getLastTokenUsage() {
|
|
1444
|
+
return this._lastTokenUsage;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Generate hierarchical work items (Project → Epic → Story)
|
|
1449
|
+
* (Now used by Project Expansion ceremony)
|
|
1450
|
+
* @param {Object} collectedValues - Values from questionnaire
|
|
1451
|
+
*/
|
|
1452
|
+
async generateHierarchy(collectedValues) {
|
|
1453
|
+
const t0 = Date.now();
|
|
1454
|
+
debug('generateHierarchy called', { ceremony: this.ceremonyName, valuesCount: Object.keys(collectedValues).length });
|
|
1455
|
+
sendSectionHeader('Generating project hierarchy...');
|
|
1456
|
+
|
|
1457
|
+
// Read agent instructions
|
|
1458
|
+
const epicStoryDecomposerAgent = loadAgent('epic-story-decomposer.md', path.dirname(this.avcPath));
|
|
1459
|
+
|
|
1460
|
+
// 1. Decompose into Epics and Stories
|
|
1461
|
+
sendInfo('Stage 5/6: Decomposing features into Epics and Stories...');
|
|
1462
|
+
const decompositionPrompt = this.buildDecompositionPrompt(collectedValues);
|
|
1463
|
+
const hierarchy = await this.retryWithBackoff(
|
|
1464
|
+
() => this.llmProvider.generateJSON(decompositionPrompt, epicStoryDecomposerAgent),
|
|
1465
|
+
'hierarchy decomposition'
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
// Validate response structure
|
|
1469
|
+
if (!hierarchy.epics || !Array.isArray(hierarchy.epics)) {
|
|
1470
|
+
throw new Error('Invalid hierarchy response: missing epics array');
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
sendSuccess(`Generated ${hierarchy.epics.length} Epics with ${hierarchy.validation?.storyCount || 0} Stories`);
|
|
1474
|
+
|
|
1475
|
+
// 2. Write all files to disk
|
|
1476
|
+
sendSectionHeader('Stage 6/6: Writing files to disk...');
|
|
1477
|
+
await this.writeHierarchyToFiles(hierarchy, collectedValues);
|
|
1478
|
+
|
|
1479
|
+
// Display token usage statistics
|
|
1480
|
+
if (this.llmProvider) {
|
|
1481
|
+
const mainUsage = this.llmProvider.getTokenUsage();
|
|
1482
|
+
|
|
1483
|
+
sendSectionHeader('Token Usage:');
|
|
1484
|
+
|
|
1485
|
+
// Main provider usage
|
|
1486
|
+
console.log(` Main Provider (${this._providerName}):`);
|
|
1487
|
+
console.log(` Input: ${mainUsage.inputTokens.toLocaleString()} tokens`);
|
|
1488
|
+
console.log(` Output: ${mainUsage.outputTokens.toLocaleString()} tokens`);
|
|
1489
|
+
console.log(` Calls: ${mainUsage.totalCalls}`);
|
|
1490
|
+
|
|
1491
|
+
// Validation provider usage (if used)
|
|
1492
|
+
if (this.validationLLMProvider) {
|
|
1493
|
+
const validationUsage = this.validationLLMProvider.getTokenUsage();
|
|
1494
|
+
console.log(`\n Validation Provider (${this._validationProvider}):`);
|
|
1495
|
+
console.log(` Input: ${validationUsage.inputTokens.toLocaleString()} tokens`);
|
|
1496
|
+
console.log(` Output: ${validationUsage.outputTokens.toLocaleString()} tokens`);
|
|
1497
|
+
console.log(` Calls: ${validationUsage.totalCalls}`);
|
|
1498
|
+
|
|
1499
|
+
// Total
|
|
1500
|
+
const totalInput = mainUsage.inputTokens + validationUsage.inputTokens;
|
|
1501
|
+
const totalOutput = mainUsage.outputTokens + validationUsage.outputTokens;
|
|
1502
|
+
const totalCalls = mainUsage.totalCalls + validationUsage.totalCalls;
|
|
1503
|
+
|
|
1504
|
+
console.log(`\n Total:`);
|
|
1505
|
+
console.log(` Input: ${totalInput.toLocaleString()} tokens`);
|
|
1506
|
+
console.log(` Output: ${totalOutput.toLocaleString()} tokens`);
|
|
1507
|
+
console.log(` Calls: ${totalCalls}`);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Finalize run — tokens already saved per-call via addIncremental
|
|
1511
|
+
this.tokenTracker.finalizeRun(this.ceremonyName);
|
|
1512
|
+
|
|
1513
|
+
if (this.validationLLMProvider) {
|
|
1514
|
+
this.tokenTracker.finalizeRun(`${this.ceremonyName}-validation`);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
sendSuccess('Token history updated');
|
|
1518
|
+
const mainUsageFinal = this.llmProvider.getTokenUsage();
|
|
1519
|
+
debug('generateHierarchy complete', {
|
|
1520
|
+
duration: `${Date.now() - t0}ms`,
|
|
1521
|
+
epicCount: hierarchy?.epics?.length,
|
|
1522
|
+
mainProvider: { provider: this._providerName, inputTokens: mainUsageFinal.inputTokens, outputTokens: mainUsageFinal.outputTokens, calls: mainUsageFinal.totalCalls }
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Build prompt for Epic/Story decomposition
|
|
1529
|
+
* @param {Object} collectedValues - Questionnaire responses
|
|
1530
|
+
* @returns {string} Prompt for decomposition agent
|
|
1531
|
+
*/
|
|
1532
|
+
buildDecompositionPrompt(collectedValues) {
|
|
1533
|
+
const { INITIAL_SCOPE, TARGET_USERS, MISSION_STATEMENT, TECHNICAL_CONSIDERATIONS, SECURITY_AND_COMPLIANCE_REQUIREMENTS } = collectedValues;
|
|
1534
|
+
|
|
1535
|
+
return `Given the following project definition:
|
|
1536
|
+
|
|
1537
|
+
**Initial Scope (Features to Implement):**
|
|
1538
|
+
${INITIAL_SCOPE}
|
|
1539
|
+
|
|
1540
|
+
**Target Users:**
|
|
1541
|
+
${TARGET_USERS}
|
|
1542
|
+
|
|
1543
|
+
**Mission Statement:**
|
|
1544
|
+
${MISSION_STATEMENT}
|
|
1545
|
+
|
|
1546
|
+
**Technical Considerations:**
|
|
1547
|
+
${TECHNICAL_CONSIDERATIONS}
|
|
1548
|
+
|
|
1549
|
+
**Security and Compliance Requirements:**
|
|
1550
|
+
${SECURITY_AND_COMPLIANCE_REQUIREMENTS}
|
|
1551
|
+
|
|
1552
|
+
Decompose this project into Epics (domain-based groupings) and Stories (user-facing capabilities per Epic) — create as many as needed to fully cover the scope.
|
|
1553
|
+
|
|
1554
|
+
Return your response as JSON following the exact structure specified in your instructions.`;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Write hierarchy files to .avc/project/ directory
|
|
1559
|
+
* @param {Object} hierarchy - Decomposition result (epics array)
|
|
1560
|
+
* @param {Object} collectedValues - Questionnaire responses
|
|
1561
|
+
*/
|
|
1562
|
+
async writeHierarchyToFiles(hierarchy, collectedValues) {
|
|
1563
|
+
const projectPath = path.join(this.avcPath, 'project');
|
|
1564
|
+
|
|
1565
|
+
// Write Epic and Story files
|
|
1566
|
+
for (const epic of hierarchy.epics) {
|
|
1567
|
+
const epicDir = path.join(projectPath, epic.id);
|
|
1568
|
+
if (!fs.existsSync(epicDir)) {
|
|
1569
|
+
fs.mkdirSync(epicDir, { recursive: true });
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// Write Epic doc.md (initially empty, updated by retrospective ceremony)
|
|
1573
|
+
fs.writeFileSync(
|
|
1574
|
+
path.join(epicDir, 'doc.md'),
|
|
1575
|
+
`# ${epic.name}\n\n*Documentation will be added during implementation and retrospective ceremonies.*\n`,
|
|
1576
|
+
'utf8'
|
|
1577
|
+
);
|
|
1578
|
+
sendIndented(`${epic.id}/doc.md`);
|
|
1579
|
+
|
|
1580
|
+
// Write Epic work.json
|
|
1581
|
+
const epicWorkJson = {
|
|
1582
|
+
id: epic.id,
|
|
1583
|
+
name: epic.name,
|
|
1584
|
+
type: 'epic',
|
|
1585
|
+
domain: epic.domain,
|
|
1586
|
+
description: epic.description,
|
|
1587
|
+
features: epic.features,
|
|
1588
|
+
status: 'planned',
|
|
1589
|
+
dependencies: epic.dependencies || [],
|
|
1590
|
+
children: (epic.stories || []).map(s => s.id),
|
|
1591
|
+
metadata: {
|
|
1592
|
+
created: new Date().toISOString(),
|
|
1593
|
+
ceremony: 'sponsor-call'
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
fs.writeFileSync(
|
|
1597
|
+
path.join(epicDir, 'work.json'),
|
|
1598
|
+
JSON.stringify(epicWorkJson, null, 2),
|
|
1599
|
+
'utf8'
|
|
1600
|
+
);
|
|
1601
|
+
sendIndented(`${epic.id}/work.json`);
|
|
1602
|
+
|
|
1603
|
+
// Write Story files
|
|
1604
|
+
for (const story of epic.stories || []) {
|
|
1605
|
+
const storyDir = path.join(epicDir, story.id);
|
|
1606
|
+
if (!fs.existsSync(storyDir)) {
|
|
1607
|
+
fs.mkdirSync(storyDir, { recursive: true });
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Write Story doc.md (initially empty, updated by retrospective ceremony)
|
|
1611
|
+
fs.writeFileSync(
|
|
1612
|
+
path.join(storyDir, 'doc.md'),
|
|
1613
|
+
`# ${story.name}\n\n*Documentation will be added during implementation and retrospective ceremonies.*\n`,
|
|
1614
|
+
'utf8'
|
|
1615
|
+
);
|
|
1616
|
+
sendIndented(`${story.id}/doc.md`);
|
|
1617
|
+
|
|
1618
|
+
// Write Story work.json
|
|
1619
|
+
const storyWorkJson = {
|
|
1620
|
+
id: story.id,
|
|
1621
|
+
name: story.name,
|
|
1622
|
+
type: 'story',
|
|
1623
|
+
userType: story.userType,
|
|
1624
|
+
description: story.description,
|
|
1625
|
+
acceptance: story.acceptance,
|
|
1626
|
+
status: 'planned',
|
|
1627
|
+
dependencies: story.dependencies || [],
|
|
1628
|
+
children: [],
|
|
1629
|
+
metadata: {
|
|
1630
|
+
created: new Date().toISOString(),
|
|
1631
|
+
ceremony: 'sponsor-call'
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
fs.writeFileSync(
|
|
1635
|
+
path.join(storyDir, 'work.json'),
|
|
1636
|
+
JSON.stringify(storyWorkJson, null, 2),
|
|
1637
|
+
'utf8'
|
|
1638
|
+
);
|
|
1639
|
+
sendIndented(`${story.id}/work.json`);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
console.log(''); // Empty line between epics
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
sendSuccess(`Hierarchy written to ${projectPath}/`);
|
|
1646
|
+
sendSectionHeader(`Summary:`);
|
|
1647
|
+
console.log(` • ${hierarchy.epics.length} Epics (doc.md + work.json each)`);
|
|
1648
|
+
console.log(` • ${hierarchy.validation?.storyCount || 0} Stories (doc.md + work.json each)`);
|
|
1649
|
+
|
|
1650
|
+
const epicCount = hierarchy.epics.length;
|
|
1651
|
+
const storyCount = hierarchy.validation?.storyCount || 0;
|
|
1652
|
+
const totalFiles = (2 * epicCount) + (2 * storyCount); // Epic: 2 each, Story: 2 each
|
|
1653
|
+
console.log(` • ${totalFiles} files created\n`);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Get validation settings from ceremony configuration
|
|
1658
|
+
*/
|
|
1659
|
+
getValidationSettings() {
|
|
1660
|
+
try {
|
|
1661
|
+
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
1662
|
+
const ceremony = config.settings?.ceremonies?.find(c => c.name === this.ceremonyName);
|
|
1663
|
+
|
|
1664
|
+
return ceremony?.validation || {
|
|
1665
|
+
enabled: true,
|
|
1666
|
+
maxIterations: 3,
|
|
1667
|
+
acceptanceThreshold: 95,
|
|
1668
|
+
skipOnCriticalIssues: false
|
|
1669
|
+
};
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
// Default settings if config can't be read
|
|
1672
|
+
return {
|
|
1673
|
+
enabled: true,
|
|
1674
|
+
maxIterations: 3,
|
|
1675
|
+
acceptanceThreshold: 95,
|
|
1676
|
+
skipOnCriticalIssues: false
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Check if validation is enabled for this ceremony
|
|
1683
|
+
*/
|
|
1684
|
+
isValidationEnabled() {
|
|
1685
|
+
const settings = this.getValidationSettings();
|
|
1686
|
+
return settings.enabled !== false;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Validate documentation (doc.md) using validator-documentation agent
|
|
1691
|
+
*/
|
|
1692
|
+
async validateDocument(docContent, questionnaire) {
|
|
1693
|
+
// Get validation type-specific provider for documentation
|
|
1694
|
+
let provider;
|
|
1695
|
+
try {
|
|
1696
|
+
provider = await this.getValidationProviderForTypeInstance('documentation');
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
sendWarning('Skipping validation (validation provider not available)');
|
|
1699
|
+
return {
|
|
1700
|
+
validationStatus: 'acceptable',
|
|
1701
|
+
overallScore: 75,
|
|
1702
|
+
issues: [],
|
|
1703
|
+
strengths: ['Validation skipped - validation provider not available'],
|
|
1704
|
+
improvementPriorities: [],
|
|
1705
|
+
readyForPublication: true
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (!provider || typeof provider.generateJSON !== 'function') {
|
|
1710
|
+
sendWarning('Skipping validation (validation provider not available)');
|
|
1711
|
+
return {
|
|
1712
|
+
validationStatus: 'acceptable',
|
|
1713
|
+
overallScore: 75,
|
|
1714
|
+
issues: [],
|
|
1715
|
+
strengths: ['Validation skipped - validation provider not available'],
|
|
1716
|
+
improvementPriorities: [],
|
|
1717
|
+
readyForPublication: true
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Read validator agent instructions
|
|
1722
|
+
const validatorAgent = this.loadAgentInstructions('validator-documentation.md');
|
|
1723
|
+
|
|
1724
|
+
// Build validation prompt
|
|
1725
|
+
let prompt = `Validate the following project documentation:\n\n`;
|
|
1726
|
+
prompt += `**doc.md Content:**\n\`\`\`markdown\n${docContent}\n\`\`\`\n\n`;
|
|
1727
|
+
|
|
1728
|
+
// Enhance prompt with verification feedback if available
|
|
1729
|
+
prompt = this.enhancePromptWithFeedback(prompt, 'validator-documentation');
|
|
1730
|
+
|
|
1731
|
+
const deploymentTarget = questionnaire.DEPLOYMENT_TARGET || '';
|
|
1732
|
+
const isLocalDeployment = /local|localhost|dev\s*machine|on.?prem/i.test(deploymentTarget);
|
|
1733
|
+
|
|
1734
|
+
prompt += `**Original Questionnaire Data:**\n`;
|
|
1735
|
+
prompt += `- MISSION_STATEMENT: ${questionnaire.MISSION_STATEMENT || 'N/A'}\n`;
|
|
1736
|
+
prompt += `- TARGET_USERS: ${questionnaire.TARGET_USERS || 'N/A'}\n`;
|
|
1737
|
+
prompt += `- INITIAL_SCOPE: ${questionnaire.INITIAL_SCOPE || 'N/A'}\n`;
|
|
1738
|
+
prompt += `- TECHNICAL_CONSIDERATIONS: ${questionnaire.TECHNICAL_CONSIDERATIONS || 'N/A'}\n`;
|
|
1739
|
+
prompt += `- DEPLOYMENT_TARGET: ${deploymentTarget || 'N/A'}\n`;
|
|
1740
|
+
prompt += `- TECHNICAL_EXCLUSIONS: ${questionnaire.TECHNICAL_EXCLUSIONS || 'None'}\n`;
|
|
1741
|
+
prompt += `- SECURITY_AND_COMPLIANCE_REQUIREMENTS: ${questionnaire.SECURITY_AND_COMPLIANCE_REQUIREMENTS || 'N/A'}\n\n`;
|
|
1742
|
+
|
|
1743
|
+
if (isLocalDeployment) {
|
|
1744
|
+
prompt += `**DEPLOYMENT ALIGNMENT CHECK — CRITICAL:**\n`;
|
|
1745
|
+
prompt += `The user chose LOCAL deployment ("${deploymentTarget}").\n`;
|
|
1746
|
+
prompt += `Flag as a CRITICAL contentIssue (category: "consistency") any mention of:\n`;
|
|
1747
|
+
prompt += `- Cloud providers: AWS, GCP, Google Cloud, Azure, DigitalOcean, Cloudflare, etc.\n`;
|
|
1748
|
+
prompt += `- Container orchestration: Kubernetes, ECS, EKS, GKE, AKS, Fargate, etc.\n`;
|
|
1749
|
+
prompt += `- Managed cloud services: RDS, S3, CloudFront, Lambda, Firebase, Supabase, etc.\n`;
|
|
1750
|
+
prompt += `- CI/CD pipelines: GitHub Actions, GitLab CI, CircleCI, Jenkins, etc. (unless user explicitly mentioned them in TECHNICAL_CONSIDERATIONS)\n`;
|
|
1751
|
+
prompt += `- Infrastructure as Code: Terraform, CloudFormation, Pulumi, etc.\n`;
|
|
1752
|
+
prompt += `These contradict the user's stated local deployment target and must be removed or replaced with local equivalents.\n\n`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (questionnaire.TECHNICAL_EXCLUSIONS) {
|
|
1756
|
+
prompt += `**EXCLUSION ALIGNMENT CHECK — CRITICAL:**\n`;
|
|
1757
|
+
prompt += `The user explicitly excluded: ${questionnaire.TECHNICAL_EXCLUSIONS}\n`;
|
|
1758
|
+
prompt += `Flag as a CRITICAL contentIssue (category: "consistency") any mention of an excluded technology being recommended, suggested, or used in the document.\n\n`;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
prompt += `Return your validation as JSON following the exact structure specified in your instructions.`;
|
|
1762
|
+
|
|
1763
|
+
// Call validation provider for validation
|
|
1764
|
+
const validation = await this.retryWithBackoff(
|
|
1765
|
+
() => provider.generateJSON(prompt, validatorAgent),
|
|
1766
|
+
'documentation validation'
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1769
|
+
// Post-process verification of validator output
|
|
1770
|
+
const verifier = new LLMVerifier(provider, 'validator-documentation', this.verificationTracker);
|
|
1771
|
+
console.log('[DEBUG] validateDocument - Input to verifier (preview):', JSON.stringify(validation, null, 2).substring(0, 200));
|
|
1772
|
+
const verificationResult = await verifier.verify(JSON.stringify(validation, null, 2));
|
|
1773
|
+
console.log('[DEBUG] validateDocument - Verification result:', {
|
|
1774
|
+
rulesApplied: verificationResult.rulesApplied.map(r => r.id),
|
|
1775
|
+
contentLength: verificationResult.content.length,
|
|
1776
|
+
contentPreview: verificationResult.content.substring(0, 300)
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
if (verificationResult.rulesApplied.length > 0) {
|
|
1780
|
+
// Store feedback for agent learning
|
|
1781
|
+
this.verificationFeedback['validator-documentation'] = verificationResult.rulesApplied.map(rule => ({
|
|
1782
|
+
ruleId: rule.id,
|
|
1783
|
+
ruleName: rule.name,
|
|
1784
|
+
severity: rule.severity
|
|
1785
|
+
}));
|
|
1786
|
+
|
|
1787
|
+
// Collect for final ceremony summary
|
|
1788
|
+
for (const rule of verificationResult.rulesApplied) {
|
|
1789
|
+
this.validationIssues.push({ stage: 'Documentation Validation', ruleId: rule.id, name: rule.name, severity: rule.severity, description: rule.description });
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Parse corrected JSON back to object
|
|
1793
|
+
console.log('[DEBUG] validateDocument - Attempting to parse verification result as JSON');
|
|
1794
|
+
|
|
1795
|
+
// Strip markdown code fences if present
|
|
1796
|
+
const cleanedContent = this.stripMarkdownCodeFences(verificationResult.content);
|
|
1797
|
+
console.log('[DEBUG] validateDocument - After stripping code fences:', {
|
|
1798
|
+
originalLength: verificationResult.content.length,
|
|
1799
|
+
cleanedLength: cleanedContent.length,
|
|
1800
|
+
cleanedPreview: cleanedContent.substring(0, 200)
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
try {
|
|
1804
|
+
const parsed = JSON.parse(cleanedContent);
|
|
1805
|
+
console.log('[DEBUG] validateDocument - Successfully parsed JSON');
|
|
1806
|
+
return parsed;
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
console.error('[ERROR] validateDocument - JSON parse failed:', error.message);
|
|
1809
|
+
console.error('[ERROR] validateDocument - Raw content that failed to parse:', cleanedContent);
|
|
1810
|
+
throw error;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
return validation;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Improve documentation based on validation feedback
|
|
1819
|
+
*/
|
|
1820
|
+
async improveDocument(docContent, validationResult, questionnaire) {
|
|
1821
|
+
// Use refinement provider (separate model from validator for improve/refine step)
|
|
1822
|
+
let provider;
|
|
1823
|
+
try {
|
|
1824
|
+
provider = await this.getRefinementProviderInstance();
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
sendWarning('Skipping improvement (refinement provider not available)');
|
|
1827
|
+
return docContent;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if (!provider || typeof provider.generateText !== 'function') {
|
|
1831
|
+
sendWarning('Skipping improvement (validation provider not available)');
|
|
1832
|
+
return docContent;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Read documentation creator agent
|
|
1836
|
+
const creatorAgent = this.loadAgentInstructions('project-documentation-creator.md');
|
|
1837
|
+
|
|
1838
|
+
// Build improvement prompt with validation feedback
|
|
1839
|
+
let prompt = `Improve the following project documentation based on validation feedback:\n\n`;
|
|
1840
|
+
|
|
1841
|
+
prompt += `**Current doc.md:**\n\`\`\`markdown\n${docContent}\n\`\`\`\n\n`;
|
|
1842
|
+
|
|
1843
|
+
prompt += `**Validation Feedback:**\n`;
|
|
1844
|
+
prompt += `- Overall Score: ${validationResult.overallScore}/100\n`;
|
|
1845
|
+
prompt += `- Status: ${validationResult.validationStatus}\n\n`;
|
|
1846
|
+
|
|
1847
|
+
if (validationResult.structuralIssues && validationResult.structuralIssues.length > 0) {
|
|
1848
|
+
prompt += `**Structural Issues to Fix:**\n`;
|
|
1849
|
+
validationResult.structuralIssues.forEach((issue, i) => {
|
|
1850
|
+
prompt += `${i + 1}. [${issue.severity.toUpperCase()}] ${issue.section}: ${issue.issue}\n`;
|
|
1851
|
+
prompt += ` Suggestion: ${issue.suggestion}\n`;
|
|
1852
|
+
});
|
|
1853
|
+
prompt += `\n`;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (validationResult.contentIssues && validationResult.contentIssues.length > 0) {
|
|
1857
|
+
prompt += `**Content Issues to Fix:**\n`;
|
|
1858
|
+
validationResult.contentIssues.forEach((issue, i) => {
|
|
1859
|
+
prompt += `${i + 1}. [${issue.severity.toUpperCase()}] ${issue.section}: ${issue.description}\n`;
|
|
1860
|
+
prompt += ` Suggestion: ${issue.suggestion}\n`;
|
|
1861
|
+
});
|
|
1862
|
+
prompt += `\n`;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (validationResult.applicationFlowGaps && validationResult.applicationFlowGaps.length > 0) {
|
|
1866
|
+
prompt += `**Application Flow Gaps to Address:**\n`;
|
|
1867
|
+
validationResult.applicationFlowGaps.forEach((gap, i) => {
|
|
1868
|
+
prompt += `${i + 1}. Missing: ${gap.missingFlow}\n`;
|
|
1869
|
+
prompt += ` Impact: ${gap.impact}\n`;
|
|
1870
|
+
prompt += ` Suggestion: ${gap.suggestion}\n`;
|
|
1871
|
+
});
|
|
1872
|
+
prompt += `\n`;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
prompt += `**Improvement Priorities:**\n`;
|
|
1876
|
+
validationResult.improvementPriorities.forEach((priority, i) => {
|
|
1877
|
+
prompt += `${i + 1}. ${priority}\n`;
|
|
1878
|
+
});
|
|
1879
|
+
prompt += `\n`;
|
|
1880
|
+
|
|
1881
|
+
prompt += `**Original Questionnaire Data:**\n`;
|
|
1882
|
+
prompt += `- MISSION_STATEMENT: ${questionnaire.MISSION_STATEMENT || 'N/A'}\n`;
|
|
1883
|
+
prompt += `- TARGET_USERS: ${questionnaire.TARGET_USERS || 'N/A'}\n`;
|
|
1884
|
+
prompt += `- INITIAL_SCOPE: ${questionnaire.INITIAL_SCOPE || 'N/A'}\n`;
|
|
1885
|
+
prompt += `- TECHNICAL_CONSIDERATIONS: ${questionnaire.TECHNICAL_CONSIDERATIONS || 'N/A'}\n`;
|
|
1886
|
+
prompt += `- SECURITY_AND_COMPLIANCE_REQUIREMENTS: ${questionnaire.SECURITY_AND_COMPLIANCE_REQUIREMENTS || 'N/A'}\n\n`;
|
|
1887
|
+
|
|
1888
|
+
prompt += `Generate an improved version of the project documentation that addresses all the issues identified above. `;
|
|
1889
|
+
prompt += `Maintain the existing strengths while fixing the identified problems. `;
|
|
1890
|
+
prompt += `Return ONLY the improved markdown content, not wrapped in JSON.`;
|
|
1891
|
+
|
|
1892
|
+
// Call validation provider to regenerate documentation
|
|
1893
|
+
const improvedDoc = await this.retryWithBackoff(
|
|
1894
|
+
() => provider.generateText(prompt, creatorAgent),
|
|
1895
|
+
'documentation improvement'
|
|
1896
|
+
);
|
|
1897
|
+
|
|
1898
|
+
return improvedDoc;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
/**
|
|
1903
|
+
* Iterative validation and improvement loop
|
|
1904
|
+
*/
|
|
1905
|
+
async iterativeValidation(content, type, questionnaire, stageNum = null, stageTotal = null) {
|
|
1906
|
+
const settings = this.getValidationSettings();
|
|
1907
|
+
const maxIterations = settings.maxIterations || 100;
|
|
1908
|
+
const threshold = settings.acceptanceThreshold || 95;
|
|
1909
|
+
|
|
1910
|
+
let currentContent = content;
|
|
1911
|
+
let iteration = 0;
|
|
1912
|
+
|
|
1913
|
+
while (iteration < maxIterations) {
|
|
1914
|
+
// Emit fractional stage progress so the progress bar moves within this stage
|
|
1915
|
+
if (stageNum !== null && stageTotal !== null) {
|
|
1916
|
+
const current = parseFloat((stageNum + iteration * 0.2).toFixed(1));
|
|
1917
|
+
await this.reportProgressWithDelay(
|
|
1918
|
+
`Stage ${current}/${stageTotal}: Validating documentation (pass ${iteration + 1})...`,
|
|
1919
|
+
null, 50
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
// Report validation iteration progress
|
|
1923
|
+
this.reportSubstep(`Validation ${iteration + 1}/${maxIterations}: Validating Project Brief structure...`);
|
|
1924
|
+
await this.reportDetail(`Calling ${this.validationLLMProvider?.model || 'LLM'} to validate…`);
|
|
1925
|
+
const issueCountBefore = this.validationIssues.length;
|
|
1926
|
+
const validation = await this.withHeartbeat(
|
|
1927
|
+
() => this.validateDocument(currentContent, questionnaire),
|
|
1928
|
+
(elapsed) => {
|
|
1929
|
+
if (elapsed < 15) return `Checking structure and completeness…`;
|
|
1930
|
+
if (elapsed < 30) return `Reviewing content quality…`;
|
|
1931
|
+
if (elapsed < 45) return `Analyzing gaps and issues…`;
|
|
1932
|
+
return `Validating documentation… (${elapsed}s)`;
|
|
1933
|
+
},
|
|
1934
|
+
15000
|
|
1935
|
+
);
|
|
1936
|
+
// Tag newly-added issues with the current iteration number
|
|
1937
|
+
for (let i = issueCountBefore; i < this.validationIssues.length; i++) {
|
|
1938
|
+
this.validationIssues[i].iteration = iteration + 1;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
this.reportSubstep('Analyzing validation results...');
|
|
1942
|
+
|
|
1943
|
+
// Report token usage
|
|
1944
|
+
if (this.validationLLMProvider && typeof this.validationLLMProvider.getTokenUsage === 'function') {
|
|
1945
|
+
const usage = this.validationLLMProvider.getTokenUsage();
|
|
1946
|
+
this.reportSubstep('Validation complete', {
|
|
1947
|
+
tokensUsed: {
|
|
1948
|
+
input: usage.inputTokens,
|
|
1949
|
+
output: usage.outputTokens
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
await this.reportDetail(`${usage.inputTokens.toLocaleString()} in · ${usage.outputTokens.toLocaleString()} out tokens`);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// Log validation results to debug (not console)
|
|
1956
|
+
const issues = validation.issues || validation.contentIssues || [];
|
|
1957
|
+
const structuralIssues = validation.structuralIssues || [];
|
|
1958
|
+
const flowGaps = validation.applicationFlowGaps || [];
|
|
1959
|
+
const allIssues = [...issues, ...structuralIssues];
|
|
1960
|
+
|
|
1961
|
+
debug(`Score: ${validation.overallScore}/100`, {
|
|
1962
|
+
status: validation.validationStatus,
|
|
1963
|
+
issues: allIssues.length,
|
|
1964
|
+
flowGaps: flowGaps.length
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
await this.reportDetail(`Score: ${validation.overallScore ?? '?'}/100 — ${allIssues.length} issue(s) found`);
|
|
1968
|
+
|
|
1969
|
+
// Check if ready — also accept immediately if no issues found at all
|
|
1970
|
+
const noIssues = allIssues.length === 0 && flowGaps.length === 0;
|
|
1971
|
+
if (noIssues || (validation.readyForPublication && validation.overallScore >= threshold)) {
|
|
1972
|
+
const reason = noIssues ? 'no issues found' : `score ≥ ${threshold}`;
|
|
1973
|
+
await this.reportDetail(`✓ Accepted (${reason})`);
|
|
1974
|
+
debug(`${type} passed validation`);
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// Check if max iterations reached
|
|
1979
|
+
if (iteration + 1 >= maxIterations) {
|
|
1980
|
+
await this.reportDetail(`Max iterations reached — accepting current version`);
|
|
1981
|
+
debug('Max iterations reached. Accepting current version.');
|
|
1982
|
+
break;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Improve
|
|
1986
|
+
debug(`Improving ${type} based on feedback`);
|
|
1987
|
+
if (stageNum !== null && stageTotal !== null) {
|
|
1988
|
+
const current = parseFloat((stageNum + iteration * 0.2 + 0.1).toFixed(1));
|
|
1989
|
+
await this.reportProgressWithDelay(
|
|
1990
|
+
`Stage ${current}/${stageTotal}: Improving documentation (pass ${iteration + 1})...`,
|
|
1991
|
+
null, 50
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
this.reportSubstep(`Improving Project Brief based on validation...`);
|
|
1995
|
+
await this.reportDetail(`Calling ${this._refinementModel || this.validationLLMProvider?.model || 'LLM'} to improve…`);
|
|
1996
|
+
currentContent = await this.withHeartbeat(
|
|
1997
|
+
() => this.improveDocument(currentContent, validation, questionnaire),
|
|
1998
|
+
(elapsed) => {
|
|
1999
|
+
if (elapsed < 15) return `Applying structural improvements…`;
|
|
2000
|
+
if (elapsed < 30) return `Enhancing content quality…`;
|
|
2001
|
+
if (elapsed < 45) return `Resolving identified issues…`;
|
|
2002
|
+
return `Improving Project Brief… (${elapsed}s)`;
|
|
2003
|
+
},
|
|
2004
|
+
15000
|
|
2005
|
+
);
|
|
2006
|
+
|
|
2007
|
+
// Report token usage after improvement
|
|
2008
|
+
if (this.validationLLMProvider && typeof this.validationLLMProvider.getTokenUsage === 'function') {
|
|
2009
|
+
const usage = this.validationLLMProvider.getTokenUsage();
|
|
2010
|
+
this.reportSubstep('Applying improvements...', {
|
|
2011
|
+
tokensUsed: {
|
|
2012
|
+
input: usage.inputTokens,
|
|
2013
|
+
output: usage.outputTokens
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
await this.reportDetail(`${usage.inputTokens.toLocaleString()} in · ${usage.outputTokens.toLocaleString()} out tokens`);
|
|
2017
|
+
} else {
|
|
2018
|
+
this.reportSubstep('Applying improvements...');
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
iteration++;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
return currentContent;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* Get architecture recommendations based on mission statement and initial scope
|
|
2029
|
+
* @param {string} missionStatement - The project's mission statement
|
|
2030
|
+
* @param {string} initialScope - The initial scope/features
|
|
2031
|
+
* @param {Object|null} databaseRecommendation - Optional database recommendation context
|
|
2032
|
+
* @returns {Promise<Array>} Array of architecture recommendations
|
|
2033
|
+
*/
|
|
2034
|
+
async getArchitectureRecommendations(missionStatement, initialScope, databaseContext = null, deploymentStrategy = null) {
|
|
2035
|
+
debug('getArchitectureRecommendations called', {
|
|
2036
|
+
missionStatement,
|
|
2037
|
+
initialScope,
|
|
2038
|
+
hasDatabaseContext: !!databaseContext,
|
|
2039
|
+
userChoice: databaseContext?.userChoice,
|
|
2040
|
+
deploymentStrategy
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
try {
|
|
2044
|
+
// Get stage-specific provider for architecture recommendation
|
|
2045
|
+
const provider = await this.getProviderForStageInstance('architecture-recommendation');
|
|
2046
|
+
|
|
2047
|
+
if (!provider || typeof provider.generateJSON !== 'function') {
|
|
2048
|
+
throw new Error('Architecture recommendation provider not available');
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// Read agent instructions
|
|
2052
|
+
debug('Loading architecture-recommender.md agent');
|
|
2053
|
+
const architectureRecommenderAgent = loadAgent('architecture-recommender.md', path.dirname(this.avcPath));
|
|
2054
|
+
|
|
2055
|
+
// Build prompt
|
|
2056
|
+
let prompt = `Given the following project definition:
|
|
2057
|
+
|
|
2058
|
+
**Mission Statement:**
|
|
2059
|
+
${missionStatement}
|
|
2060
|
+
|
|
2061
|
+
**Initial Scope (Features to Implement):**
|
|
2062
|
+
${initialScope}`;
|
|
2063
|
+
|
|
2064
|
+
// Add database context if available (new comparison format)
|
|
2065
|
+
if (databaseContext?.comparison) {
|
|
2066
|
+
prompt += `
|
|
2067
|
+
|
|
2068
|
+
**Database Context:**`;
|
|
2069
|
+
|
|
2070
|
+
if (databaseContext.userChoice) {
|
|
2071
|
+
const chosenOption = databaseContext.userChoice === 'sql' ? databaseContext.comparison.sqlOption : databaseContext.comparison.nosqlOption;
|
|
2072
|
+
prompt += `
|
|
2073
|
+
- User's Choice: ${databaseContext.userChoice.toUpperCase()} (${chosenOption.database})
|
|
2074
|
+
- Specific Version: ${chosenOption.specificVersion || chosenOption.database}`;
|
|
2075
|
+
|
|
2076
|
+
if (chosenOption.estimatedCosts) {
|
|
2077
|
+
prompt += `
|
|
2078
|
+
- Estimated Monthly Cost: ${chosenOption.estimatedCosts.monthly}`;
|
|
2079
|
+
}
|
|
2080
|
+
} else {
|
|
2081
|
+
// No user choice yet, provide both options
|
|
2082
|
+
prompt += `
|
|
2083
|
+
- SQL Option: ${databaseContext.comparison.sqlOption.database} (~${databaseContext.comparison.sqlOption.estimatedCosts?.monthly || 'TBD'}/mo)
|
|
2084
|
+
- NoSQL Option: ${databaseContext.comparison.nosqlOption.database} (~${databaseContext.comparison.nosqlOption.estimatedCosts?.monthly || 'TBD'}/mo)
|
|
2085
|
+
- Note: User has not chosen yet, provide architectures compatible with both`;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
if (databaseContext.keyMetrics) {
|
|
2089
|
+
prompt += `
|
|
2090
|
+
- Read/Write Ratio: ${databaseContext.keyMetrics.estimatedReadWriteRatio || 'Not specified'}
|
|
2091
|
+
- Expected Throughput: ${databaseContext.keyMetrics.expectedThroughput || 'Not specified'}
|
|
2092
|
+
- Data Complexity: ${databaseContext.keyMetrics.dataComplexity || 'Not specified'}`;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Add deployment strategy context if provided
|
|
2097
|
+
if (deploymentStrategy === 'local-mvp') {
|
|
2098
|
+
prompt += `
|
|
2099
|
+
|
|
2100
|
+
**Deployment Strategy:** Local MVP First
|
|
2101
|
+
|
|
2102
|
+
CRITICAL FILTERING REQUIREMENT:
|
|
2103
|
+
- Return ONLY local development architectures (no cloud-specific services)
|
|
2104
|
+
- Every architecture MUST include a migrationPath object with cloud migration details
|
|
2105
|
+
- NO Lambda, ECS, AKS, GKE, or other cloud-managed services
|
|
2106
|
+
- Focus on: Docker Compose, localhost setups, local databases
|
|
2107
|
+
|
|
2108
|
+
Required architectures to consider:
|
|
2109
|
+
1. Docker Compose full-stack (PostgreSQL/MongoDB, backend, frontend)
|
|
2110
|
+
2. Lightweight localhost setup (SQLite/JSON, Express/Flask, React dev server)
|
|
2111
|
+
3. Framework-specific local development (Django, Rails, Next.js dev)
|
|
2112
|
+
|
|
2113
|
+
Each architecture must include:
|
|
2114
|
+
- Clear "zero cloud costs" messaging
|
|
2115
|
+
- Production parity explanation (Docker vs simple localhost)
|
|
2116
|
+
- Specific migration path to 2-3 cloud architectures
|
|
2117
|
+
- Estimated migration effort (days) and complexity level
|
|
2118
|
+
|
|
2119
|
+
Example architecture structure:
|
|
2120
|
+
{
|
|
2121
|
+
"name": "Local Docker Compose Full-Stack",
|
|
2122
|
+
"description": "...runs entirely on your machine with zero cloud costs...Ready to migrate to AWS ECS, Azure Container Apps, or GCP Cloud Run when ready for production...",
|
|
2123
|
+
"requiresCloudProvider": false,
|
|
2124
|
+
"bestFor": "MVP development with production-like local environment",
|
|
2125
|
+
"migrationPath": {
|
|
2126
|
+
"readyForCloud": true,
|
|
2127
|
+
"suggestedCloudArchitectures": ["AWS ECS", "Azure Container Apps", "GCP Cloud Run"],
|
|
2128
|
+
"estimatedMigrationEffort": "2-3 days",
|
|
2129
|
+
"migrationComplexity": "Medium",
|
|
2130
|
+
"keyMigrationSteps": ["Set up managed database", "Create container registry", "Deploy to cloud service"]
|
|
2131
|
+
}
|
|
2132
|
+
}`;
|
|
2133
|
+
} else if (deploymentStrategy === 'cloud') {
|
|
2134
|
+
prompt += `
|
|
2135
|
+
|
|
2136
|
+
**Deployment Strategy:** Cloud Deployment
|
|
2137
|
+
|
|
2138
|
+
CRITICAL FILTERING REQUIREMENT:
|
|
2139
|
+
- Return ONLY cloud-native architectures (AWS/Azure/GCP managed services or PaaS)
|
|
2140
|
+
- NO local development options (Docker Compose, localhost setups)
|
|
2141
|
+
- Focus on: Serverless, containers, managed services, PaaS platforms
|
|
2142
|
+
|
|
2143
|
+
Required considerations:
|
|
2144
|
+
- Serverless options (Lambda, Cloud Functions, Cloud Run)
|
|
2145
|
+
- Container orchestration (ECS, AKS, GKE)
|
|
2146
|
+
- PaaS platforms (Vercel, Railway, Render for simpler projects)
|
|
2147
|
+
- Managed databases (RDS, DynamoDB, Atlas, Cosmos DB)
|
|
2148
|
+
|
|
2149
|
+
Each architecture must include:
|
|
2150
|
+
- Specific cloud services and managed offerings
|
|
2151
|
+
- Estimated monthly costs (low/medium/high traffic scenarios)
|
|
2152
|
+
- Auto-scaling and managed infrastructure benefits
|
|
2153
|
+
- Production-ready from day one messaging
|
|
2154
|
+
|
|
2155
|
+
Example architecture structure:
|
|
2156
|
+
{
|
|
2157
|
+
"name": "Serverless Backend + SPA on AWS",
|
|
2158
|
+
"description": "AWS Lambda for backend API, API Gateway for routing, DynamoDB for database, S3 + CloudFront for frontend. Scales automatically, pay only for usage...",
|
|
2159
|
+
"requiresCloudProvider": true,
|
|
2160
|
+
"bestFor": "Scalable APIs with variable traffic, cost optimization",
|
|
2161
|
+
"estimatedMonthlyCost": {
|
|
2162
|
+
"low": "$10-30 (< 10K requests/day)",
|
|
2163
|
+
"medium": "$50-150 (10K-100K requests/day)",
|
|
2164
|
+
"high": "$200-500 (100K+ requests/day)"
|
|
2165
|
+
}
|
|
2166
|
+
}`;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
prompt += `
|
|
2170
|
+
|
|
2171
|
+
Analyze this project and recommend 3-5 deployment architectures that best fit these requirements.${databaseContext ? ' Consider the database context when recommending deployment patterns and cloud services.' : ''}${deploymentStrategy ? ` IMPORTANT: Follow the deployment strategy filtering requirements above - return ONLY ${deploymentStrategy === 'local-mvp' ? 'local' : 'cloud'} architectures.` : ''}
|
|
2172
|
+
|
|
2173
|
+
Return your response as JSON following the exact structure specified in your instructions.`;
|
|
2174
|
+
|
|
2175
|
+
debug('Calling LLM for architecture recommendations');
|
|
2176
|
+
const result = await this.retryWithBackoff(
|
|
2177
|
+
() => provider.generateJSON(prompt, architectureRecommenderAgent),
|
|
2178
|
+
'architecture recommendations'
|
|
2179
|
+
);
|
|
2180
|
+
|
|
2181
|
+
debug('Architecture recommendations received', { count: result.architectures?.length });
|
|
2182
|
+
|
|
2183
|
+
// Validate response structure
|
|
2184
|
+
if (!result.architectures || !Array.isArray(result.architectures)) {
|
|
2185
|
+
throw new Error('Invalid architecture recommendation response: missing architectures array');
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
if (result.architectures.length < 1) {
|
|
2189
|
+
throw new Error('No architecture recommendations received');
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// Validate each architecture has required fields
|
|
2193
|
+
result.architectures.forEach((arch, index) => {
|
|
2194
|
+
if (!arch.name || !arch.description || typeof arch.requiresCloudProvider !== 'boolean' || !arch.bestFor) {
|
|
2195
|
+
throw new Error(`Architecture at index ${index} is missing required fields`);
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
return result.architectures;
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
console.error('[ERROR] getArchitectureRecommendations failed:', error.message);
|
|
2202
|
+
debug('getArchitectureRecommendations error', { error: error.message, stack: error.stack });
|
|
2203
|
+
throw error;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/**
|
|
2208
|
+
* Get database recommendation based on mission statement and initial scope (quick analysis)
|
|
2209
|
+
* @param {string} missionStatement - The project's mission statement
|
|
2210
|
+
* @param {string} initialScope - The initial scope/features
|
|
2211
|
+
* @returns {Promise<Object>} Database recommendation object
|
|
2212
|
+
*/
|
|
2213
|
+
async getDatabaseRecommendation(missionStatement, initialScope, deploymentStrategy = null) {
|
|
2214
|
+
debug('getDatabaseRecommendation called', { missionStatement, initialScope, deploymentStrategy });
|
|
2215
|
+
|
|
2216
|
+
try {
|
|
2217
|
+
// Get stage-specific provider for database recommendation
|
|
2218
|
+
const provider = await this.getProviderForStageInstance('database-recommendation');
|
|
2219
|
+
|
|
2220
|
+
if (!provider || typeof provider.generateJSON !== 'function') {
|
|
2221
|
+
throw new Error('Database recommendation provider not available');
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// Read agent instructions
|
|
2225
|
+
debug('Loading database-recommender.md agent');
|
|
2226
|
+
const databaseRecommenderAgent = loadAgent('database-recommender.md', path.dirname(this.avcPath));
|
|
2227
|
+
|
|
2228
|
+
// Build prompt with deployment strategy context
|
|
2229
|
+
let prompt = `Given the following project definition:
|
|
2230
|
+
|
|
2231
|
+
**Mission Statement:**
|
|
2232
|
+
${missionStatement}
|
|
2233
|
+
|
|
2234
|
+
**Initial Scope (Features to Implement):**
|
|
2235
|
+
${initialScope}
|
|
2236
|
+
`;
|
|
2237
|
+
|
|
2238
|
+
// Add deployment strategy context if provided
|
|
2239
|
+
if (deploymentStrategy === 'local-mvp') {
|
|
2240
|
+
prompt += `
|
|
2241
|
+
**Deployment Strategy:** Local MVP First
|
|
2242
|
+
The user has chosen to start with a local development environment and migrate to cloud later.
|
|
2243
|
+
|
|
2244
|
+
IMPORTANT: Prioritize local-friendly databases:
|
|
2245
|
+
- For SQL: Recommend SQLite (zero setup, file-based) or PostgreSQL in Docker (production parity)
|
|
2246
|
+
- For NoSQL: Recommend local MongoDB in Docker or JSON file storage
|
|
2247
|
+
- Include clear migration paths to cloud databases (SQLite → RDS/Cloud SQL, local MongoDB → Atlas)
|
|
2248
|
+
- Emphasize zero cost during MVP phase
|
|
2249
|
+
- Show cost comparison: "$0/month local" vs cloud costs
|
|
2250
|
+
|
|
2251
|
+
`;
|
|
2252
|
+
} else if (deploymentStrategy === 'cloud') {
|
|
2253
|
+
prompt += `
|
|
2254
|
+
**Deployment Strategy:** Cloud Deployment
|
|
2255
|
+
The user has chosen to deploy to production cloud infrastructure from day one.
|
|
2256
|
+
|
|
2257
|
+
IMPORTANT: Prioritize managed cloud databases:
|
|
2258
|
+
- For SQL: Recommend AWS RDS, Azure Database, Google Cloud SQL
|
|
2259
|
+
- For NoSQL: Recommend DynamoDB, MongoDB Atlas, Azure Cosmos DB
|
|
2260
|
+
- Emphasize managed features (backups, scaling, monitoring, high availability)
|
|
2261
|
+
- Include realistic monthly cost estimates
|
|
2262
|
+
- Focus on production-ready, scalable options
|
|
2263
|
+
|
|
2264
|
+
`;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
prompt += `
|
|
2268
|
+
Analyze this project and determine if it needs a database, and if so, recommend the most appropriate database solution.
|
|
2269
|
+
|
|
2270
|
+
Return your response as JSON following the exact structure specified in your instructions.`;
|
|
2271
|
+
|
|
2272
|
+
debug('Calling LLM for database recommendation');
|
|
2273
|
+
const result = await this.retryWithBackoff(
|
|
2274
|
+
() => provider.generateJSON(prompt, databaseRecommenderAgent),
|
|
2275
|
+
'database recommendation'
|
|
2276
|
+
);
|
|
2277
|
+
|
|
2278
|
+
debug('Database recommendation received', {
|
|
2279
|
+
hasDatabaseNeeds: result.hasDatabaseNeeds,
|
|
2280
|
+
confidence: result.confidence,
|
|
2281
|
+
hasComparison: !!result.comparison
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
// Validate response structure
|
|
2285
|
+
if (typeof result.hasDatabaseNeeds !== 'boolean' || !result.confidence) {
|
|
2286
|
+
throw new Error('Invalid database recommendation response: missing required fields');
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// If database is needed, validate comparison structure
|
|
2290
|
+
if (result.hasDatabaseNeeds && result.comparison) {
|
|
2291
|
+
if (!result.comparison.sqlOption || !result.comparison.nosqlOption) {
|
|
2292
|
+
throw new Error('Invalid database recommendation: missing sqlOption or nosqlOption in comparison');
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
return result;
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
console.error('[ERROR] getDatabaseRecommendation failed:', error.message);
|
|
2299
|
+
debug('getDatabaseRecommendation error', { error: error.message, stack: error.stack });
|
|
2300
|
+
throw error;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/**
|
|
2305
|
+
* Get detailed database recommendation with user inputs
|
|
2306
|
+
* @param {string} missionStatement - The project's mission statement
|
|
2307
|
+
* @param {string} initialScope - The initial scope/features
|
|
2308
|
+
* @param {Object} userAnswers - User's detailed answers
|
|
2309
|
+
* @param {string} userAnswers.readWriteRatio - e.g., "70/30"
|
|
2310
|
+
* @param {string} userAnswers.dailyRequests - e.g., "10000"
|
|
2311
|
+
* @param {string} userAnswers.costSensitivity - "Low" | "Medium" | "High"
|
|
2312
|
+
* @param {string} userAnswers.dataRelationships - "Simple" | "Moderate" | "Complex"
|
|
2313
|
+
* @returns {Promise<Object>} Detailed database recommendation
|
|
2314
|
+
*/
|
|
2315
|
+
async getDatabaseDetailedRecommendation(missionStatement, initialScope, userAnswers) {
|
|
2316
|
+
debug('getDatabaseDetailedRecommendation called', {
|
|
2317
|
+
missionStatement,
|
|
2318
|
+
initialScope,
|
|
2319
|
+
userAnswers
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
try {
|
|
2323
|
+
// Get stage-specific provider for detailed database recommendation
|
|
2324
|
+
const provider = await this.getProviderForStageInstance('database-deep-dive');
|
|
2325
|
+
|
|
2326
|
+
if (!provider || typeof provider.generateJSON !== 'function') {
|
|
2327
|
+
throw new Error('Database deep-dive provider not available');
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// Read agent instructions
|
|
2331
|
+
debug('Loading database-deep-dive.md agent');
|
|
2332
|
+
const databaseDeepDiveAgent = loadAgent('database-deep-dive.md', path.dirname(this.avcPath));
|
|
2333
|
+
|
|
2334
|
+
// Build prompt
|
|
2335
|
+
const prompt = `Given the following project definition and user requirements:
|
|
2336
|
+
|
|
2337
|
+
**Mission Statement:**
|
|
2338
|
+
${missionStatement}
|
|
2339
|
+
|
|
2340
|
+
**Initial Scope (Features to Implement):**
|
|
2341
|
+
${initialScope}
|
|
2342
|
+
|
|
2343
|
+
**User Requirements:**
|
|
2344
|
+
- Read/Write Ratio: ${userAnswers.readWriteRatio}
|
|
2345
|
+
- Expected Daily Requests: ${userAnswers.dailyRequests}
|
|
2346
|
+
- Cost Sensitivity: ${userAnswers.costSensitivity}
|
|
2347
|
+
- Data Relationships: ${userAnswers.dataRelationships}
|
|
2348
|
+
|
|
2349
|
+
Provide a detailed database architecture recommendation including specific configurations, sizing, and cost estimates.
|
|
2350
|
+
|
|
2351
|
+
Return your response as JSON following the exact structure specified in your instructions.`;
|
|
2352
|
+
|
|
2353
|
+
debug('Calling LLM for detailed database recommendation');
|
|
2354
|
+
const result = await this.retryWithBackoff(
|
|
2355
|
+
() => provider.generateJSON(prompt, databaseDeepDiveAgent),
|
|
2356
|
+
'detailed database recommendation'
|
|
2357
|
+
);
|
|
2358
|
+
|
|
2359
|
+
debug('Detailed database recommendation received', {
|
|
2360
|
+
hasComparison: !!result.comparison,
|
|
2361
|
+
recommendation: result.recommendation
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
// Validate response structure (new comparison format)
|
|
2365
|
+
if (!result.comparison || !result.comparison.sqlOption || !result.comparison.nosqlOption) {
|
|
2366
|
+
throw new Error('Invalid detailed database recommendation response: missing comparison with sqlOption and nosqlOption');
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// Validate sqlOption has required fields
|
|
2370
|
+
if (!result.comparison.sqlOption.database || !result.comparison.sqlOption.architecture || !result.comparison.sqlOption.estimatedCosts) {
|
|
2371
|
+
throw new Error('Invalid sqlOption in detailed database recommendation: missing required fields');
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// Validate nosqlOption has required fields
|
|
2375
|
+
if (!result.comparison.nosqlOption.database || !result.comparison.nosqlOption.architecture || !result.comparison.nosqlOption.estimatedCosts) {
|
|
2376
|
+
throw new Error('Invalid nosqlOption in detailed database recommendation: missing required fields');
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
return result;
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
console.error('[ERROR] getDatabaseDetailedRecommendation failed:', error.message);
|
|
2382
|
+
debug('getDatabaseDetailedRecommendation error', { error: error.message, stack: error.stack });
|
|
2383
|
+
throw error;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/**
|
|
2388
|
+
* Pre-fill questionnaire answers based on architecture selection
|
|
2389
|
+
* @param {string} missionStatement - The project's mission statement
|
|
2390
|
+
* @param {string} initialScope - The initial scope/features
|
|
2391
|
+
* @param {Object} architecture - Selected architecture object
|
|
2392
|
+
* @param {string|null} cloudProvider - Selected cloud provider (AWS/Azure/GCP) or null
|
|
2393
|
+
* @param {Object|null} databaseRecommendation - Optional database recommendation context
|
|
2394
|
+
* @returns {Promise<Object>} Object with pre-filled answers
|
|
2395
|
+
*/
|
|
2396
|
+
async prefillQuestions(missionStatement, initialScope, architecture, cloudProvider = null, databaseRecommendation = null, deploymentStrategy = null) {
|
|
2397
|
+
debug('prefillQuestions called', {
|
|
2398
|
+
missionStatement,
|
|
2399
|
+
initialScope,
|
|
2400
|
+
architectureName: architecture.name,
|
|
2401
|
+
cloudProvider,
|
|
2402
|
+
deploymentStrategy
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
try {
|
|
2406
|
+
// Get stage-specific provider for question prefilling
|
|
2407
|
+
const provider = await this.getProviderForStageInstance('question-prefilling');
|
|
2408
|
+
|
|
2409
|
+
if (!provider || typeof provider.generateJSON !== 'function') {
|
|
2410
|
+
throw new Error('Question prefilling provider not available');
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// Read agent instructions
|
|
2414
|
+
debug('Loading question-prefiller.md agent');
|
|
2415
|
+
const questionPrefillerAgent = loadAgent('question-prefiller.md', path.dirname(this.avcPath));
|
|
2416
|
+
|
|
2417
|
+
// Build prompt
|
|
2418
|
+
let prompt = `Given the following project context:
|
|
2419
|
+
|
|
2420
|
+
**Mission Statement:**
|
|
2421
|
+
${missionStatement}
|
|
2422
|
+
|
|
2423
|
+
**Initial Scope (Features to Implement):**
|
|
2424
|
+
${initialScope}
|
|
2425
|
+
|
|
2426
|
+
**Selected Architecture:**
|
|
2427
|
+
- Name: ${architecture.name}
|
|
2428
|
+
- Description: ${architecture.description}
|
|
2429
|
+
- Best For: ${architecture.bestFor}`;
|
|
2430
|
+
|
|
2431
|
+
if (cloudProvider) {
|
|
2432
|
+
prompt += `\n- Cloud Provider: ${cloudProvider}`;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// Add deployment strategy context if provided
|
|
2436
|
+
if (deploymentStrategy) {
|
|
2437
|
+
const strategyName = deploymentStrategy === 'local-mvp' ? 'Local MVP First' : 'Cloud Deployment';
|
|
2438
|
+
prompt += `
|
|
2439
|
+
|
|
2440
|
+
**Deployment Strategy:** ${strategyName}`;
|
|
2441
|
+
|
|
2442
|
+
if (deploymentStrategy === 'local-mvp') {
|
|
2443
|
+
prompt += `
|
|
2444
|
+
|
|
2445
|
+
CRITICAL: Deployment strategy affects ONLY deployment and technical choices, NOT target users.
|
|
2446
|
+
|
|
2447
|
+
**TARGET_USERS:**
|
|
2448
|
+
- Infer from mission statement and scope ONLY
|
|
2449
|
+
- Ignore deployment strategy completely
|
|
2450
|
+
- Example: If mission is "task management for remote teams", target users are remote team members, NOT developers
|
|
2451
|
+
- The deployment choice (local vs cloud) does NOT change who the end users are
|
|
2452
|
+
|
|
2453
|
+
**DEPLOYMENT_TARGET requirements:**
|
|
2454
|
+
- Emphasize local development environment (Docker Compose, localhost)
|
|
2455
|
+
- Mention zero cloud costs during MVP development phase
|
|
2456
|
+
- Include migration readiness: "Ready to migrate to [cloud options] when scaling to production"
|
|
2457
|
+
|
|
2458
|
+
**TECHNICAL_CONSIDERATIONS requirements:**
|
|
2459
|
+
- Include local stack details (SQLite or local PostgreSQL/MongoDB in Docker, containerization)
|
|
2460
|
+
- Focus on: Zero cost, rapid iteration, easy debugging, production parity with Docker
|
|
2461
|
+
- DO NOT mention cloud services (RDS, Lambda, ECS, etc.) in technical details`;
|
|
2462
|
+
} else if (deploymentStrategy === 'cloud') {
|
|
2463
|
+
prompt += `
|
|
2464
|
+
|
|
2465
|
+
CRITICAL: Deployment strategy affects ONLY deployment and technical choices, NOT target users.
|
|
2466
|
+
|
|
2467
|
+
**TARGET_USERS:**
|
|
2468
|
+
- Infer from mission statement and scope ONLY
|
|
2469
|
+
- Ignore deployment strategy completely
|
|
2470
|
+
- The deployment choice (local vs cloud) does NOT change who the end users are
|
|
2471
|
+
|
|
2472
|
+
**DEPLOYMENT_TARGET requirements:**
|
|
2473
|
+
- Detail cloud infrastructure (managed services, auto-scaling, regions)
|
|
2474
|
+
- Emphasize production-ready from day one
|
|
2475
|
+
- Include cost considerations and scaling strategy
|
|
2476
|
+
|
|
2477
|
+
**TECHNICAL_CONSIDERATIONS requirements:**
|
|
2478
|
+
- Include cloud-specific details (managed database, CDN, load balancers)
|
|
2479
|
+
- Emphasize: monitoring, backups, high availability, auto-scaling
|
|
2480
|
+
- Focus on: Managed services, production features`;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// Add database context if available (supports both old and new format)
|
|
2485
|
+
if (databaseRecommendation) {
|
|
2486
|
+
prompt += `
|
|
2487
|
+
|
|
2488
|
+
**Database Recommendation:**`;
|
|
2489
|
+
|
|
2490
|
+
// New format with comparison
|
|
2491
|
+
if (databaseRecommendation.comparison) {
|
|
2492
|
+
const { sqlOption, nosqlOption } = databaseRecommendation.comparison;
|
|
2493
|
+
const userChoice = databaseRecommendation.userChoice;
|
|
2494
|
+
|
|
2495
|
+
if (userChoice === 'sql') {
|
|
2496
|
+
prompt += `
|
|
2497
|
+
- Chosen Database: ${sqlOption.database} (SQL)`;
|
|
2498
|
+
if (sqlOption.specificVersion) {
|
|
2499
|
+
prompt += `
|
|
2500
|
+
- Specific Version: ${sqlOption.specificVersion}`;
|
|
2501
|
+
}
|
|
2502
|
+
if (sqlOption.architecture) {
|
|
2503
|
+
prompt += `
|
|
2504
|
+
- Architecture: ${sqlOption.architecture.primaryInstance || ''}`;
|
|
2505
|
+
if (sqlOption.architecture.readReplicas) {
|
|
2506
|
+
prompt += ` with ${sqlOption.architecture.readReplicas} read replica(s)`;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (sqlOption.estimatedCosts) {
|
|
2510
|
+
prompt += `
|
|
2511
|
+
- Estimated Cost: ${sqlOption.estimatedCosts.monthly}`;
|
|
2512
|
+
}
|
|
2513
|
+
} else if (userChoice === 'nosql') {
|
|
2514
|
+
prompt += `
|
|
2515
|
+
- Chosen Database: ${nosqlOption.database} (NoSQL)`;
|
|
2516
|
+
if (nosqlOption.specificVersion) {
|
|
2517
|
+
prompt += `
|
|
2518
|
+
- Specific Version: ${nosqlOption.specificVersion}`;
|
|
2519
|
+
}
|
|
2520
|
+
if (nosqlOption.architecture) {
|
|
2521
|
+
prompt += `
|
|
2522
|
+
- Architecture: ${nosqlOption.architecture.capacity || nosqlOption.architecture.primaryInstance || ''}`;
|
|
2523
|
+
if (nosqlOption.architecture.indexes) {
|
|
2524
|
+
prompt += `, ${nosqlOption.architecture.indexes} indexes`;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
if (nosqlOption.estimatedCosts) {
|
|
2528
|
+
prompt += `
|
|
2529
|
+
- Estimated Cost: ${nosqlOption.estimatedCosts.monthly}`;
|
|
2530
|
+
}
|
|
2531
|
+
} else {
|
|
2532
|
+
// No user choice yet, show both options
|
|
2533
|
+
prompt += `
|
|
2534
|
+
- SQL Option: ${sqlOption.database} (~${sqlOption.estimatedCosts?.monthly || 'TBD'})
|
|
2535
|
+
- NoSQL Option: ${nosqlOption.database} (~${nosqlOption.estimatedCosts?.monthly || 'TBD'})`;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
if (databaseRecommendation.keyMetrics) {
|
|
2539
|
+
prompt += `
|
|
2540
|
+
- Read/Write Ratio: ${databaseRecommendation.keyMetrics.readWriteRatio || databaseRecommendation.keyMetrics.estimatedReadWriteRatio || 'Not specified'}`;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
// Legacy format (backward compatibility)
|
|
2544
|
+
else {
|
|
2545
|
+
prompt += `
|
|
2546
|
+
- Primary Database: ${databaseRecommendation.primaryDatabase || databaseRecommendation.recommendation?.primaryDatabase || 'Not specified'}
|
|
2547
|
+
- Type: ${databaseRecommendation.type || databaseRecommendation.recommendation?.type || 'Not specified'}`;
|
|
2548
|
+
|
|
2549
|
+
if (databaseRecommendation.specificVersion) {
|
|
2550
|
+
prompt += `
|
|
2551
|
+
- Specific Version: ${databaseRecommendation.specificVersion}`;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
if (databaseRecommendation.architecture) {
|
|
2555
|
+
prompt += `
|
|
2556
|
+
- Architecture: ${databaseRecommendation.architecture.primaryInstance || ''}`;
|
|
2557
|
+
if (databaseRecommendation.architecture.readReplicas) {
|
|
2558
|
+
prompt += ` with ${databaseRecommendation.architecture.readReplicas} read replica(s)`;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
if (databaseRecommendation.estimatedCosts) {
|
|
2563
|
+
prompt += `
|
|
2564
|
+
- Estimated Cost: ${databaseRecommendation.estimatedCosts.monthly || ''}`;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
prompt += `
|
|
2570
|
+
|
|
2571
|
+
Generate intelligent, context-aware answers for these questions:
|
|
2572
|
+
1. TARGET_USERS: Who will use this application and what are their roles/characteristics?
|
|
2573
|
+
2. DEPLOYMENT_TARGET: Where and how will this application be deployed?
|
|
2574
|
+
3. TECHNICAL_CONSIDERATIONS: Technology stack, architectural patterns, scalability, and performance
|
|
2575
|
+
4. SECURITY_AND_COMPLIANCE_REQUIREMENTS: Security measures, privacy, authentication, and compliance
|
|
2576
|
+
|
|
2577
|
+
Return your response as JSON following the exact structure specified in your instructions.`;
|
|
2578
|
+
|
|
2579
|
+
debug('Calling LLM for question prefilling');
|
|
2580
|
+
const result = await this.retryWithBackoff(
|
|
2581
|
+
() => provider.generateJSON(prompt, questionPrefillerAgent),
|
|
2582
|
+
'question prefilling'
|
|
2583
|
+
);
|
|
2584
|
+
|
|
2585
|
+
// Normalize common field name variants that local LLMs sometimes produce.
|
|
2586
|
+
// Maps known abbreviations/alternatives → canonical field names.
|
|
2587
|
+
const FIELD_ALIASES = {
|
|
2588
|
+
DEPLOY_TARGET: 'DEPLOYMENT_TARGET',
|
|
2589
|
+
DEPLOY: 'DEPLOYMENT_TARGET',
|
|
2590
|
+
DEPLOYMENT: 'DEPLOYMENT_TARGET',
|
|
2591
|
+
TARGET_DEPLOYMENT: 'DEPLOYMENT_TARGET',
|
|
2592
|
+
SECURITY_REQUIREMENTS: 'SECURITY_AND_COMPLIANCE_REQUIREMENTS',
|
|
2593
|
+
SECURITY_COMPLIANCE: 'SECURITY_AND_COMPLIANCE_REQUIREMENTS',
|
|
2594
|
+
COMPLIANCE_REQUIREMENTS: 'SECURITY_AND_COMPLIANCE_REQUIREMENTS',
|
|
2595
|
+
SECURITY: 'SECURITY_AND_COMPLIANCE_REQUIREMENTS',
|
|
2596
|
+
TECHNICAL: 'TECHNICAL_CONSIDERATIONS',
|
|
2597
|
+
TECH_CONSIDERATIONS: 'TECHNICAL_CONSIDERATIONS',
|
|
2598
|
+
TECH_STACK: 'TECHNICAL_CONSIDERATIONS',
|
|
2599
|
+
USERS: 'TARGET_USERS',
|
|
2600
|
+
TARGET_USER: 'TARGET_USERS',
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
for (const [alias, canonical] of Object.entries(FIELD_ALIASES)) {
|
|
2604
|
+
if (result[alias] !== undefined) {
|
|
2605
|
+
if (!result[canonical] && result[alias]) {
|
|
2606
|
+
// Canonical missing or empty — use alias value
|
|
2607
|
+
debug(`Normalizing field name: ${alias} → ${canonical}`);
|
|
2608
|
+
result[canonical] = result[alias];
|
|
2609
|
+
}
|
|
2610
|
+
delete result[alias];
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
debug('Question prefilling received', {
|
|
2615
|
+
hasTargetUsers: !!result.TARGET_USERS,
|
|
2616
|
+
hasDeploymentTarget: !!result.DEPLOYMENT_TARGET,
|
|
2617
|
+
hasTechnicalConsiderations: !!result.TECHNICAL_CONSIDERATIONS,
|
|
2618
|
+
hasSecurityRequirements: !!result.SECURITY_AND_COMPLIANCE_REQUIREMENTS
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
// Validate response structure
|
|
2622
|
+
const requiredFields = [
|
|
2623
|
+
'TARGET_USERS',
|
|
2624
|
+
'DEPLOYMENT_TARGET',
|
|
2625
|
+
'TECHNICAL_CONSIDERATIONS',
|
|
2626
|
+
'SECURITY_AND_COMPLIANCE_REQUIREMENTS'
|
|
2627
|
+
];
|
|
2628
|
+
|
|
2629
|
+
const missingFields = requiredFields.filter(field => !result[field]);
|
|
2630
|
+
if (missingFields.length > 0) {
|
|
2631
|
+
sendWarning(`Warning: Pre-filling missing fields: ${missingFields.join(', ')}`);
|
|
2632
|
+
// Fill missing fields with empty strings
|
|
2633
|
+
missingFields.forEach(field => {
|
|
2634
|
+
result[field] = '';
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
return result;
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
console.error('[ERROR] prefillQuestions failed:', error.message);
|
|
2641
|
+
debug('prefillQuestions error', { error: error.message, stack: error.stack });
|
|
2642
|
+
throw error;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* Generate migration guide for local-to-cloud deployment
|
|
2648
|
+
* @param {Object} architecture - Selected local architecture
|
|
2649
|
+
* @param {string} databaseType - Database type ('sql' or 'nosql')
|
|
2650
|
+
* @param {Object} questionnaire - Full questionnaire answers
|
|
2651
|
+
* @returns {Promise<string>} Migration guide markdown
|
|
2652
|
+
*/
|
|
2653
|
+
async generateMigrationGuide(architecture, databaseType, questionnaire) {
|
|
2654
|
+
debug('generateMigrationGuide called', {
|
|
2655
|
+
architectureName: architecture.name,
|
|
2656
|
+
databaseType
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2659
|
+
try {
|
|
2660
|
+
// Get stage-specific provider for migration guide generation
|
|
2661
|
+
this.reportSubstep('Preparing migration guide generator...');
|
|
2662
|
+
const provider = await this.getProviderForStageInstance('migration-guide-generation');
|
|
2663
|
+
|
|
2664
|
+
if (!provider || typeof provider.generateText !== 'function') {
|
|
2665
|
+
throw new Error('Migration guide generation provider not available');
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// Read agent instructions
|
|
2669
|
+
debug('Loading migration-guide-generator.md agent');
|
|
2670
|
+
const migrationGuideAgent = loadAgent('migration-guide-generator.md', path.dirname(this.avcPath));
|
|
2671
|
+
|
|
2672
|
+
// Build comprehensive prompt
|
|
2673
|
+
this.reportSubstep('Generating cloud migration guide (this may take 30-60 seconds)...');
|
|
2674
|
+
const prompt = `Generate a comprehensive cloud migration guide for the following local development setup:
|
|
2675
|
+
|
|
2676
|
+
**Local Architecture:**
|
|
2677
|
+
- Name: ${architecture.name}
|
|
2678
|
+
- Description: ${architecture.description}
|
|
2679
|
+
- Best For: ${architecture.bestFor}
|
|
2680
|
+
|
|
2681
|
+
**Database:**
|
|
2682
|
+
- Type: ${databaseType ? (databaseType.toUpperCase()) : 'Not specified'}
|
|
2683
|
+
|
|
2684
|
+
**Project Context:**
|
|
2685
|
+
- Mission: ${questionnaire.MISSION_STATEMENT}
|
|
2686
|
+
- Scope: ${questionnaire.INITIAL_SCOPE}
|
|
2687
|
+
- Technical Stack: ${questionnaire.TECHNICAL_CONSIDERATIONS || 'To be determined'}
|
|
2688
|
+
|
|
2689
|
+
**Target Users:** ${questionnaire.TARGET_USERS || 'General users'}
|
|
2690
|
+
|
|
2691
|
+
Generate a complete DEPLOYMENT_MIGRATION.md document following the structure specified in your instructions.
|
|
2692
|
+
|
|
2693
|
+
Include:
|
|
2694
|
+
1. Current local stack summary
|
|
2695
|
+
2. When to migrate decision criteria
|
|
2696
|
+
3. 3-4 cloud migration options with costs and complexity
|
|
2697
|
+
4. Database-specific migration guide
|
|
2698
|
+
5. Environment variable changes
|
|
2699
|
+
6. CI/CD pipeline recommendation
|
|
2700
|
+
7. Monitoring and observability setup
|
|
2701
|
+
8. Cost comparison table
|
|
2702
|
+
9. Comprehensive migration checklist
|
|
2703
|
+
10. Common issues and solutions
|
|
2704
|
+
11. Support resources
|
|
2705
|
+
|
|
2706
|
+
Make it actionable with specific CLI commands, code examples, and cost estimates.`;
|
|
2707
|
+
|
|
2708
|
+
debug('Calling LLM for migration guide generation');
|
|
2709
|
+
const result = await this.retryWithBackoff(
|
|
2710
|
+
() => provider.generateText(prompt, migrationGuideAgent),
|
|
2711
|
+
'migration guide generation'
|
|
2712
|
+
);
|
|
2713
|
+
|
|
2714
|
+
debug('Migration guide generated', { length: result.length });
|
|
2715
|
+
this.reportSubstep('Writing DEPLOYMENT_MIGRATION.md...');
|
|
2716
|
+
|
|
2717
|
+
return result;
|
|
2718
|
+
} catch (error) {
|
|
2719
|
+
console.error('[ERROR] generateMigrationGuide failed:', error.message);
|
|
2720
|
+
debug('generateMigrationGuide error', { error: error.message, stack: error.stack });
|
|
2721
|
+
throw error;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
/**
|
|
2726
|
+
* Configure the log file for debug() writes.
|
|
2727
|
+
* Called from repl-ink.js when the CommandLogger starts/stops.
|
|
2728
|
+
* When filePath is null, debug() is silent (no terminal or file output).
|
|
2729
|
+
* @param {string|null} filePath - Absolute path to the active log file
|
|
2730
|
+
*/
|
|
2731
|
+
static setDebugLogFile(filePath) {
|
|
2732
|
+
_debugLogFile = filePath;
|
|
689
2733
|
}
|
|
690
2734
|
}
|
|
691
2735
|
|