@agile-vibe-coding/avc 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +129 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/epic-story-decomposer.md +280 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +79 -0
- package/cli/agents/mission-scope-validator.md +112 -0
- package/cli/agents/project-context-extractor.md +107 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/solver-epic-api.json +15 -0
- package/cli/agents/solver-epic-api.md +39 -0
- package/cli/agents/solver-epic-backend.json +15 -0
- package/cli/agents/solver-epic-backend.md +39 -0
- package/cli/agents/solver-epic-cloud.json +15 -0
- package/cli/agents/solver-epic-cloud.md +39 -0
- package/cli/agents/solver-epic-data.json +15 -0
- package/cli/agents/solver-epic-data.md +39 -0
- package/cli/agents/solver-epic-database.json +15 -0
- package/cli/agents/solver-epic-database.md +39 -0
- package/cli/agents/solver-epic-developer.json +15 -0
- package/cli/agents/solver-epic-developer.md +39 -0
- package/cli/agents/solver-epic-devops.json +15 -0
- package/cli/agents/solver-epic-devops.md +39 -0
- package/cli/agents/solver-epic-frontend.json +15 -0
- package/cli/agents/solver-epic-frontend.md +39 -0
- package/cli/agents/solver-epic-mobile.json +15 -0
- package/cli/agents/solver-epic-mobile.md +39 -0
- package/cli/agents/solver-epic-qa.json +15 -0
- package/cli/agents/solver-epic-qa.md +39 -0
- package/cli/agents/solver-epic-security.json +15 -0
- package/cli/agents/solver-epic-security.md +39 -0
- package/cli/agents/solver-epic-solution-architect.json +15 -0
- package/cli/agents/solver-epic-solution-architect.md +39 -0
- package/cli/agents/solver-epic-test-architect.json +15 -0
- package/cli/agents/solver-epic-test-architect.md +39 -0
- package/cli/agents/solver-epic-ui.json +15 -0
- package/cli/agents/solver-epic-ui.md +39 -0
- package/cli/agents/solver-epic-ux.json +15 -0
- package/cli/agents/solver-epic-ux.md +39 -0
- package/cli/agents/solver-story-api.json +15 -0
- package/cli/agents/solver-story-api.md +39 -0
- package/cli/agents/solver-story-backend.json +15 -0
- package/cli/agents/solver-story-backend.md +39 -0
- package/cli/agents/solver-story-cloud.json +15 -0
- package/cli/agents/solver-story-cloud.md +39 -0
- package/cli/agents/solver-story-data.json +15 -0
- package/cli/agents/solver-story-data.md +39 -0
- package/cli/agents/solver-story-database.json +15 -0
- package/cli/agents/solver-story-database.md +39 -0
- package/cli/agents/solver-story-developer.json +15 -0
- package/cli/agents/solver-story-developer.md +39 -0
- package/cli/agents/solver-story-devops.json +15 -0
- package/cli/agents/solver-story-devops.md +39 -0
- package/cli/agents/solver-story-frontend.json +15 -0
- package/cli/agents/solver-story-frontend.md +39 -0
- package/cli/agents/solver-story-mobile.json +15 -0
- package/cli/agents/solver-story-mobile.md +39 -0
- package/cli/agents/solver-story-qa.json +15 -0
- package/cli/agents/solver-story-qa.md +39 -0
- package/cli/agents/solver-story-security.json +15 -0
- package/cli/agents/solver-story-security.md +39 -0
- package/cli/agents/solver-story-solution-architect.json +15 -0
- package/cli/agents/solver-story-solution-architect.md +39 -0
- package/cli/agents/solver-story-test-architect.json +15 -0
- package/cli/agents/solver-story-test-architect.md +39 -0
- package/cli/agents/solver-story-ui.json +15 -0
- package/cli/agents/solver-story-ui.md +39 -0
- package/cli/agents/solver-story-ux.json +15 -0
- package/cli/agents/solver-story-ux.md +39 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +152 -0
- package/cli/agents/validator-documentation.md +453 -0
- package/cli/agents/validator-epic-api.json +93 -0
- package/cli/agents/validator-epic-api.md +137 -0
- package/cli/agents/validator-epic-backend.json +93 -0
- package/cli/agents/validator-epic-backend.md +130 -0
- package/cli/agents/validator-epic-cloud.json +93 -0
- package/cli/agents/validator-epic-cloud.md +137 -0
- package/cli/agents/validator-epic-data.json +93 -0
- package/cli/agents/validator-epic-data.md +130 -0
- package/cli/agents/validator-epic-database.json +93 -0
- package/cli/agents/validator-epic-database.md +137 -0
- package/cli/agents/validator-epic-developer.json +74 -0
- package/cli/agents/validator-epic-developer.md +153 -0
- package/cli/agents/validator-epic-devops.json +74 -0
- package/cli/agents/validator-epic-devops.md +153 -0
- package/cli/agents/validator-epic-frontend.json +74 -0
- package/cli/agents/validator-epic-frontend.md +153 -0
- package/cli/agents/validator-epic-mobile.json +93 -0
- package/cli/agents/validator-epic-mobile.md +130 -0
- package/cli/agents/validator-epic-qa.json +93 -0
- package/cli/agents/validator-epic-qa.md +130 -0
- package/cli/agents/validator-epic-security.json +74 -0
- package/cli/agents/validator-epic-security.md +154 -0
- package/cli/agents/validator-epic-solution-architect.json +74 -0
- package/cli/agents/validator-epic-solution-architect.md +156 -0
- package/cli/agents/validator-epic-test-architect.json +93 -0
- package/cli/agents/validator-epic-test-architect.md +130 -0
- package/cli/agents/validator-epic-ui.json +93 -0
- package/cli/agents/validator-epic-ui.md +130 -0
- package/cli/agents/validator-epic-ux.json +93 -0
- package/cli/agents/validator-epic-ux.md +130 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/agents/validator-story-api.json +104 -0
- package/cli/agents/validator-story-api.md +152 -0
- package/cli/agents/validator-story-backend.json +104 -0
- package/cli/agents/validator-story-backend.md +152 -0
- package/cli/agents/validator-story-cloud.json +104 -0
- package/cli/agents/validator-story-cloud.md +152 -0
- package/cli/agents/validator-story-data.json +104 -0
- package/cli/agents/validator-story-data.md +152 -0
- package/cli/agents/validator-story-database.json +104 -0
- package/cli/agents/validator-story-database.md +152 -0
- package/cli/agents/validator-story-developer.json +104 -0
- package/cli/agents/validator-story-developer.md +152 -0
- package/cli/agents/validator-story-devops.json +104 -0
- package/cli/agents/validator-story-devops.md +152 -0
- package/cli/agents/validator-story-frontend.json +104 -0
- package/cli/agents/validator-story-frontend.md +152 -0
- package/cli/agents/validator-story-mobile.json +104 -0
- package/cli/agents/validator-story-mobile.md +152 -0
- package/cli/agents/validator-story-qa.json +104 -0
- package/cli/agents/validator-story-qa.md +152 -0
- package/cli/agents/validator-story-security.json +104 -0
- package/cli/agents/validator-story-security.md +152 -0
- package/cli/agents/validator-story-solution-architect.json +104 -0
- package/cli/agents/validator-story-solution-architect.md +152 -0
- package/cli/agents/validator-story-test-architect.json +104 -0
- package/cli/agents/validator-story-test-architect.md +152 -0
- package/cli/agents/validator-story-ui.json +104 -0
- package/cli/agents/validator-story-ui.md +152 -0
- package/cli/agents/validator-story-ux.json +104 -0
- package/cli/agents/validator-story-ux.md +152 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/build-docs.js +298 -0
- package/cli/ceremony-history.js +369 -0
- package/cli/command-logger.js +245 -0
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +1174 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/index.js +3 -25
- package/cli/init-model-config.js +697 -0
- package/cli/init.js +1765 -100
- package/cli/kanban-server-manager.js +228 -0
- package/cli/llm-claude.js +109 -0
- package/cli/llm-gemini.js +115 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +233 -0
- package/cli/llm-provider.js +300 -0
- package/cli/llm-token-limits.js +102 -0
- package/cli/llm-verifier.js +454 -0
- package/cli/logger.js +32 -5
- package/cli/message-constants.js +58 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +297 -0
- package/cli/model-pricing.js +169 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +269 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +332 -0
- package/cli/repl-ink.js +5840 -504
- package/cli/repl-old.js +4 -4
- package/cli/seed-processor.js +792 -0
- package/cli/sprint-planning-processor.js +1813 -0
- package/cli/template-processor.js +2306 -108
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +34 -0
- package/cli/token-tracker.js +520 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +605 -0
- package/cli/verification-tracker.js +563 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
- package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +622 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +353 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +118 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +401 -0
- package/kanban/client/src/lib/status-grouping.js +144 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +115 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +516 -0
- package/kanban/server/routes/ceremony.js +305 -0
- package/kanban/server/routes/costs.js +157 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +303 -0
- package/kanban/server/routes/websocket.js +276 -0
- package/kanban/server/routes/work-items.js +347 -0
- package/kanban/server/services/CeremonyService.js +1190 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/sponsor-call-worker.js +84 -0
- package/kanban/server/workers/sprint-planning-worker.js +130 -0
- package/package.json +34 -7
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { LLMProvider } from './llm-provider.js';
|
|
4
|
+
import { TokenTracker } from './token-tracker.js';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { getCeremonyHeader } from './message-constants.js';
|
|
7
|
+
import { sendError, sendWarning, sendSuccess, sendInfo, sendOutput, sendIndented, sendSectionHeader } from './messaging-api.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SeedProcessor - Decomposes a Story into Tasks and Subtasks
|
|
14
|
+
*/
|
|
15
|
+
class SeedProcessor {
|
|
16
|
+
/**
|
|
17
|
+
* Write structured entry to active command log file only.
|
|
18
|
+
* Uses [DEBUG] prefix so ConsoleOutputManager routes to file, not terminal.
|
|
19
|
+
*/
|
|
20
|
+
debug(message, data = null) {
|
|
21
|
+
const ts = new Date().toISOString();
|
|
22
|
+
if (data !== null) {
|
|
23
|
+
console.log(`[DEBUG][${ts}] ${message}`, JSON.stringify(data, null, 2));
|
|
24
|
+
} else {
|
|
25
|
+
console.log(`[DEBUG][${ts}] ${message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor(storyId) {
|
|
30
|
+
this.storyId = storyId;
|
|
31
|
+
this.ceremonyName = 'seed';
|
|
32
|
+
this.avcPath = path.join(process.cwd(), '.avc');
|
|
33
|
+
this.projectPath = path.join(this.avcPath, 'project');
|
|
34
|
+
|
|
35
|
+
// Extract epic ID from story ID (context-0001-0001 → context-0001)
|
|
36
|
+
const epicId = this.extractEpicId(storyId);
|
|
37
|
+
|
|
38
|
+
// Build nested path: .avc/project/context-0001/context-0001-0001
|
|
39
|
+
this.storyPath = path.join(this.projectPath, epicId, storyId);
|
|
40
|
+
|
|
41
|
+
this.avcConfigPath = path.join(this.avcPath, 'avc.json');
|
|
42
|
+
this.agentsPath = path.join(__dirname, 'agents');
|
|
43
|
+
|
|
44
|
+
// Read ceremony config
|
|
45
|
+
const { provider, model, stagesConfig } = this.readCeremonyConfig();
|
|
46
|
+
this._providerName = provider;
|
|
47
|
+
this._modelName = model;
|
|
48
|
+
this.stagesConfig = stagesConfig;
|
|
49
|
+
this.llmProvider = null;
|
|
50
|
+
this._stageProviders = {};
|
|
51
|
+
|
|
52
|
+
// Initialize token tracker
|
|
53
|
+
this.tokenTracker = new TokenTracker(this.avcPath);
|
|
54
|
+
this.tokenTracker.init();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract Epic ID from Story ID
|
|
59
|
+
* @param {string} storyId - Story ID (e.g., context-0001-0001)
|
|
60
|
+
* @returns {string} Epic ID (e.g., context-0001)
|
|
61
|
+
*/
|
|
62
|
+
extractEpicId(storyId) {
|
|
63
|
+
// Story format: context-XXXX-YYYY
|
|
64
|
+
// Epic format: context-XXXX
|
|
65
|
+
const match = storyId.match(/^(context-\d+)-\d+$/);
|
|
66
|
+
|
|
67
|
+
if (!match) {
|
|
68
|
+
throw new Error(`Invalid story ID format: ${storyId}. Expected: context-XXXX-YYYY`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return match[1];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
readCeremonyConfig() {
|
|
75
|
+
try {
|
|
76
|
+
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
77
|
+
const ceremony = config.settings?.ceremonies?.find(c => c.name === this.ceremonyName);
|
|
78
|
+
|
|
79
|
+
if (!ceremony) {
|
|
80
|
+
sendWarning(`Ceremony '${this.ceremonyName}' not found in config, using defaults`);
|
|
81
|
+
this.debug('[WARNING] Ceremony not found in config — using defaults', { ceremonyName: this.ceremonyName });
|
|
82
|
+
return { provider: 'claude', model: 'claude-sonnet-4-5-20250929' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = {
|
|
86
|
+
provider: ceremony.provider || 'claude',
|
|
87
|
+
model: ceremony.defaultModel || 'claude-sonnet-4-5-20250929',
|
|
88
|
+
stagesConfig: ceremony.stages || {}
|
|
89
|
+
};
|
|
90
|
+
this.debug('[INFO] Ceremony config loaded', { provider: result.provider, model: result.model, ceremonyName: this.ceremonyName });
|
|
91
|
+
return result;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
sendWarning(`Could not read ceremony config: ${error.message}`);
|
|
94
|
+
this.debug('[ERROR] Failed to read ceremony config', { error: error.message });
|
|
95
|
+
return { provider: 'claude', model: 'claude-sonnet-4-5-20250929' };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async initializeLLMProvider() {
|
|
100
|
+
try {
|
|
101
|
+
this.llmProvider = await LLMProvider.create(this._providerName, this._modelName);
|
|
102
|
+
this.llmProvider.onCall((delta) => this.tokenTracker.addIncremental(this.ceremonyName, delta));
|
|
103
|
+
return this.llmProvider;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
sendWarning(`Could not initialize ${this._providerName} provider`);
|
|
106
|
+
sendOutput(`${error.message}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async retryWithBackoff(fn, operation, maxRetries = 3) {
|
|
112
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
return await fn();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const isLastAttempt = attempt === maxRetries;
|
|
117
|
+
const isRetriable = error.message?.includes('rate limit') ||
|
|
118
|
+
error.message?.includes('timeout') ||
|
|
119
|
+
error.message?.includes('503');
|
|
120
|
+
|
|
121
|
+
if (isLastAttempt || !isRetriable) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
126
|
+
sendWarning(`Retry ${attempt}/${maxRetries} in ${delay/1000}s: ${operation}`);
|
|
127
|
+
sendOutput(`Error: ${error.message}`);
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get or create a provider for a specific stage, using stage-specific model if configured.
|
|
135
|
+
* Falls back to ceremony-level model if the stage has no explicit config.
|
|
136
|
+
*/
|
|
137
|
+
async getProviderForStageInstance(stageName) {
|
|
138
|
+
const stageConfig = this.stagesConfig?.[stageName] || {};
|
|
139
|
+
const provider = stageConfig.provider || this._providerName;
|
|
140
|
+
const model = stageConfig.model || this._modelName;
|
|
141
|
+
const cacheKey = `${stageName}:${provider}:${model}`;
|
|
142
|
+
|
|
143
|
+
if (this._stageProviders[cacheKey]) return this._stageProviders[cacheKey];
|
|
144
|
+
|
|
145
|
+
const instance = await LLMProvider.create(provider, model);
|
|
146
|
+
instance.onCall((delta) => this.tokenTracker.addIncremental(this.ceremonyName, delta));
|
|
147
|
+
this._stageProviders[cacheKey] = instance;
|
|
148
|
+
return instance;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Distribute documentation content from a parent doc.md to a child item's doc.md.
|
|
153
|
+
* Mirrors the same method in SprintPlanningProcessor.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} parentDocContent - Current content of the parent doc.md
|
|
156
|
+
* @param {Object} childItem - Task or Subtask object
|
|
157
|
+
* @param {'task'|'subtask'} childType - Type of child item
|
|
158
|
+
* @returns {Promise<{childDoc: string, parentDoc: string}>}
|
|
159
|
+
*/
|
|
160
|
+
async distributeDocContent(parentDocContent, childItem, childType) {
|
|
161
|
+
const agentPath = path.join(this.agentsPath, 'doc-distributor.md');
|
|
162
|
+
const agentInstructions = fs.readFileSync(agentPath, 'utf8');
|
|
163
|
+
|
|
164
|
+
let itemDescription;
|
|
165
|
+
if (childType === 'task') {
|
|
166
|
+
const acceptance = (childItem.acceptance || []).map(a => `- ${a}`).join('\n') || 'none specified';
|
|
167
|
+
itemDescription = `Type: task
|
|
168
|
+
Name: ${childItem.name}
|
|
169
|
+
Category: ${childItem.category || 'general'}
|
|
170
|
+
Description: ${childItem.description || ''}
|
|
171
|
+
Technical Scope: ${childItem.technicalScope || ''}
|
|
172
|
+
Acceptance criteria:
|
|
173
|
+
${acceptance}`;
|
|
174
|
+
} else {
|
|
175
|
+
const acceptance = (childItem.acceptance || []).map(a => `- ${a}`).join('\n') || 'none specified';
|
|
176
|
+
itemDescription = `Type: subtask
|
|
177
|
+
Name: ${childItem.name}
|
|
178
|
+
Description: ${childItem.description || ''}
|
|
179
|
+
Technical Details: ${childItem.technicalDetails || ''}
|
|
180
|
+
Acceptance criteria:
|
|
181
|
+
${acceptance}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const prompt = `## Parent Document
|
|
185
|
+
|
|
186
|
+
${parentDocContent}
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Child Item to Create Documentation For
|
|
191
|
+
|
|
192
|
+
${itemDescription}
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
Extract content specifically about this ${childType} from the parent document into the child's \`doc.md\`. Remove the extracted content from the parent document. Return JSON with \`child_doc\` and \`parent_doc\` fields.`;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const provider = await this.getProviderForStageInstance('doc-distribution');
|
|
200
|
+
const result = await this.retryWithBackoff(
|
|
201
|
+
() => provider.generateJSON(prompt, agentInstructions),
|
|
202
|
+
`doc distribution for ${childType}: ${childItem.name}`
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const childDoc = (typeof result.child_doc === 'string' && result.child_doc.trim())
|
|
206
|
+
? result.child_doc
|
|
207
|
+
: `# ${childItem.name}\n\n${childItem.description || ''}\n`;
|
|
208
|
+
|
|
209
|
+
const parentDoc = (typeof result.parent_doc === 'string' && result.parent_doc.trim())
|
|
210
|
+
? result.parent_doc
|
|
211
|
+
: parentDocContent;
|
|
212
|
+
|
|
213
|
+
return { childDoc, parentDoc };
|
|
214
|
+
} catch (err) {
|
|
215
|
+
this.debug('[WARNING] Doc distribution failed — using stub doc', { error: err.message, childType, name: childItem.name });
|
|
216
|
+
return {
|
|
217
|
+
childDoc: `# ${childItem.name}\n\n${childItem.description || ''}\n`,
|
|
218
|
+
parentDoc: parentDocContent
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// STAGE 1: Validate prerequisites
|
|
224
|
+
validatePrerequisites() {
|
|
225
|
+
this.debug('[INFO] validatePrerequisites() called', { storyId: this.storyId, storyPath: this.storyPath });
|
|
226
|
+
|
|
227
|
+
// Check Story ID format
|
|
228
|
+
if (!this.storyId || !this.storyId.match(/^context-\d{4}-\d{4}$/)) {
|
|
229
|
+
this.debug('[ERROR] Invalid story ID format', { storyId: this.storyId });
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Invalid Story ID format: ${this.storyId}\nExpected format: context-XXXX-XXXX (e.g., context-0001-0001)`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check Story directory exists
|
|
236
|
+
const storyDirExists = fs.existsSync(this.storyPath);
|
|
237
|
+
this.debug('[DEBUG] Story directory check', { storyPath: this.storyPath, exists: storyDirExists });
|
|
238
|
+
if (!storyDirExists) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Story ${this.storyId} not found.\nPlease run /sprint-planning first to create Stories.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check Story work.json exists
|
|
245
|
+
const storyWorkJsonPath = path.join(this.storyPath, 'work.json');
|
|
246
|
+
const workJsonExists = fs.existsSync(storyWorkJsonPath);
|
|
247
|
+
this.debug('[DEBUG] Story work.json check', { path: storyWorkJsonPath, exists: workJsonExists });
|
|
248
|
+
if (!workJsonExists) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Story metadata not found: ${this.storyId}/work.json`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check Story has no existing tasks
|
|
255
|
+
const storyWork = JSON.parse(fs.readFileSync(storyWorkJsonPath, 'utf8'));
|
|
256
|
+
this.debug('[DEBUG] Story work.json content', {
|
|
257
|
+
name: storyWork.name,
|
|
258
|
+
type: storyWork.type,
|
|
259
|
+
existingChildren: storyWork.children?.length || 0,
|
|
260
|
+
children: storyWork.children || [],
|
|
261
|
+
});
|
|
262
|
+
if (storyWork.children && storyWork.children.length > 0) {
|
|
263
|
+
this.debug('[ERROR] Story already has tasks — cannot re-seed', { children: storyWork.children });
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Story ${this.storyId} already has tasks\nChildren: ${storyWork.children.join(', ')}\nCannot re-seed a Story that already has Tasks.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check Story context.md exists
|
|
270
|
+
const storyContextPath = path.join(this.storyPath, 'context.md');
|
|
271
|
+
const contextExists = fs.existsSync(storyContextPath);
|
|
272
|
+
this.debug('[DEBUG] Story context.md check', { path: storyContextPath, exists: contextExists });
|
|
273
|
+
if (!contextExists) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Story context not found: ${this.storyId}/context.md`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.debug('[INFO] validatePrerequisites() passed — all checks OK');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// STAGE 2: Read Story context and hierarchy
|
|
283
|
+
readStoryContext() {
|
|
284
|
+
this.debug('[INFO] readStoryContext() called', { storyPath: this.storyPath });
|
|
285
|
+
|
|
286
|
+
// Read Story work.json
|
|
287
|
+
const storyWorkJsonPath = path.join(this.storyPath, 'work.json');
|
|
288
|
+
const storyWork = JSON.parse(fs.readFileSync(storyWorkJsonPath, 'utf8'));
|
|
289
|
+
this.debug('[DEBUG] Story work.json loaded', {
|
|
290
|
+
name: storyWork.name,
|
|
291
|
+
type: storyWork.type,
|
|
292
|
+
id: storyWork.id,
|
|
293
|
+
description: storyWork.description?.substring(0, 100),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Read Story context.md
|
|
297
|
+
const storyContextPath = path.join(this.storyPath, 'context.md');
|
|
298
|
+
const storyContext = fs.readFileSync(storyContextPath, 'utf8');
|
|
299
|
+
this.debug('[DEBUG] Story context.md loaded', { sizeChars: storyContext.length });
|
|
300
|
+
|
|
301
|
+
// Extract Epic ID from Story ID (context-0001-0001 → context-0001)
|
|
302
|
+
const epicId = this.extractEpicId(this.storyId);
|
|
303
|
+
const epicPath = path.join(this.projectPath, epicId);
|
|
304
|
+
|
|
305
|
+
// Read Epic context.md
|
|
306
|
+
const epicContextPath = path.join(epicPath, 'context.md');
|
|
307
|
+
let epicContext = '';
|
|
308
|
+
if (fs.existsSync(epicContextPath)) {
|
|
309
|
+
epicContext = fs.readFileSync(epicContextPath, 'utf8');
|
|
310
|
+
this.debug('[DEBUG] Epic context.md loaded', { epicId, sizeChars: epicContext.length });
|
|
311
|
+
} else {
|
|
312
|
+
this.debug('[WARNING] Epic context.md not found', { epicContextPath });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Read Epic work.json
|
|
316
|
+
const epicWorkJsonPath = path.join(epicPath, 'work.json');
|
|
317
|
+
let epicWork = {};
|
|
318
|
+
if (fs.existsSync(epicWorkJsonPath)) {
|
|
319
|
+
epicWork = JSON.parse(fs.readFileSync(epicWorkJsonPath, 'utf8'));
|
|
320
|
+
this.debug('[DEBUG] Epic work.json loaded', { epicName: epicWork.name, storyCount: epicWork.children?.length });
|
|
321
|
+
} else {
|
|
322
|
+
this.debug('[WARNING] Epic work.json not found', { epicWorkJsonPath });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Read Project context.md
|
|
326
|
+
const projectContextPath = path.join(this.projectPath, 'project/context.md');
|
|
327
|
+
let projectContext = '';
|
|
328
|
+
if (fs.existsSync(projectContextPath)) {
|
|
329
|
+
projectContext = fs.readFileSync(projectContextPath, 'utf8');
|
|
330
|
+
this.debug('[DEBUG] Project context.md loaded', { sizeChars: projectContext.length });
|
|
331
|
+
} else {
|
|
332
|
+
this.debug('[WARNING] Project context.md not found', { projectContextPath });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.debug('[INFO] readStoryContext() complete', {
|
|
336
|
+
storyName: storyWork.name,
|
|
337
|
+
epicId,
|
|
338
|
+
hasEpicContext: !!epicContext,
|
|
339
|
+
hasProjectContext: !!projectContext,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
storyWork,
|
|
344
|
+
storyContext,
|
|
345
|
+
epicWork,
|
|
346
|
+
epicContext,
|
|
347
|
+
projectContext
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// STAGE 3: Decompose Story → Tasks + Subtasks
|
|
352
|
+
async decomposeIntoTasksSubtasks(contextData) {
|
|
353
|
+
const startTime = Date.now();
|
|
354
|
+
this.debug('[INFO] decomposeIntoTasksSubtasks() called', { storyId: this.storyId });
|
|
355
|
+
|
|
356
|
+
if (!this.llmProvider) {
|
|
357
|
+
this.debug('[INFO] Initializing LLM provider', { provider: this._providerName, model: this._modelName });
|
|
358
|
+
await this.initializeLLMProvider();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!this.llmProvider) {
|
|
362
|
+
this.debug('[ERROR] LLM provider initialization failed');
|
|
363
|
+
throw new Error('LLM provider required for decomposition');
|
|
364
|
+
}
|
|
365
|
+
this.debug('[INFO] LLM provider ready', { provider: this._providerName, model: this._modelName });
|
|
366
|
+
|
|
367
|
+
// Read agent instructions
|
|
368
|
+
const taskSubtaskDecomposerAgent = fs.readFileSync(
|
|
369
|
+
path.join(this.agentsPath, 'task-subtask-decomposer.md'),
|
|
370
|
+
'utf8'
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Build prompt
|
|
374
|
+
const { storyWork, storyContext, epicWork, epicContext, projectContext } = contextData;
|
|
375
|
+
|
|
376
|
+
let prompt = `Given the following Story, decompose it into Tasks and Subtasks:
|
|
377
|
+
|
|
378
|
+
**Story ID:** ${this.storyId}
|
|
379
|
+
**Story Name:** ${storyWork.name}
|
|
380
|
+
**Story Description:** ${storyWork.description}
|
|
381
|
+
**User Type:** ${storyWork.userType}
|
|
382
|
+
**Acceptance Criteria:**
|
|
383
|
+
${storyWork.acceptance.map((ac, i) => `${i + 1}. ${ac}`).join('\n')}
|
|
384
|
+
|
|
385
|
+
**Story Context:**
|
|
386
|
+
${storyContext}
|
|
387
|
+
|
|
388
|
+
**Epic Context (${epicWork.name}):**
|
|
389
|
+
${epicContext}
|
|
390
|
+
|
|
391
|
+
**Project Context:**
|
|
392
|
+
${projectContext}
|
|
393
|
+
|
|
394
|
+
Decompose this Story into:
|
|
395
|
+
- 2-5 Tasks (technical components: backend, frontend, database, testing, infrastructure)
|
|
396
|
+
- 1-3 Subtasks per Task (atomic work units, implementable in 1-4 hours)
|
|
397
|
+
|
|
398
|
+
Return your response as JSON following the exact structure specified in your instructions.`;
|
|
399
|
+
|
|
400
|
+
this.debug('[INFO] Calling LLM for Task/Subtask decomposition', {
|
|
401
|
+
provider: this._providerName,
|
|
402
|
+
model: this._modelName,
|
|
403
|
+
storyName: contextData.storyWork?.name,
|
|
404
|
+
promptLengthChars: prompt.length,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const hierarchy = await this.retryWithBackoff(
|
|
408
|
+
() => this.llmProvider.generateJSON(prompt, taskSubtaskDecomposerAgent),
|
|
409
|
+
'Task/Subtask decomposition'
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (!hierarchy.tasks || !Array.isArray(hierarchy.tasks)) {
|
|
413
|
+
this.debug('[ERROR] Invalid LLM response — missing tasks array', { responseKeys: Object.keys(hierarchy) });
|
|
414
|
+
throw new Error('Invalid decomposition response: missing tasks array');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Calculate total subtasks
|
|
418
|
+
const totalSubtasks = hierarchy.tasks.reduce((sum, task) => sum + (task.subtasks?.length || 0), 0);
|
|
419
|
+
|
|
420
|
+
this.debug('[INFO] Decomposition complete', {
|
|
421
|
+
taskCount: hierarchy.tasks.length,
|
|
422
|
+
totalSubtasks,
|
|
423
|
+
taskNames: hierarchy.tasks.map(t => t.name),
|
|
424
|
+
duration: `${Date.now() - startTime}ms`,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
return hierarchy;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// STAGE 4: Validate Task/Subtask structure
|
|
432
|
+
validateTaskSubtaskStructure(hierarchy) {
|
|
433
|
+
this.debug('[INFO] validateTaskSubtaskStructure() called', { taskCount: hierarchy.tasks?.length });
|
|
434
|
+
|
|
435
|
+
const { tasks } = hierarchy;
|
|
436
|
+
|
|
437
|
+
if (tasks.length < 2 || tasks.length > 5) {
|
|
438
|
+
this.debug('[WARNING] Unexpected task count', { count: tasks.length, expected: '2-5' });
|
|
439
|
+
sendWarning(`Expected 2-5 Tasks, got ${tasks.length}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const task of tasks) {
|
|
443
|
+
const subtaskCount = task.subtasks?.length || 0;
|
|
444
|
+
if (subtaskCount < 1 || subtaskCount > 3) {
|
|
445
|
+
this.debug('[WARNING] Unexpected subtask count', { task: task.name, count: subtaskCount, expected: '1-3' });
|
|
446
|
+
sendWarning(`Task ${task.name} has ${subtaskCount} Subtasks (expected 1-3)`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Validate Task ID format (context-XXXX-XXXX-XXXX)
|
|
450
|
+
if (!task.id || !task.id.match(/^context-\d{4}-\d{4}-\d{4}$/)) {
|
|
451
|
+
throw new Error(`Invalid Task ID format: ${task.id}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Validate Subtask ID format (context-XXXX-XXXX-XXXX-XXXX)
|
|
455
|
+
for (const subtask of task.subtasks || []) {
|
|
456
|
+
if (!subtask.id || !subtask.id.match(/^context-\d{4}-\d{4}-\d{4}-\d{4}$/)) {
|
|
457
|
+
throw new Error(`Invalid Subtask ID format: ${subtask.id}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// STAGE 5-6: Generate contexts
|
|
464
|
+
async generateContext(level, id, data, agentInstructions) {
|
|
465
|
+
const prompt = this.buildContextPrompt(level, id, data);
|
|
466
|
+
const result = await this.llmProvider.generateJSON(prompt, agentInstructions);
|
|
467
|
+
|
|
468
|
+
if (!result.contextMarkdown || !result.tokenCount) {
|
|
469
|
+
throw new Error(`Invalid context response for ${id}: missing required fields`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
buildContextPrompt(level, id, data) {
|
|
476
|
+
const { projectContext, epicContext, storyContext, task, subtask } = data;
|
|
477
|
+
|
|
478
|
+
let prompt = `Generate a context.md file for the following ${level}:\n\n`;
|
|
479
|
+
prompt += `**Level:** ${level}\n`;
|
|
480
|
+
prompt += `**ID:** ${id}\n\n`;
|
|
481
|
+
|
|
482
|
+
if (level === 'task') {
|
|
483
|
+
prompt += `**Task Name:** ${task.name}\n`;
|
|
484
|
+
prompt += `**Task Category:** ${task.category}\n`;
|
|
485
|
+
prompt += `**Task Description:** ${task.description}\n`;
|
|
486
|
+
prompt += `**Technical Scope:** ${task.technicalScope}\n`;
|
|
487
|
+
prompt += `**Acceptance Criteria:**\n${task.acceptance.map((ac, i) => `${i + 1}. ${ac}`).join('\n')}\n\n`;
|
|
488
|
+
prompt += `**Story Context:**\n${storyContext}\n\n`;
|
|
489
|
+
prompt += `**Epic Context:**\n${epicContext}\n\n`;
|
|
490
|
+
prompt += `**Project Context:**\n${projectContext}\n\n`;
|
|
491
|
+
} else if (level === 'subtask') {
|
|
492
|
+
prompt += `**Subtask Name:** ${subtask.name}\n`;
|
|
493
|
+
prompt += `**Subtask Description:** ${subtask.description}\n`;
|
|
494
|
+
prompt += `**Technical Details:** ${subtask.technicalDetails}\n`;
|
|
495
|
+
prompt += `**Acceptance Criteria:**\n${subtask.acceptance.map((ac, i) => `${i + 1}. ${ac}`).join('\n')}\n\n`;
|
|
496
|
+
prompt += `**Task Context (${task.name}):**\n`;
|
|
497
|
+
prompt += `- Category: ${task.category}\n`;
|
|
498
|
+
prompt += `- Technical Scope: ${task.technicalScope}\n\n`;
|
|
499
|
+
prompt += `**Story Context:**\n${storyContext}\n\n`;
|
|
500
|
+
prompt += `**Epic Context:**\n${epicContext}\n\n`;
|
|
501
|
+
prompt += `**Project Context:**\n${projectContext}\n\n`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
prompt += `Return your response as JSON following the exact structure specified in your instructions.`;
|
|
505
|
+
|
|
506
|
+
return prompt;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// STAGE 7: Write Task/Subtask files
|
|
510
|
+
async writeTaskSubtaskFiles(hierarchy, contextData) {
|
|
511
|
+
// Read agent
|
|
512
|
+
const featureContextGeneratorAgent = fs.readFileSync(
|
|
513
|
+
path.join(this.agentsPath, 'feature-context-generator.md'),
|
|
514
|
+
'utf8'
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const { storyContext, epicContext, projectContext } = contextData;
|
|
518
|
+
|
|
519
|
+
// Read story doc.md for hierarchical doc distribution (story → task → subtask)
|
|
520
|
+
const storyDocPath = path.join(this.storyPath, 'doc.md');
|
|
521
|
+
let storyDocContent = '';
|
|
522
|
+
const doDistribute = fs.existsSync(storyDocPath);
|
|
523
|
+
if (doDistribute) {
|
|
524
|
+
storyDocContent = fs.readFileSync(storyDocPath, 'utf8');
|
|
525
|
+
this.debug('[INFO] Story doc.md loaded for doc distribution', { sizeChars: storyDocContent.length });
|
|
526
|
+
} else {
|
|
527
|
+
this.debug('[WARNING] Story doc.md not found — doc distribution skipped, using stub task docs');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let taskCount = 0;
|
|
531
|
+
let subtaskCount = 0;
|
|
532
|
+
const taskIds = [];
|
|
533
|
+
|
|
534
|
+
for (const task of hierarchy.tasks) {
|
|
535
|
+
const taskDir = path.join(this.projectPath, task.id);
|
|
536
|
+
|
|
537
|
+
if (!fs.existsSync(taskDir)) {
|
|
538
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Distribute documentation: extract task-specific content from story doc
|
|
542
|
+
let taskDocContent;
|
|
543
|
+
if (doDistribute) {
|
|
544
|
+
this.debug(`[INFO] Distributing docs: story/doc.md → ${task.id}/doc.md`);
|
|
545
|
+
const distributed = await this.distributeDocContent(storyDocContent, task, 'task');
|
|
546
|
+
taskDocContent = distributed.childDoc;
|
|
547
|
+
storyDocContent = distributed.parentDoc;
|
|
548
|
+
this.debug(`[INFO] After distribution: story doc ${storyDocContent.length} bytes, task doc ${taskDocContent.length} bytes`);
|
|
549
|
+
} else {
|
|
550
|
+
taskDocContent = `# ${task.name}\n\n${task.description || ''}\n`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Generate and write Task context.md
|
|
554
|
+
const taskContext = await this.retryWithBackoff(
|
|
555
|
+
() => this.generateContext('task', task.id, { projectContext, epicContext, storyContext, task }, featureContextGeneratorAgent),
|
|
556
|
+
`Task ${task.id} context`
|
|
557
|
+
);
|
|
558
|
+
fs.writeFileSync(
|
|
559
|
+
path.join(taskDir, 'context.md'),
|
|
560
|
+
taskContext.contextMarkdown,
|
|
561
|
+
'utf8'
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Write Task work.json
|
|
565
|
+
const taskWorkJson = {
|
|
566
|
+
id: task.id,
|
|
567
|
+
name: task.name,
|
|
568
|
+
type: 'task',
|
|
569
|
+
category: task.category,
|
|
570
|
+
description: task.description,
|
|
571
|
+
technicalScope: task.technicalScope,
|
|
572
|
+
acceptance: task.acceptance,
|
|
573
|
+
status: 'planned',
|
|
574
|
+
dependencies: task.dependencies || [],
|
|
575
|
+
children: (task.subtasks || []).map(s => s.id),
|
|
576
|
+
metadata: {
|
|
577
|
+
created: new Date().toISOString(),
|
|
578
|
+
ceremony: this.ceremonyName,
|
|
579
|
+
tokenBudget: taskContext.tokenCount
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
fs.writeFileSync(
|
|
583
|
+
path.join(taskDir, 'work.json'),
|
|
584
|
+
JSON.stringify(taskWorkJson, null, 2),
|
|
585
|
+
'utf8'
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
taskCount++;
|
|
589
|
+
taskIds.push(task.id);
|
|
590
|
+
|
|
591
|
+
// Write Subtask files
|
|
592
|
+
for (const subtask of task.subtasks || []) {
|
|
593
|
+
const subtaskDir = path.join(this.projectPath, subtask.id);
|
|
594
|
+
|
|
595
|
+
if (!fs.existsSync(subtaskDir)) {
|
|
596
|
+
fs.mkdirSync(subtaskDir, { recursive: true });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Distribute documentation: extract subtask-specific content from task doc
|
|
600
|
+
let subtaskDocContent;
|
|
601
|
+
if (doDistribute) {
|
|
602
|
+
this.debug(`[INFO] Distributing docs: ${task.id}/doc.md → ${subtask.id}/doc.md`);
|
|
603
|
+
const distributed = await this.distributeDocContent(taskDocContent, subtask, 'subtask');
|
|
604
|
+
subtaskDocContent = distributed.childDoc;
|
|
605
|
+
taskDocContent = distributed.parentDoc;
|
|
606
|
+
this.debug(`[INFO] After distribution: task doc ${taskDocContent.length} bytes, subtask doc ${subtaskDocContent.length} bytes`);
|
|
607
|
+
} else {
|
|
608
|
+
subtaskDocContent = `# ${subtask.name}\n\n${subtask.description || ''}\n`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Write Subtask doc.md with distributed content
|
|
612
|
+
fs.writeFileSync(
|
|
613
|
+
path.join(subtaskDir, 'doc.md'),
|
|
614
|
+
subtaskDocContent,
|
|
615
|
+
'utf8'
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
// Generate and write Subtask context.md
|
|
619
|
+
const subtaskContext = await this.retryWithBackoff(
|
|
620
|
+
() => this.generateContext('subtask', subtask.id, { projectContext, epicContext, storyContext, task, subtask }, featureContextGeneratorAgent),
|
|
621
|
+
`Subtask ${subtask.id} context`
|
|
622
|
+
);
|
|
623
|
+
fs.writeFileSync(
|
|
624
|
+
path.join(subtaskDir, 'context.md'),
|
|
625
|
+
subtaskContext.contextMarkdown,
|
|
626
|
+
'utf8'
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
// Write Subtask work.json
|
|
630
|
+
const subtaskWorkJson = {
|
|
631
|
+
id: subtask.id,
|
|
632
|
+
name: subtask.name,
|
|
633
|
+
type: 'subtask',
|
|
634
|
+
description: subtask.description,
|
|
635
|
+
technicalDetails: subtask.technicalDetails,
|
|
636
|
+
acceptance: subtask.acceptance,
|
|
637
|
+
status: 'planned',
|
|
638
|
+
dependencies: subtask.dependencies || [],
|
|
639
|
+
children: [], // Subtasks have no children
|
|
640
|
+
metadata: {
|
|
641
|
+
created: new Date().toISOString(),
|
|
642
|
+
ceremony: this.ceremonyName,
|
|
643
|
+
tokenBudget: subtaskContext.tokenCount
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
fs.writeFileSync(
|
|
647
|
+
path.join(subtaskDir, 'work.json'),
|
|
648
|
+
JSON.stringify(subtaskWorkJson, null, 2),
|
|
649
|
+
'utf8'
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
subtaskCount++;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Write Task doc.md AFTER all subtasks have extracted their portions
|
|
656
|
+
fs.writeFileSync(
|
|
657
|
+
path.join(taskDir, 'doc.md'),
|
|
658
|
+
taskDocContent,
|
|
659
|
+
'utf8'
|
|
660
|
+
);
|
|
661
|
+
this.debug(`[INFO] Wrote ${task.id}/doc.md (${taskDocContent.length} bytes)`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Write final story doc.md AFTER all tasks extracted their portions
|
|
665
|
+
if (doDistribute) {
|
|
666
|
+
fs.writeFileSync(storyDocPath, storyDocContent, 'utf8');
|
|
667
|
+
this.debug(`[INFO] Updated story doc.md after task extraction (${storyDocContent.length} bytes)`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return { taskCount, subtaskCount, taskIds };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// STAGE 8: Update Story work.json
|
|
674
|
+
updateStoryWorkJson(taskIds) {
|
|
675
|
+
const storyWorkJsonPath = path.join(this.storyPath, 'work.json');
|
|
676
|
+
const storyWork = JSON.parse(fs.readFileSync(storyWorkJsonPath, 'utf8'));
|
|
677
|
+
|
|
678
|
+
storyWork.children = taskIds;
|
|
679
|
+
storyWork.metadata.lastUpdated = new Date().toISOString();
|
|
680
|
+
storyWork.metadata.seeded = true;
|
|
681
|
+
|
|
682
|
+
fs.writeFileSync(
|
|
683
|
+
storyWorkJsonPath,
|
|
684
|
+
JSON.stringify(storyWork, null, 2),
|
|
685
|
+
'utf8'
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Display summary
|
|
691
|
+
displaySummary(hierarchy, contextData, taskCount, subtaskCount) {
|
|
692
|
+
sendOutput(`${contextData.storyWork.name}: ${taskCount} Tasks, ${subtaskCount} Subtasks created.`);
|
|
693
|
+
sendOutput('Run /seed on each task to continue decomposition.');
|
|
694
|
+
for (const task of hierarchy.tasks) {
|
|
695
|
+
sendIndented(`Task: ${task.name} (${task.id})`, 1);
|
|
696
|
+
for (const subtask of task.subtasks || []) {
|
|
697
|
+
sendIndented(`- Subtask: ${subtask.name}`, 2);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Main execution method
|
|
703
|
+
async execute() {
|
|
704
|
+
const execStartTime = Date.now();
|
|
705
|
+
this.debug('[INFO] SeedProcessor.execute() started', {
|
|
706
|
+
storyId: this.storyId,
|
|
707
|
+
storyPath: this.storyPath,
|
|
708
|
+
provider: this._providerName,
|
|
709
|
+
model: this._modelName,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
try {
|
|
713
|
+
const header = getCeremonyHeader('seed');
|
|
714
|
+
console.log(`\n${header.title}\n`);
|
|
715
|
+
console.log(`Story: ${this.storyId}\n`);
|
|
716
|
+
|
|
717
|
+
// Stage 1: Validate
|
|
718
|
+
this.debug('[INFO] Stage 1/3: Validating prerequisites');
|
|
719
|
+
this.validatePrerequisites();
|
|
720
|
+
|
|
721
|
+
// Stage 2: Read Story context
|
|
722
|
+
this.debug('[INFO] Stage 2a: Reading Story context files');
|
|
723
|
+
sendInfo('Reading Story context...');
|
|
724
|
+
const contextData = this.readStoryContext();
|
|
725
|
+
|
|
726
|
+
// Stage 3: Decompose
|
|
727
|
+
this.debug('[INFO] Stage 2b: Decomposing Story into Tasks/Subtasks via LLM');
|
|
728
|
+
let hierarchy = await this.decomposeIntoTasksSubtasks(contextData);
|
|
729
|
+
|
|
730
|
+
// Stage 4: Validate
|
|
731
|
+
this.debug('[INFO] Stage 2c: Validating decomposition structure');
|
|
732
|
+
this.validateTaskSubtaskStructure(hierarchy);
|
|
733
|
+
|
|
734
|
+
// Stage 5-7: Generate contexts and write files
|
|
735
|
+
this.debug('[INFO] Stage 2/3: Generating context files and writing to disk');
|
|
736
|
+
sendSectionHeader('Stage 2/3: Generating context files');
|
|
737
|
+
const { taskCount, subtaskCount, taskIds } = await this.writeTaskSubtaskFiles(hierarchy, contextData);
|
|
738
|
+
this.debug('[INFO] Files written', { taskCount, subtaskCount, taskIds });
|
|
739
|
+
|
|
740
|
+
// Stage 8: Update Story work.json
|
|
741
|
+
this.debug('[INFO] Stage 3/3: Updating Story work.json with task IDs');
|
|
742
|
+
this.updateStoryWorkJson(taskIds);
|
|
743
|
+
|
|
744
|
+
// Display summary
|
|
745
|
+
this.displaySummary(hierarchy, contextData, taskCount, subtaskCount);
|
|
746
|
+
|
|
747
|
+
// Display token usage
|
|
748
|
+
if (this.llmProvider) {
|
|
749
|
+
const usage = this.llmProvider.getTokenUsage();
|
|
750
|
+
this.debug('[INFO] Token usage summary', {
|
|
751
|
+
inputTokens: usage.inputTokens,
|
|
752
|
+
outputTokens: usage.outputTokens,
|
|
753
|
+
totalTokens: usage.totalTokens,
|
|
754
|
+
apiCalls: usage.totalCalls,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
sendSectionHeader('Token Usage');
|
|
758
|
+
sendIndented(`Input: ${usage.inputTokens.toLocaleString()} tokens`, 1);
|
|
759
|
+
sendIndented(`Output: ${usage.outputTokens.toLocaleString()} tokens`, 1);
|
|
760
|
+
sendIndented(`Total: ${usage.totalTokens.toLocaleString()} tokens`, 1);
|
|
761
|
+
sendIndented(`API Calls: ${usage.totalCalls}`, 1);
|
|
762
|
+
|
|
763
|
+
this.tokenTracker.finalizeRun(this.ceremonyName);
|
|
764
|
+
this.debug('[INFO] Token history finalized in .avc/token-history.json');
|
|
765
|
+
sendSuccess('Token history updated');
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
sendSectionHeader('Next steps');
|
|
769
|
+
sendIndented('1. Review Task/Subtask breakdown in .avc/project/', 1);
|
|
770
|
+
sendIndented('2. Start implementing Subtasks (smallest work units)', 1);
|
|
771
|
+
|
|
772
|
+
this.debug('[INFO] SeedProcessor.execute() complete', {
|
|
773
|
+
storyId: this.storyId,
|
|
774
|
+
duration: `${Date.now() - execStartTime}ms`,
|
|
775
|
+
taskCount,
|
|
776
|
+
subtaskCount,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
} catch (error) {
|
|
780
|
+
this.debug('[ERROR] SeedProcessor.execute() failed', {
|
|
781
|
+
error: error.message,
|
|
782
|
+
stack: error.stack,
|
|
783
|
+
storyId: this.storyId,
|
|
784
|
+
duration: `${Date.now() - execStartTime}ms`,
|
|
785
|
+
});
|
|
786
|
+
sendError(`Seed ceremony failed: ${error.message}`);
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export { SeedProcessor };
|