@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,1813 @@
|
|
|
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 { EpicStoryValidator } from './epic-story-validator.js';
|
|
6
|
+
import { VerificationTracker } from './verification-tracker.js';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { getCeremonyHeader } from './message-constants.js';
|
|
9
|
+
import { sendError, sendWarning, sendSuccess, sendInfo, sendOutput, sendIndented, sendSectionHeader, sendCeremonyHeader, sendProgress, sendSubstep } from './messaging-api.js';
|
|
10
|
+
import { outputBuffer } from './output-buffer.js';
|
|
11
|
+
import { loadAgent } from './agent-loader.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
/** Local-timezone ISO string (e.g. 2026-03-04T18:05:16.554+01:00) */
|
|
17
|
+
function localISO(date = new Date()) {
|
|
18
|
+
const p = n => String(n).padStart(2, '0');
|
|
19
|
+
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
|
20
|
+
const tz = -date.getTimezoneOffset();
|
|
21
|
+
const sign = tz >= 0 ? '+' : '-';
|
|
22
|
+
const tzH = p(Math.floor(Math.abs(tz) / 60));
|
|
23
|
+
const tzM = p(Math.abs(tz) % 60);
|
|
24
|
+
return `${date.getFullYear()}-${p(date.getMonth()+1)}-${p(date.getDate())}T${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}.${ms}${sign}${tzH}:${tzM}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SprintPlanningProcessor - Creates/expands Epics and Stories with duplicate detection
|
|
29
|
+
*/
|
|
30
|
+
class SprintPlanningProcessor {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.ceremonyName = 'sprint-planning';
|
|
33
|
+
this.avcPath = path.join(process.cwd(), '.avc');
|
|
34
|
+
this.projectPath = path.join(this.avcPath, 'project');
|
|
35
|
+
this.projectDocPath = path.join(this.projectPath, 'doc.md');
|
|
36
|
+
this.avcConfigPath = path.join(this.avcPath, 'avc.json');
|
|
37
|
+
this.agentsPath = path.join(__dirname, 'agents');
|
|
38
|
+
|
|
39
|
+
// Read ceremony config
|
|
40
|
+
const { provider, model, stagesConfig } = this.readCeremonyConfig();
|
|
41
|
+
this._providerName = provider;
|
|
42
|
+
this._modelName = model;
|
|
43
|
+
this.llmProvider = null;
|
|
44
|
+
this.stagesConfig = stagesConfig;
|
|
45
|
+
|
|
46
|
+
// Stage provider cache
|
|
47
|
+
this._stageProviders = {};
|
|
48
|
+
|
|
49
|
+
// Initialize token tracker
|
|
50
|
+
this.tokenTracker = new TokenTracker(this.avcPath);
|
|
51
|
+
this.tokenTracker.init();
|
|
52
|
+
|
|
53
|
+
// Initialize verification tracker
|
|
54
|
+
this.verificationTracker = new VerificationTracker(this.avcPath);
|
|
55
|
+
|
|
56
|
+
// Debug mode - always enabled for comprehensive logging
|
|
57
|
+
this.debugMode = true;
|
|
58
|
+
|
|
59
|
+
// Cost threshold protection
|
|
60
|
+
this._costThreshold = options?.costThreshold ?? null;
|
|
61
|
+
this._costLimitReachedCallback = options?.costLimitReachedCallback ?? null;
|
|
62
|
+
this._runningCost = 0;
|
|
63
|
+
|
|
64
|
+
// Optional user-selection gate between Stage 4 and Stage 5
|
|
65
|
+
// When provided, the processor calls this async function with the decomposed hierarchy
|
|
66
|
+
// and waits for it to resolve with { selectedEpicIds, selectedStoryIds }.
|
|
67
|
+
// When null (default), the processor runs straight through without pausing.
|
|
68
|
+
this._selectionCallback = options?.selectionCallback ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Structured debug logger - writes ONLY to file via CommandLogger
|
|
73
|
+
*/
|
|
74
|
+
debug(message, data = null) {
|
|
75
|
+
if (!this.debugMode) return;
|
|
76
|
+
|
|
77
|
+
const timestamp = localISO();
|
|
78
|
+
const prefix = `[${timestamp}] [DEBUG]`;
|
|
79
|
+
|
|
80
|
+
if (data === null) {
|
|
81
|
+
console.log(`${prefix} ${message}`);
|
|
82
|
+
} else {
|
|
83
|
+
// Combine message and data in single log call with [DEBUG] prefix
|
|
84
|
+
console.log(`${prefix} ${message}\n${JSON.stringify(data, null, 2)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stage boundary marker
|
|
90
|
+
*/
|
|
91
|
+
debugStage(stageNumber, stageName) {
|
|
92
|
+
const separator = '='.repeat(50);
|
|
93
|
+
this.debug(`\n${separator}`);
|
|
94
|
+
this.debug(`STAGE ${stageNumber}: ${stageName.toUpperCase()}`);
|
|
95
|
+
this.debug(separator);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Log elapsed time for a labelled operation
|
|
100
|
+
*/
|
|
101
|
+
debugTiming(label, startMs) {
|
|
102
|
+
const elapsed = Date.now() - startMs;
|
|
103
|
+
this.debug(`[TIMING] ${label}: ${elapsed}ms (${(elapsed / 1000).toFixed(1)}s)`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sub-section separator for grouping related log entries within a stage
|
|
108
|
+
*/
|
|
109
|
+
debugSection(title) {
|
|
110
|
+
const line = '-'.repeat(60);
|
|
111
|
+
this.debug(`\n${line}`);
|
|
112
|
+
this.debug(`-- ${title}`);
|
|
113
|
+
this.debug(line);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Log a full hierarchy tree for snapshot comparison across runs
|
|
118
|
+
* @param {string} label - Label for this snapshot (e.g. "PRE-RUN" or "POST-RUN")
|
|
119
|
+
* @param {Array} epics - Array of {id, name, stories:[{id, name}]} objects
|
|
120
|
+
*/
|
|
121
|
+
debugHierarchySnapshot(label, epics) {
|
|
122
|
+
this.debugSection(`${label} HIERARCHY SNAPSHOT`);
|
|
123
|
+
if (!epics || epics.length === 0) {
|
|
124
|
+
this.debug(`${label}: (empty - no epics found)`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.debug(`${label}: ${epics.length} epics`);
|
|
128
|
+
for (const epic of epics) {
|
|
129
|
+
const storyCount = epic.stories ? epic.stories.length : 0;
|
|
130
|
+
this.debug(` [${epic.id}] ${epic.name} (${storyCount} stories)`);
|
|
131
|
+
for (const story of epic.stories || []) {
|
|
132
|
+
this.debug(` [${story.id}] ${story.name}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Flat totals for quick comparison
|
|
136
|
+
const totalStories = epics.reduce((sum, e) => sum + (e.stories?.length || 0), 0);
|
|
137
|
+
this.debug(`${label} TOTALS: ${epics.length} epics, ${totalStories} stories`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* API call logging with timing
|
|
142
|
+
*/
|
|
143
|
+
async debugApiCall(operation, fn) {
|
|
144
|
+
this.debug(`\n${'='.repeat(50)}`);
|
|
145
|
+
this.debug(`LLM API CALL: ${operation}`);
|
|
146
|
+
this.debug('='.repeat(50));
|
|
147
|
+
this.debug(`Provider: ${this._providerName}`);
|
|
148
|
+
this.debug(`Model: ${this._modelName}`);
|
|
149
|
+
|
|
150
|
+
const startTime = Date.now();
|
|
151
|
+
try {
|
|
152
|
+
const result = await fn();
|
|
153
|
+
const duration = Date.now() - startTime;
|
|
154
|
+
|
|
155
|
+
this.debug(`Response received (${duration}ms)`);
|
|
156
|
+
return result;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const duration = Date.now() - startTime;
|
|
159
|
+
this.debug(`API call failed after ${duration}ms`, {
|
|
160
|
+
error: error.message,
|
|
161
|
+
stack: error.stack
|
|
162
|
+
});
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
readCeremonyConfig() {
|
|
168
|
+
try {
|
|
169
|
+
const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
|
|
170
|
+
const ceremony = config.settings?.ceremonies?.find(c => c.name === this.ceremonyName);
|
|
171
|
+
|
|
172
|
+
if (!ceremony) {
|
|
173
|
+
sendWarning(`Ceremony '${this.ceremonyName}' not found in config, using defaults`);
|
|
174
|
+
return {
|
|
175
|
+
provider: 'claude',
|
|
176
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
177
|
+
stagesConfig: null
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
provider: ceremony.provider || 'claude',
|
|
183
|
+
model: ceremony.defaultModel || 'claude-sonnet-4-5-20250929',
|
|
184
|
+
stagesConfig: ceremony.stages || null
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
sendWarning(`Could not read ceremony config: ${error.message}`);
|
|
188
|
+
return {
|
|
189
|
+
provider: 'claude',
|
|
190
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
191
|
+
stagesConfig: null
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get provider and model for a specific stage
|
|
198
|
+
* Falls back to ceremony-level config if stage-specific config not found
|
|
199
|
+
* @param {string} stageName - Stage name ('decomposition', 'validation', 'doc-distribution')
|
|
200
|
+
* @returns {Object} { provider, model }
|
|
201
|
+
*/
|
|
202
|
+
getProviderForStage(stageName) {
|
|
203
|
+
// Check if stage-specific config exists
|
|
204
|
+
if (this.stagesConfig && this.stagesConfig[stageName]) {
|
|
205
|
+
const stageConfig = this.stagesConfig[stageName];
|
|
206
|
+
return {
|
|
207
|
+
provider: stageConfig.provider || this._providerName,
|
|
208
|
+
model: stageConfig.model || this._modelName
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Fall back to ceremony-level config
|
|
213
|
+
return {
|
|
214
|
+
provider: this._providerName,
|
|
215
|
+
model: this._modelName
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get or create LLM provider for a specific stage
|
|
221
|
+
* @param {string} stageName - Stage name ('decomposition', 'validation', 'doc-distribution')
|
|
222
|
+
* @returns {Promise<LLMProvider>} LLM provider instance
|
|
223
|
+
*/
|
|
224
|
+
async getProviderForStageInstance(stageName) {
|
|
225
|
+
const { provider, model } = this.getProviderForStage(stageName);
|
|
226
|
+
|
|
227
|
+
// Check if we already have a provider for this stage
|
|
228
|
+
const cacheKey = `${stageName}:${provider}:${model}`;
|
|
229
|
+
|
|
230
|
+
if (this._stageProviders[cacheKey]) {
|
|
231
|
+
this.debug(`Using cached provider for ${stageName}: ${provider} (${model})`);
|
|
232
|
+
return this._stageProviders[cacheKey];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Create new provider
|
|
236
|
+
this.debug(`Creating new provider for ${stageName}: ${provider} (${model})`);
|
|
237
|
+
const providerInstance = await LLMProvider.create(provider, model);
|
|
238
|
+
this._registerTokenCallback(providerInstance, `${this.ceremonyName}-${stageName}`);
|
|
239
|
+
this._stageProviders[cacheKey] = providerInstance;
|
|
240
|
+
|
|
241
|
+
return providerInstance;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run an async LLM fn with a periodic elapsed-time heartbeat detail message.
|
|
246
|
+
* Keeps the UI updated while waiting for long LLM calls to complete.
|
|
247
|
+
*/
|
|
248
|
+
async _withProgressHeartbeat(fn, getMsg, progressCallback, intervalMs = 5000) {
|
|
249
|
+
const startTime = Date.now();
|
|
250
|
+
let lastMsg = null;
|
|
251
|
+
const timer = setInterval(() => {
|
|
252
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
253
|
+
const msg = getMsg(elapsed);
|
|
254
|
+
if (msg != null && msg !== lastMsg) {
|
|
255
|
+
lastMsg = msg;
|
|
256
|
+
progressCallback?.(null, null, { detail: msg })?.catch?.(() => {});
|
|
257
|
+
}
|
|
258
|
+
}, intervalMs);
|
|
259
|
+
try {
|
|
260
|
+
return await fn();
|
|
261
|
+
} finally {
|
|
262
|
+
clearInterval(timer);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Aggregate token usage across all provider instances:
|
|
268
|
+
* - this.llmProvider (Stage 5 validation fallback)
|
|
269
|
+
* - this._stageProviders (Stage 4 decomposition, Stage 7 doc-distribution)
|
|
270
|
+
* - this._validator._validatorProviders (Stage 5 per-validator providers)
|
|
271
|
+
*/
|
|
272
|
+
_aggregateAllTokenUsage() {
|
|
273
|
+
const totals = { inputTokens: 0, outputTokens: 0, totalTokens: 0, totalCalls: 0, estimatedCost: 0 };
|
|
274
|
+
const add = (provider) => {
|
|
275
|
+
if (!provider) return;
|
|
276
|
+
const u = provider.getTokenUsage();
|
|
277
|
+
totals.inputTokens += u.inputTokens || 0;
|
|
278
|
+
totals.outputTokens += u.outputTokens || 0;
|
|
279
|
+
totals.totalTokens += u.totalTokens || (u.inputTokens || 0) + (u.outputTokens || 0);
|
|
280
|
+
totals.totalCalls += u.totalCalls || 0;
|
|
281
|
+
totals.estimatedCost += u.estimatedCost || 0;
|
|
282
|
+
};
|
|
283
|
+
add(this.llmProvider);
|
|
284
|
+
for (const p of Object.values(this._stageProviders)) add(p);
|
|
285
|
+
if (this._validator) {
|
|
286
|
+
for (const p of Object.values(this._validator._validatorProviders)) add(p);
|
|
287
|
+
}
|
|
288
|
+
return totals;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Register a per-call token callback on a provider instance.
|
|
293
|
+
* Each LLM API call fires addIncremental() so tokens are persisted crash-safely.
|
|
294
|
+
* @param {object} provider - LLM provider instance
|
|
295
|
+
* @param {string} [stageKey] - Stage-specific key (e.g. 'sprint-planning-decomposition').
|
|
296
|
+
* Defaults to this.ceremonyName so the parent roll-up bucket still accumulates totals.
|
|
297
|
+
*/
|
|
298
|
+
_registerTokenCallback(provider, stageKey) {
|
|
299
|
+
if (!provider) return;
|
|
300
|
+
const key = stageKey ?? this.ceremonyName;
|
|
301
|
+
provider.onCall((delta) => {
|
|
302
|
+
this.tokenTracker.addIncremental(key, delta);
|
|
303
|
+
if (delta.model) {
|
|
304
|
+
const cost = this.tokenTracker.calculateCost(delta.input, delta.output, delta.model);
|
|
305
|
+
this._runningCost += cost?.total ?? 0;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async initializeLLMProvider() {
|
|
311
|
+
try {
|
|
312
|
+
this.llmProvider = await LLMProvider.create(this._providerName, this._modelName);
|
|
313
|
+
this._registerTokenCallback(this.llmProvider);
|
|
314
|
+
return this.llmProvider;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
this.debug(`Could not initialize ${this._providerName} provider`);
|
|
317
|
+
this.debug(`Error: ${error.message}`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async retryWithBackoff(fn, operation, maxRetries = 3) {
|
|
323
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
324
|
+
try {
|
|
325
|
+
return await fn();
|
|
326
|
+
} catch (error) {
|
|
327
|
+
const isLastAttempt = attempt === maxRetries;
|
|
328
|
+
const isRetriable = error.message?.includes('rate limit') ||
|
|
329
|
+
error.message?.includes('timeout') ||
|
|
330
|
+
error.message?.includes('503');
|
|
331
|
+
|
|
332
|
+
if (isLastAttempt || !isRetriable) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
337
|
+
this.debug(`Retry ${attempt}/${maxRetries} in ${delay/1000}s: ${operation}`);
|
|
338
|
+
this.debug(`Error: ${error.message}`);
|
|
339
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// STAGE 1: Validate prerequisites
|
|
345
|
+
validatePrerequisites() {
|
|
346
|
+
this.debugStage(1, 'Validate Prerequisites');
|
|
347
|
+
this.debug('Checking prerequisites...');
|
|
348
|
+
|
|
349
|
+
if (!fs.existsSync(this.projectDocPath)) {
|
|
350
|
+
this.debug(`✗ Project doc missing: ${this.projectDocPath}`);
|
|
351
|
+
throw new Error(
|
|
352
|
+
'Project documentation not found. Please run /sponsor-call first.'
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const docSize = fs.statSync(this.projectDocPath).size;
|
|
356
|
+
this.debug(`✓ Project doc exists: ${this.projectDocPath} (${docSize} bytes)`);
|
|
357
|
+
|
|
358
|
+
this.debug('Prerequisites validated successfully');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// STAGE 2: Read existing hierarchy
|
|
362
|
+
readExistingHierarchy() {
|
|
363
|
+
this.debugStage(2, 'Read Existing Hierarchy');
|
|
364
|
+
|
|
365
|
+
const existingEpics = new Map(); // name -> id
|
|
366
|
+
const existingStories = new Map(); // name -> id
|
|
367
|
+
const maxEpicNum = { value: 0 };
|
|
368
|
+
const maxStoryNums = new Map(); // epicId -> maxNum
|
|
369
|
+
const preRunSnapshot = []; // Rich snapshot for cross-run comparison
|
|
370
|
+
|
|
371
|
+
if (!fs.existsSync(this.projectPath)) {
|
|
372
|
+
this.debug('Project path does not exist yet (first run)');
|
|
373
|
+
return { existingEpics, existingStories, maxEpicNum, maxStoryNums, preRunSnapshot };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.debug(`Scanning directory: ${this.projectPath}`);
|
|
377
|
+
const dirs = fs.readdirSync(this.projectPath).sort();
|
|
378
|
+
this.debug(`Found ${dirs.length} top-level entries to scan`);
|
|
379
|
+
|
|
380
|
+
// Scan top-level directories (epics)
|
|
381
|
+
for (const dir of dirs) {
|
|
382
|
+
const epicWorkJsonPath = path.join(this.projectPath, dir, 'work.json');
|
|
383
|
+
|
|
384
|
+
if (!fs.existsSync(epicWorkJsonPath)) continue;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const work = JSON.parse(fs.readFileSync(epicWorkJsonPath, 'utf8'));
|
|
388
|
+
|
|
389
|
+
if (work.type === 'epic') {
|
|
390
|
+
this.debug(`Found existing Epic: ${work.id} "${work.name}" [status=${work.status}, created=${work.metadata?.created || 'unknown'}]`);
|
|
391
|
+
existingEpics.set(work.name.toLowerCase(), work.id);
|
|
392
|
+
|
|
393
|
+
const epicEntry = {
|
|
394
|
+
id: work.id,
|
|
395
|
+
name: work.name,
|
|
396
|
+
domain: work.domain || '',
|
|
397
|
+
status: work.status || 'unknown',
|
|
398
|
+
created: work.metadata?.created || null,
|
|
399
|
+
ceremony: work.metadata?.ceremony || null,
|
|
400
|
+
description: (work.description || '').substring(0, 120),
|
|
401
|
+
stories: []
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Track max epic number (context-0001 → 1)
|
|
405
|
+
const match = work.id.match(/^context-(\d+)$/);
|
|
406
|
+
if (match) {
|
|
407
|
+
const num = parseInt(match[1], 10);
|
|
408
|
+
if (num > maxEpicNum.value) maxEpicNum.value = num;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Scan for nested stories under this epic
|
|
412
|
+
const epicDir = path.join(this.projectPath, dir);
|
|
413
|
+
const epicSubdirs = fs.readdirSync(epicDir).filter(subdir => {
|
|
414
|
+
const subdirPath = path.join(epicDir, subdir);
|
|
415
|
+
return fs.statSync(subdirPath).isDirectory();
|
|
416
|
+
}).sort();
|
|
417
|
+
|
|
418
|
+
this.debug(`Scanning ${epicSubdirs.length} subdirectories under epic ${work.id}`);
|
|
419
|
+
|
|
420
|
+
for (const storyDir of epicSubdirs) {
|
|
421
|
+
const storyWorkJsonPath = path.join(epicDir, storyDir, 'work.json');
|
|
422
|
+
|
|
423
|
+
if (!fs.existsSync(storyWorkJsonPath)) continue;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const storyWork = JSON.parse(fs.readFileSync(storyWorkJsonPath, 'utf8'));
|
|
427
|
+
|
|
428
|
+
if (storyWork.type === 'story') {
|
|
429
|
+
this.debug(` Found existing Story: ${storyWork.id} "${storyWork.name}" [status=${storyWork.status}, created=${storyWork.metadata?.created || 'unknown'}]`);
|
|
430
|
+
existingStories.set(storyWork.name.toLowerCase(), storyWork.id);
|
|
431
|
+
epicEntry.stories.push({
|
|
432
|
+
id: storyWork.id,
|
|
433
|
+
name: storyWork.name,
|
|
434
|
+
status: storyWork.status || 'unknown',
|
|
435
|
+
created: storyWork.metadata?.created || null,
|
|
436
|
+
userType: storyWork.userType || ''
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Track max story number per epic (context-0001-0003 → epic 0001, story 3)
|
|
440
|
+
const storyMatch = storyWork.id.match(/^context-(\d+)-(\d+)$/);
|
|
441
|
+
if (storyMatch) {
|
|
442
|
+
const epicId = `context-${storyMatch[1]}`;
|
|
443
|
+
const storyNum = parseInt(storyMatch[2], 10);
|
|
444
|
+
|
|
445
|
+
if (!maxStoryNums.has(epicId)) {
|
|
446
|
+
maxStoryNums.set(epicId, 0);
|
|
447
|
+
}
|
|
448
|
+
if (storyNum > maxStoryNums.get(epicId)) {
|
|
449
|
+
maxStoryNums.set(epicId, storyNum);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
this.debug(`Could not parse ${storyWorkJsonPath}: ${error.message}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
preRunSnapshot.push(epicEntry);
|
|
459
|
+
}
|
|
460
|
+
} catch (error) {
|
|
461
|
+
this.debug(`Could not parse ${epicWorkJsonPath}: ${error.message}`);
|
|
462
|
+
sendWarning(`Could not parse ${epicWorkJsonPath}: ${error.message}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Log complete pre-run state for cross-run comparison
|
|
467
|
+
this.debugSection('PRE-RUN STATE - Full Existing Hierarchy');
|
|
468
|
+
this.debug('Pre-run counts', {
|
|
469
|
+
epics: existingEpics.size,
|
|
470
|
+
stories: existingStories.size,
|
|
471
|
+
maxEpicNum: maxEpicNum.value,
|
|
472
|
+
maxStoryNums: Object.fromEntries(maxStoryNums)
|
|
473
|
+
});
|
|
474
|
+
this.debugHierarchySnapshot('PRE-RUN', preRunSnapshot);
|
|
475
|
+
this.debug('All existing epic names (for duplicate detection)', Array.from(existingEpics.keys()));
|
|
476
|
+
this.debug('All existing story names (for duplicate detection)', Array.from(existingStories.keys()));
|
|
477
|
+
|
|
478
|
+
return { existingEpics, existingStories, maxEpicNum, maxStoryNums, preRunSnapshot };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// STAGE 3: Collect new scope (optional expansion)
|
|
482
|
+
async collectNewScope() {
|
|
483
|
+
this.debugStage(3, 'Collect New Scope');
|
|
484
|
+
|
|
485
|
+
this.debug(`Reading project doc: ${this.projectDocPath}`);
|
|
486
|
+
const docContent = fs.readFileSync(this.projectDocPath, 'utf8');
|
|
487
|
+
this.debug(`Doc content loaded (${docContent.length} chars)`);
|
|
488
|
+
|
|
489
|
+
// Log full doc.md content for cross-run comparison
|
|
490
|
+
this.debugSection('PROJECT DOC.MD CONTENT (full text used as scope source)');
|
|
491
|
+
this.debug('doc.md full content:\n' + docContent);
|
|
492
|
+
|
|
493
|
+
// Try to extract scope from known section headers
|
|
494
|
+
const scopeFromSection = this.tryExtractScopeFromSections(docContent);
|
|
495
|
+
|
|
496
|
+
if (scopeFromSection) {
|
|
497
|
+
this.debug(`✓ Scope extracted from section (${scopeFromSection.length} chars)`);
|
|
498
|
+
this.debugSection('SCOPE TEXT SENT TO LLM (extracted from doc section)');
|
|
499
|
+
this.debug('Full scope text:\n' + scopeFromSection);
|
|
500
|
+
return scopeFromSection;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Fallback: Use entire doc.md
|
|
504
|
+
this.debug('⚠️ No standard scope section found');
|
|
505
|
+
this.debug('Using entire doc.md content as scope source');
|
|
506
|
+
|
|
507
|
+
sendWarning('No standard scope section found in doc.md');
|
|
508
|
+
sendIndented('Using entire documentation for feature extraction.', 1);
|
|
509
|
+
sendIndented('For better results and lower token usage, consider adding one of:', 1);
|
|
510
|
+
sendIndented('- "## Initial Scope"', 1);
|
|
511
|
+
sendIndented('- "## Scope"', 1);
|
|
512
|
+
sendIndented('- "## Features"', 1);
|
|
513
|
+
|
|
514
|
+
this.debugSection('SCOPE TEXT SENT TO LLM (full doc.md - no scope section found)');
|
|
515
|
+
this.debug(`Using full doc content (${docContent.length} chars) as scope`);
|
|
516
|
+
return docContent;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Try to extract scope from known section headers
|
|
521
|
+
* Returns null if no section found
|
|
522
|
+
*/
|
|
523
|
+
tryExtractScopeFromSections(docContent) {
|
|
524
|
+
// Section headers to try (in priority order)
|
|
525
|
+
const sectionHeaders = [
|
|
526
|
+
'Initial Scope', // Official AVC convention
|
|
527
|
+
'Scope', // Common variation
|
|
528
|
+
'Project Scope', // Formal variation
|
|
529
|
+
'Features', // Common alternative
|
|
530
|
+
'Core Features', // Detailed variation
|
|
531
|
+
'Requirements', // Specification style
|
|
532
|
+
'Functional Requirements', // Formal specification
|
|
533
|
+
'User Stories', // Agile style
|
|
534
|
+
'Feature List', // Simple list style
|
|
535
|
+
'Objectives', // Goal-oriented style
|
|
536
|
+
'Goals', // Simple goal style
|
|
537
|
+
'Deliverables', // Project management style
|
|
538
|
+
'Product Features', // Product-focused
|
|
539
|
+
'System Requirements' // Technical specification
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
this.debug(`Attempting to extract scope from known sections...`);
|
|
543
|
+
this.debug(`Trying ${sectionHeaders.length} section name variations`);
|
|
544
|
+
|
|
545
|
+
// Try each section header
|
|
546
|
+
for (const header of sectionHeaders) {
|
|
547
|
+
// Build regex (case-insensitive). Allow optional numeric prefix so
|
|
548
|
+
// "## 3. Initial Scope" matches when searching for "Initial Scope".
|
|
549
|
+
const regex = new RegExp(
|
|
550
|
+
`##\\s+(?:\\d+\\.\\s+)?${this.escapeRegex(header)}\\s+([\\s\\S]+?)(?=\\n#{1,2}[^#]|$)`,
|
|
551
|
+
'i'
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
const match = docContent.match(regex);
|
|
555
|
+
|
|
556
|
+
if (match && match[1].trim().length > 0) {
|
|
557
|
+
const scope = match[1].trim();
|
|
558
|
+
this.debug(`✓ Found scope in section: "## ${header}"`);
|
|
559
|
+
this.debug(`Extracted ${scope.length} chars`);
|
|
560
|
+
return scope;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
this.debug(`✗ Section "## ${header}" not found or empty`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this.debug('✗ No known scope section found');
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Escape special regex characters in section names
|
|
572
|
+
*/
|
|
573
|
+
escapeRegex(str) {
|
|
574
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// STAGE 4: Decompose into Epics + Stories
|
|
578
|
+
async decomposeIntoEpicsStories(scope, existingEpics, existingStories, progressCallback = null) {
|
|
579
|
+
this.debugStage(4, 'Decompose into Epics + Stories');
|
|
580
|
+
|
|
581
|
+
this.debug('Stage 1/3: Decomposing scope into Epics and Stories');
|
|
582
|
+
|
|
583
|
+
// Get stage-specific provider for decomposition
|
|
584
|
+
const provider = await this.getProviderForStageInstance('decomposition');
|
|
585
|
+
const { provider: providerName, model: modelName } = this.getProviderForStage('decomposition');
|
|
586
|
+
|
|
587
|
+
this.debug('Using provider for decomposition', { provider: providerName, model: modelName });
|
|
588
|
+
await progressCallback?.(null, `Using model: ${modelName}`, {});
|
|
589
|
+
|
|
590
|
+
// Read agent instructions
|
|
591
|
+
const agentPath = path.join(this.agentsPath, 'epic-story-decomposer.md');
|
|
592
|
+
this.debug(`Loading agent: ${agentPath}`);
|
|
593
|
+
const epicStoryDecomposerAgent = fs.readFileSync(agentPath, 'utf8');
|
|
594
|
+
this.debug(`Agent loaded (${epicStoryDecomposerAgent.length} bytes)`);
|
|
595
|
+
|
|
596
|
+
// Build prompt with duplicate detection
|
|
597
|
+
this.debug('Constructing decomposition prompt...');
|
|
598
|
+
const existingEpicNames = Array.from(existingEpics.keys());
|
|
599
|
+
const existingStoryNames = Array.from(existingStories.keys());
|
|
600
|
+
|
|
601
|
+
let prompt = `Given the following project scope:
|
|
602
|
+
|
|
603
|
+
**Initial Scope (Features to Implement):**
|
|
604
|
+
${scope}
|
|
605
|
+
`;
|
|
606
|
+
|
|
607
|
+
if (existingEpicNames.length > 0) {
|
|
608
|
+
prompt += `\n**Existing Epics (DO NOT DUPLICATE):**
|
|
609
|
+
${existingEpicNames.map(name => `- ${name}`).join('\n')}
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (existingStoryNames.length > 0) {
|
|
614
|
+
prompt += `\n**Existing Stories (DO NOT DUPLICATE):**
|
|
615
|
+
${existingStoryNames.map(name => `- ${name}`).join('\n')}
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
prompt += `\nDecompose this project into NEW Epics (3-7 domain-based groupings) and Stories (2-8 user-facing capabilities per Epic).
|
|
620
|
+
|
|
621
|
+
IMPORTANT: Only generate NEW Epics and Stories. Skip any that match the existing ones.
|
|
622
|
+
|
|
623
|
+
Return your response as JSON following the exact structure specified in your instructions.`;
|
|
624
|
+
|
|
625
|
+
this.debug('Prompt includes', {
|
|
626
|
+
scopeLength: scope.length,
|
|
627
|
+
existingEpics: existingEpicNames.length,
|
|
628
|
+
existingStories: existingStoryNames.length,
|
|
629
|
+
totalPromptSize: prompt.length
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const existingNote = existingEpicNames.length > 0
|
|
633
|
+
? ` (skipping ${existingEpicNames.length} existing epics)`
|
|
634
|
+
: '';
|
|
635
|
+
await progressCallback?.(null, `Calling LLM to decompose scope${existingNote}…`, {});
|
|
636
|
+
await progressCallback?.(null, null, { detail: `Sending to ${providerName} (${modelName})…` });
|
|
637
|
+
|
|
638
|
+
// Log full decomposition prompt for duplicate detection analysis
|
|
639
|
+
this.debug('\n' + '='.repeat(80));
|
|
640
|
+
this.debug('FULL DECOMPOSITION PROMPT:');
|
|
641
|
+
this.debug('='.repeat(80));
|
|
642
|
+
this.debug(prompt);
|
|
643
|
+
this.debug('='.repeat(80) + '\n');
|
|
644
|
+
|
|
645
|
+
// LLM call with full request/response logging
|
|
646
|
+
const hierarchy = await this.debugApiCall(
|
|
647
|
+
'Epic/Story Decomposition',
|
|
648
|
+
async () => {
|
|
649
|
+
this.debug('Request payload', {
|
|
650
|
+
model: modelName,
|
|
651
|
+
maxTokens: 8000,
|
|
652
|
+
agentInstructions: `${epicStoryDecomposerAgent.substring(0, 100)}...`,
|
|
653
|
+
promptPreview: `${prompt.substring(0, 200)}...`
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
this.debug('Sending request to LLM API...');
|
|
657
|
+
|
|
658
|
+
const result = await this._withProgressHeartbeat(
|
|
659
|
+
() => this.retryWithBackoff(
|
|
660
|
+
() => provider.generateJSON(prompt, epicStoryDecomposerAgent),
|
|
661
|
+
'Epic/Story decomposition'
|
|
662
|
+
),
|
|
663
|
+
(elapsed) => {
|
|
664
|
+
if (elapsed < 20) return 'Reading scope and project context…';
|
|
665
|
+
if (elapsed < 40) return 'Identifying domain boundaries…';
|
|
666
|
+
if (elapsed < 60) return 'Structuring epics and stories…';
|
|
667
|
+
if (elapsed < 80) return 'Refining decomposition…';
|
|
668
|
+
if (elapsed < 100) return 'Finalizing work item hierarchy…';
|
|
669
|
+
return 'Still decomposing…';
|
|
670
|
+
},
|
|
671
|
+
progressCallback,
|
|
672
|
+
20000 // 20s interval — phase messages change each tick
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
// Log token usage
|
|
676
|
+
const usage = provider.getTokenUsage();
|
|
677
|
+
this.debug('Response tokens', {
|
|
678
|
+
input: usage.inputTokens,
|
|
679
|
+
output: usage.outputTokens,
|
|
680
|
+
total: usage.totalTokens
|
|
681
|
+
});
|
|
682
|
+
await progressCallback?.(null, null, { detail: `${usage.inputTokens.toLocaleString()} in · ${usage.outputTokens.toLocaleString()} out tokens` });
|
|
683
|
+
|
|
684
|
+
this.debug(`Response content (${usage.outputTokens} tokens)`, {
|
|
685
|
+
epicCount: result.epics?.length || 0,
|
|
686
|
+
totalStories: result.epics?.reduce((sum, e) => sum + (e.stories?.length || 0), 0) || 0,
|
|
687
|
+
validation: result.validation
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// Log full LLM response for duplicate detection analysis
|
|
691
|
+
this.debug('\n' + '='.repeat(80));
|
|
692
|
+
this.debug('FULL LLM RESPONSE:');
|
|
693
|
+
this.debug('='.repeat(80));
|
|
694
|
+
this.debug(JSON.stringify(result, null, 2));
|
|
695
|
+
this.debug('='.repeat(80) + '\n');
|
|
696
|
+
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
if (!hierarchy.epics || !Array.isArray(hierarchy.epics)) {
|
|
702
|
+
this.debug('✗ Invalid decomposition response: missing epics array');
|
|
703
|
+
throw new Error('Invalid decomposition response: missing epics array');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const totalStories = hierarchy.epics.reduce((sum, e) => sum + (e.stories?.length || 0), 0);
|
|
707
|
+
await progressCallback?.(null, `Decomposed into ${hierarchy.epics.length} epics, ${totalStories} stories`, {});
|
|
708
|
+
for (const epic of hierarchy.epics) {
|
|
709
|
+
await progressCallback?.(null, ` ${epic.name} (${epic.stories?.length || 0} stories)`, {});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
this.debug('Parsed hierarchy', {
|
|
713
|
+
epics: hierarchy.epics.map(e => ({
|
|
714
|
+
id: e.id,
|
|
715
|
+
name: e.name,
|
|
716
|
+
storyCount: e.stories?.length || 0
|
|
717
|
+
})),
|
|
718
|
+
validation: hierarchy.validation
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
return hierarchy;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Filter the decomposed hierarchy to only the epics/stories chosen by the user.
|
|
727
|
+
* @param {Object} hierarchy - Full decomposed hierarchy
|
|
728
|
+
* @param {string[]} selectedEpicIds - Epic IDs to keep
|
|
729
|
+
* @param {string[]} selectedStoryIds - Story IDs to keep
|
|
730
|
+
* @returns {Object} Filtered hierarchy
|
|
731
|
+
*/
|
|
732
|
+
_filterHierarchyBySelection(hierarchy, selectedEpicIds, selectedStoryIds) {
|
|
733
|
+
const epicIdSet = new Set(selectedEpicIds);
|
|
734
|
+
const storyIdSet = new Set(selectedStoryIds);
|
|
735
|
+
const filteredEpics = hierarchy.epics
|
|
736
|
+
.filter(e => epicIdSet.has(e.id))
|
|
737
|
+
.map(e => ({
|
|
738
|
+
...e,
|
|
739
|
+
stories: (e.stories || []).filter(s => storyIdSet.has(s.id))
|
|
740
|
+
}));
|
|
741
|
+
return { ...hierarchy, epics: filteredEpics };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Phase 1 of contextual selection: extract structured project characteristics from scope text.
|
|
746
|
+
* Called once per sprint-planning run when useContextualSelection is enabled.
|
|
747
|
+
* @param {string} scope - Project scope text (first 3000 chars used)
|
|
748
|
+
* @param {Function} progressCallback - Optional progress callback
|
|
749
|
+
* @returns {Promise<Object>} ProjectContext JSON (empty object on failure)
|
|
750
|
+
*/
|
|
751
|
+
async extractProjectContext(scope, progressCallback) {
|
|
752
|
+
this.debug('Extracting project context for contextual agent selection');
|
|
753
|
+
try {
|
|
754
|
+
const provider = await this.getProviderForStageInstance('validation');
|
|
755
|
+
const agent = loadAgent('project-context-extractor.md');
|
|
756
|
+
const prompt = `PROJECT SCOPE:\n\n${scope.substring(0, 3000)}\n\nExtract the structured project context as JSON.`;
|
|
757
|
+
const result = await provider.generateJSON(prompt, agent);
|
|
758
|
+
return result || {};
|
|
759
|
+
} catch (err) {
|
|
760
|
+
this.debug('Project context extraction failed, continuing without context', { error: err.message });
|
|
761
|
+
return {};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// STAGE 5: Multi-Agent Validation
|
|
766
|
+
async validateHierarchy(hierarchy, progressCallback = null, scope = null) {
|
|
767
|
+
this.debugStage(5, 'Multi-Agent Validation');
|
|
768
|
+
|
|
769
|
+
// Initialize default LLM provider if not already done (for fallback)
|
|
770
|
+
if (!this.llmProvider) {
|
|
771
|
+
await this.initializeLLMProvider();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Check if smart selection is enabled
|
|
775
|
+
const useSmartSelection = this.stagesConfig?.validation?.useSmartSelection || false;
|
|
776
|
+
|
|
777
|
+
if (useSmartSelection) {
|
|
778
|
+
this.debug('Smart validator selection enabled');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Phase 1: Extract project context if contextual selection is enabled
|
|
782
|
+
const useContextualSelection = this.stagesConfig?.validation?.useContextualSelection || false;
|
|
783
|
+
this.debug(`Contextual agent selection: useContextualSelection=${useContextualSelection}, stagesConfig.validation=${JSON.stringify(this.stagesConfig?.validation)}`);
|
|
784
|
+
let projectContext = null;
|
|
785
|
+
|
|
786
|
+
if (useContextualSelection && scope) {
|
|
787
|
+
await progressCallback?.(null, 'Analyzing project context for agent selection…', {});
|
|
788
|
+
projectContext = await this.extractProjectContext(scope, progressCallback);
|
|
789
|
+
this._projectContext = projectContext;
|
|
790
|
+
this.debug('Project context extracted', projectContext);
|
|
791
|
+
} else if (useContextualSelection) {
|
|
792
|
+
this.debug('useContextualSelection=true but no scope available — skipping context extraction');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const validator = new EpicStoryValidator(
|
|
796
|
+
this.llmProvider,
|
|
797
|
+
this.verificationTracker,
|
|
798
|
+
this.stagesConfig,
|
|
799
|
+
useSmartSelection,
|
|
800
|
+
progressCallback,
|
|
801
|
+
projectContext
|
|
802
|
+
);
|
|
803
|
+
this._validator = validator;
|
|
804
|
+
this._validator.setTokenCallback((delta, stageHint) => {
|
|
805
|
+
const key = stageHint
|
|
806
|
+
? `${this.ceremonyName}-${stageHint}`
|
|
807
|
+
: this.ceremonyName;
|
|
808
|
+
this.tokenTracker.addIncremental(key, delta);
|
|
809
|
+
if (delta.model) {
|
|
810
|
+
const cost = this.tokenTracker.calculateCost(delta.input, delta.output, delta.model);
|
|
811
|
+
this._runningCost += cost?.total ?? 0;
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Validate each epic
|
|
816
|
+
for (const epic of hierarchy.epics) {
|
|
817
|
+
this.debug(`\nValidating Epic: ${epic.id} "${epic.name}"`);
|
|
818
|
+
await progressCallback?.(null, `Validating Epic: ${epic.name}`, {});
|
|
819
|
+
|
|
820
|
+
// Build epic context string for validation
|
|
821
|
+
const epicContext = `# Epic: ${epic.name}\n\n**Description:** ${epic.description}\n\n**Domain:** ${epic.domain}\n\n**Features:**\n${(epic.features || []).map(f => `- ${f}`).join('\n')}\n`;
|
|
822
|
+
|
|
823
|
+
// Validate epic with multiple domain validators
|
|
824
|
+
const _tsEpic = Date.now();
|
|
825
|
+
const epicValidation = await validator.validateEpic(epic, epicContext);
|
|
826
|
+
this.debugTiming(` validateEpic: ${epic.id} "${epic.name}"`, _tsEpic);
|
|
827
|
+
|
|
828
|
+
// Display validation summary
|
|
829
|
+
this.displayValidationSummary('Epic', epic.name, epicValidation);
|
|
830
|
+
|
|
831
|
+
// Handle validation result
|
|
832
|
+
if (epicValidation.overallStatus === 'needs-improvement') {
|
|
833
|
+
this.debug(`Epic "${epic.name}" needs improvement - showing issues`);
|
|
834
|
+
this.displayValidationIssues(epicValidation);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Validate each story under this epic
|
|
838
|
+
for (const story of epic.stories || []) {
|
|
839
|
+
this.debug(`\nValidating Story: ${story.id} "${story.name}"`);
|
|
840
|
+
await progressCallback?.(null, ` Validating story: ${story.name}`, {});
|
|
841
|
+
|
|
842
|
+
// Build story context string for validation
|
|
843
|
+
const storyContext = `# Story: ${story.name}\n\n**User Type:** ${story.userType}\n\n**Description:** ${story.description}\n\n**Acceptance Criteria:**\n${(story.acceptance || []).map((ac, i) => `${i + 1}. ${ac}`).join('\n')}\n\n**Parent Epic:** ${epic.name} (${epic.domain})\n`;
|
|
844
|
+
|
|
845
|
+
// Validate story with multiple domain validators
|
|
846
|
+
const _tsStory = Date.now();
|
|
847
|
+
const storyValidation = await validator.validateStory(story, storyContext, epic);
|
|
848
|
+
this.debugTiming(` validateStory: ${story.id} "${story.name}"`, _tsStory);
|
|
849
|
+
|
|
850
|
+
// Display validation summary
|
|
851
|
+
this.displayValidationSummary('Story', story.name, storyValidation);
|
|
852
|
+
|
|
853
|
+
// Handle validation result
|
|
854
|
+
if (storyValidation.overallStatus === 'needs-improvement') {
|
|
855
|
+
this.debug(`Story "${story.name}" needs improvement - showing issues`);
|
|
856
|
+
this.displayValidationIssues(storyValidation);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return hierarchy;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Display validation summary
|
|
866
|
+
*/
|
|
867
|
+
displayValidationSummary(type, name, validation) {
|
|
868
|
+
const statusPrefix = {
|
|
869
|
+
'excellent': 'SUCCESS:',
|
|
870
|
+
'acceptable': 'WARNING:',
|
|
871
|
+
'needs-improvement': 'ERROR:'
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const prefix = statusPrefix[validation.overallStatus] || '';
|
|
875
|
+
sendOutput(`${prefix} ${type}: ${name}\n`);
|
|
876
|
+
sendIndented(`Overall Score: ${validation.averageScore}/100`, 1);
|
|
877
|
+
sendIndented(`Validators: ${validation.validatorCount} agents`, 1);
|
|
878
|
+
sendIndented(`Issues: ${validation.criticalIssues.length} critical, ${validation.majorIssues.length} major, ${validation.minorIssues.length} minor`, 1);
|
|
879
|
+
|
|
880
|
+
// Show strengths if excellent or acceptable
|
|
881
|
+
if (validation.overallStatus !== 'needs-improvement' && validation.strengths.length > 0) {
|
|
882
|
+
sendIndented(`Strengths: ${validation.strengths.slice(0, 2).join(', ')}`, 1);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
sendOutput('\n');
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Display validation issues
|
|
890
|
+
*/
|
|
891
|
+
displayValidationIssues(validation) {
|
|
892
|
+
// Show critical issues
|
|
893
|
+
if (validation.criticalIssues.length > 0) {
|
|
894
|
+
this.debug('Critical Issues', validation.criticalIssues.slice(0, 3).map(issue => ({
|
|
895
|
+
domain: issue.domain,
|
|
896
|
+
description: issue.description,
|
|
897
|
+
suggestion: issue.suggestion
|
|
898
|
+
})));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Show improvement priorities
|
|
902
|
+
if (validation.improvementPriorities.length > 0) {
|
|
903
|
+
this.debug('Improvement Priorities', validation.improvementPriorities.slice(0, 3).map((priority, i) => ({
|
|
904
|
+
rank: i + 1,
|
|
905
|
+
priority: priority.priority,
|
|
906
|
+
mentionedBy: priority.mentionedBy
|
|
907
|
+
})));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Analyze duplicate detection decisions
|
|
913
|
+
* Logs which epics/stories should have been skipped by LLM vs which are truly new
|
|
914
|
+
*/
|
|
915
|
+
analyzeDuplicates(hierarchy, existingEpics, existingStories) {
|
|
916
|
+
this.debug('\n' + '='.repeat(80));
|
|
917
|
+
this.debug('DUPLICATE DETECTION ANALYSIS');
|
|
918
|
+
this.debug('='.repeat(80));
|
|
919
|
+
|
|
920
|
+
const skippedEpics = [];
|
|
921
|
+
const createdEpics = [];
|
|
922
|
+
|
|
923
|
+
// Analyze epics
|
|
924
|
+
for (const epic of hierarchy.epics || []) {
|
|
925
|
+
const normalized = epic.name.toLowerCase();
|
|
926
|
+
const isDuplicate = existingEpics.has(normalized);
|
|
927
|
+
|
|
928
|
+
this.debug(`\nEpic: "${epic.name}"`);
|
|
929
|
+
this.debug(` Normalized: "${normalized}"`);
|
|
930
|
+
this.debug(` Exists in previous runs: ${isDuplicate}`);
|
|
931
|
+
|
|
932
|
+
if (isDuplicate) {
|
|
933
|
+
const existingId = existingEpics.get(normalized);
|
|
934
|
+
this.debug(` ⚠️ Match found: ${existingId}`);
|
|
935
|
+
this.debug(` Action: SHOULD HAVE BEEN SKIPPED BY LLM`);
|
|
936
|
+
this.debug(` Reason: LLM generated duplicate that already exists`);
|
|
937
|
+
skippedEpics.push({ name: epic.name, existingId });
|
|
938
|
+
} else {
|
|
939
|
+
// Check for potential semantic duplicates (similar names)
|
|
940
|
+
const similarEpics = [];
|
|
941
|
+
for (const [existingName, existingId] of existingEpics.entries()) {
|
|
942
|
+
// Simple similarity: check if one name contains the other
|
|
943
|
+
if (normalized.includes(existingName) || existingName.includes(normalized)) {
|
|
944
|
+
similarEpics.push({ name: existingName, id: existingId });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (similarEpics.length > 0) {
|
|
949
|
+
this.debug(` ⚠️ Possible semantic duplicates found:`);
|
|
950
|
+
for (const similar of similarEpics) {
|
|
951
|
+
this.debug(` - "${similar.name}" (${similar.id})`);
|
|
952
|
+
}
|
|
953
|
+
this.debug(` Action: CREATE NEW (but user should review for duplicates)`);
|
|
954
|
+
} else {
|
|
955
|
+
this.debug(` ✓ Match found: NONE`);
|
|
956
|
+
this.debug(` Action: CREATE NEW`);
|
|
957
|
+
this.debug(` Reason: Genuinely new epic not in existing list`);
|
|
958
|
+
}
|
|
959
|
+
createdEpics.push(epic.name);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Analyze stories
|
|
964
|
+
const skippedStories = [];
|
|
965
|
+
const createdStories = [];
|
|
966
|
+
|
|
967
|
+
for (const epic of hierarchy.epics || []) {
|
|
968
|
+
for (const story of epic.stories || []) {
|
|
969
|
+
const normalized = story.name.toLowerCase();
|
|
970
|
+
const isDuplicate = existingStories.has(normalized);
|
|
971
|
+
|
|
972
|
+
this.debug(`\nStory: "${story.name}" (under epic "${epic.name}")`);
|
|
973
|
+
this.debug(` Normalized: "${normalized}"`);
|
|
974
|
+
this.debug(` Exists in previous runs: ${isDuplicate}`);
|
|
975
|
+
|
|
976
|
+
if (isDuplicate) {
|
|
977
|
+
const existingId = existingStories.get(normalized);
|
|
978
|
+
this.debug(` ⚠️ Match found: ${existingId}`);
|
|
979
|
+
this.debug(` Action: SHOULD HAVE BEEN SKIPPED BY LLM`);
|
|
980
|
+
skippedStories.push({ name: story.name, existingId });
|
|
981
|
+
} else {
|
|
982
|
+
this.debug(` ✓ Match found: NONE`);
|
|
983
|
+
this.debug(` Action: CREATE NEW`);
|
|
984
|
+
createdStories.push(story.name);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Summary
|
|
990
|
+
this.debug('\n' + '='.repeat(80));
|
|
991
|
+
this.debug('DUPLICATE ANALYSIS SUMMARY');
|
|
992
|
+
this.debug('='.repeat(80));
|
|
993
|
+
this.debug('Epics:', {
|
|
994
|
+
shouldBeSkipped: skippedEpics.length,
|
|
995
|
+
willCreate: createdEpics.length,
|
|
996
|
+
skippedNames: skippedEpics.map(s => s.name),
|
|
997
|
+
createdNames: createdEpics
|
|
998
|
+
});
|
|
999
|
+
this.debug('Stories:', {
|
|
1000
|
+
shouldBeSkipped: skippedStories.length,
|
|
1001
|
+
willCreate: createdStories.length,
|
|
1002
|
+
skippedNames: skippedStories.map(s => s.name),
|
|
1003
|
+
createdNames: createdStories
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
if (skippedEpics.length > 0 || skippedStories.length > 0) {
|
|
1007
|
+
this.debug('\n⚠️ WARNING: LLM generated duplicates that should have been skipped!');
|
|
1008
|
+
this.debug('This indicates LLM non-determinism or insufficient duplicate detection.');
|
|
1009
|
+
} else {
|
|
1010
|
+
this.debug('\n✓ Result: LLM correctly identified all items as duplicates or genuinely new');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
this.debug('='.repeat(80) + '\n');
|
|
1014
|
+
|
|
1015
|
+
return { skippedEpics, createdEpics, skippedStories, createdStories };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// STAGE 6: Renumber IDs to avoid collisions
|
|
1019
|
+
renumberHierarchy(hierarchy, maxEpicNum, maxStoryNums) {
|
|
1020
|
+
this.debugStage(6, 'Renumber IDs');
|
|
1021
|
+
this.debug('Renumbering hierarchy to avoid ID collisions...');
|
|
1022
|
+
|
|
1023
|
+
let nextEpicNum = maxEpicNum.value + 1;
|
|
1024
|
+
this.debug(`Next epic number: ${nextEpicNum} (after existing ${maxEpicNum.value})`);
|
|
1025
|
+
|
|
1026
|
+
for (const epic of hierarchy.epics) {
|
|
1027
|
+
const oldEpicId = epic.id;
|
|
1028
|
+
const newEpicId = `context-${String(nextEpicNum).padStart(4, '0')}`;
|
|
1029
|
+
epic.id = newEpicId;
|
|
1030
|
+
|
|
1031
|
+
this.debug(`ID mapping - Epic "${epic.name}": ${oldEpicId} -> ${newEpicId}`);
|
|
1032
|
+
|
|
1033
|
+
let nextStoryNum = (maxStoryNums.get(newEpicId) || 0) + 1;
|
|
1034
|
+
|
|
1035
|
+
for (const story of epic.stories || []) {
|
|
1036
|
+
const oldStoryId = story.id;
|
|
1037
|
+
const newStoryId = `${newEpicId}-${String(nextStoryNum).padStart(4, '0')}`;
|
|
1038
|
+
story.id = newStoryId;
|
|
1039
|
+
|
|
1040
|
+
this.debug(`ID mapping - Story "${story.name}": ${oldStoryId} -> ${newStoryId}`);
|
|
1041
|
+
nextStoryNum++;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
nextEpicNum++;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
this.debug('Renumbered hierarchy', {
|
|
1048
|
+
epics: hierarchy.epics.map(e => ({ id: e.id, name: e.name, storyCount: e.stories?.length || 0 }))
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
return hierarchy;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// STAGE 7: Write hierarchy files with distributed documentation
|
|
1055
|
+
async writeHierarchyFiles(hierarchy, progressCallback = null) {
|
|
1056
|
+
this.debugStage(7, 'Write Hierarchy Files + Distribute Documentation');
|
|
1057
|
+
this.debug('Writing hierarchy files with documentation distribution');
|
|
1058
|
+
|
|
1059
|
+
// Read the root project doc.md (used as source for all epic distributions)
|
|
1060
|
+
let projectDocContent = '';
|
|
1061
|
+
if (fs.existsSync(this.projectDocPath)) {
|
|
1062
|
+
projectDocContent = fs.readFileSync(this.projectDocPath, 'utf8');
|
|
1063
|
+
this.debug(`Read project doc.md (${projectDocContent.length} bytes) for distribution`);
|
|
1064
|
+
} else {
|
|
1065
|
+
this.debug('project/doc.md not found — skipping documentation distribution');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const doDistribute = projectDocContent.length > 0;
|
|
1069
|
+
|
|
1070
|
+
// Phase 1 (sync): Create all directories and write all work.json files
|
|
1071
|
+
for (const epic of hierarchy.epics) {
|
|
1072
|
+
const epicDir = path.join(this.projectPath, epic.id);
|
|
1073
|
+
if (!fs.existsSync(epicDir)) fs.mkdirSync(epicDir, { recursive: true });
|
|
1074
|
+
|
|
1075
|
+
const epicWorkJson = {
|
|
1076
|
+
id: epic.id,
|
|
1077
|
+
name: epic.name,
|
|
1078
|
+
type: 'epic',
|
|
1079
|
+
domain: epic.domain,
|
|
1080
|
+
description: epic.description,
|
|
1081
|
+
features: epic.features,
|
|
1082
|
+
status: 'planned',
|
|
1083
|
+
dependencies: epic.dependencies || [],
|
|
1084
|
+
children: (epic.stories || []).map(s => s.id),
|
|
1085
|
+
metadata: {
|
|
1086
|
+
...(epic.metadata || {}),
|
|
1087
|
+
created: localISO(),
|
|
1088
|
+
ceremony: this.ceremonyName
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
const workJsonPath = path.join(epicDir, 'work.json');
|
|
1092
|
+
const workJsonContent = JSON.stringify(epicWorkJson, null, 2);
|
|
1093
|
+
fs.writeFileSync(workJsonPath, workJsonContent, 'utf8');
|
|
1094
|
+
this.debug(`Writing ${workJsonPath} (${workJsonContent.length} bytes)`);
|
|
1095
|
+
|
|
1096
|
+
for (const story of epic.stories || []) {
|
|
1097
|
+
const storyDir = path.join(epicDir, story.id);
|
|
1098
|
+
if (!fs.existsSync(storyDir)) fs.mkdirSync(storyDir, { recursive: true });
|
|
1099
|
+
|
|
1100
|
+
const storyWorkJson = {
|
|
1101
|
+
id: story.id,
|
|
1102
|
+
name: story.name,
|
|
1103
|
+
type: 'story',
|
|
1104
|
+
userType: story.userType,
|
|
1105
|
+
description: story.description,
|
|
1106
|
+
acceptance: story.acceptance,
|
|
1107
|
+
status: 'planned',
|
|
1108
|
+
dependencies: story.dependencies || [],
|
|
1109
|
+
children: [],
|
|
1110
|
+
metadata: {
|
|
1111
|
+
...(story.metadata || {}),
|
|
1112
|
+
created: localISO(),
|
|
1113
|
+
ceremony: this.ceremonyName
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
const storyWorkJsonPath = path.join(storyDir, 'work.json');
|
|
1117
|
+
const storyWorkJsonContent = JSON.stringify(storyWorkJson, null, 2);
|
|
1118
|
+
fs.writeFileSync(storyWorkJsonPath, storyWorkJsonContent, 'utf8');
|
|
1119
|
+
this.debug(`Writing ${storyWorkJsonPath} (${storyWorkJsonContent.length} bytes)`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Phase 2 (parallel): Distribute docs — all epics concurrently, stories within each epic concurrently
|
|
1124
|
+
await Promise.all(
|
|
1125
|
+
hierarchy.epics.map(async (epic) => {
|
|
1126
|
+
const epicDir = path.join(this.projectPath, epic.id);
|
|
1127
|
+
|
|
1128
|
+
// Distribute epic doc from project doc
|
|
1129
|
+
let epicDocContent;
|
|
1130
|
+
if (doDistribute) {
|
|
1131
|
+
await progressCallback?.(null, `Distributing documentation → ${epic.name}`, {});
|
|
1132
|
+
this.debug(`Distributing docs: project/doc.md → ${epic.id}/doc.md`);
|
|
1133
|
+
const result = await this.distributeDocContent(projectDocContent, epic, 'epic', progressCallback);
|
|
1134
|
+
epicDocContent = result.childDoc;
|
|
1135
|
+
this.debug(`Epic doc ${epic.id}: ${epicDocContent.length} bytes`);
|
|
1136
|
+
} else {
|
|
1137
|
+
await progressCallback?.(null, `Writing Epic: ${epic.name}`, {});
|
|
1138
|
+
epicDocContent = `# ${epic.name}\n\n${epic.description || ''}\n`;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Distribute all story docs from epic doc — in parallel within this epic
|
|
1142
|
+
const storyDocs = await Promise.all(
|
|
1143
|
+
(epic.stories || []).map(async (story) => {
|
|
1144
|
+
if (!doDistribute) {
|
|
1145
|
+
return `# ${story.name}\n\n${story.description || ''}\n`;
|
|
1146
|
+
}
|
|
1147
|
+
await progressCallback?.(null, ` Distributing documentation → ${story.name}`, {});
|
|
1148
|
+
this.debug(`Distributing docs: ${epic.id}/doc.md → ${story.id}/doc.md`);
|
|
1149
|
+
const result = await this.distributeDocContent(epicDocContent, story, 'story', progressCallback);
|
|
1150
|
+
this.debug(`Story doc ${story.id}: ${result.childDoc.length} bytes`);
|
|
1151
|
+
return result.childDoc;
|
|
1152
|
+
})
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
// Write all doc.md files for this epic
|
|
1156
|
+
const epicDocPath = path.join(epicDir, 'doc.md');
|
|
1157
|
+
fs.writeFileSync(epicDocPath, epicDocContent, 'utf8');
|
|
1158
|
+
this.debug(`Writing ${epicDocPath} (${epicDocContent.length} bytes)`);
|
|
1159
|
+
|
|
1160
|
+
(epic.stories || []).forEach((story, si) => {
|
|
1161
|
+
const storyDocPath = path.join(epicDir, story.id, 'doc.md');
|
|
1162
|
+
fs.writeFileSync(storyDocPath, storyDocs[si], 'utf8');
|
|
1163
|
+
this.debug(`Writing ${storyDocPath} (${storyDocs[si].length} bytes)`);
|
|
1164
|
+
});
|
|
1165
|
+
})
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
const epicCount = hierarchy.epics.length;
|
|
1169
|
+
const storyCount = hierarchy.epics.reduce((sum, epic) => sum + (epic.stories || []).length, 0);
|
|
1170
|
+
|
|
1171
|
+
// Log all files written this run for cross-run comparison
|
|
1172
|
+
this.debugSection('FILES WRITTEN THIS RUN');
|
|
1173
|
+
const filesWritten = [];
|
|
1174
|
+
for (const epic of hierarchy.epics) {
|
|
1175
|
+
filesWritten.push(`${epic.id}/work.json`);
|
|
1176
|
+
filesWritten.push(`${epic.id}/doc.md`);
|
|
1177
|
+
for (const story of epic.stories || []) {
|
|
1178
|
+
filesWritten.push(`${epic.id}/${story.id}/work.json`);
|
|
1179
|
+
filesWritten.push(`${epic.id}/${story.id}/doc.md`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
this.debug('Files written this run', filesWritten);
|
|
1183
|
+
this.debug(`Total files written: ${filesWritten.length} (${epicCount} epics x 2 + ${storyCount} stories x 2)`);
|
|
1184
|
+
|
|
1185
|
+
// Display clean summary of created epics and stories
|
|
1186
|
+
if (hierarchy.epics.length > 0) {
|
|
1187
|
+
for (const epic of hierarchy.epics) {
|
|
1188
|
+
sendOutput(`${epic.id}: ${epic.name}\n`);
|
|
1189
|
+
for (const story of epic.stories || []) {
|
|
1190
|
+
sendIndented(`${story.id}: ${story.name}`, 1);
|
|
1191
|
+
}
|
|
1192
|
+
sendOutput('\n');
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return { epicCount, storyCount };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Stage 7: Enrich story doc.md files with implementation-specific detail.
|
|
1201
|
+
*
|
|
1202
|
+
* After doc distribution, story docs may still have vague acceptance criteria
|
|
1203
|
+
* that lack concrete API contracts, error tables, DB field names, and business rules.
|
|
1204
|
+
* This stage runs the story-doc-enricher agent on each story doc to fill those gaps.
|
|
1205
|
+
*
|
|
1206
|
+
* @param {Object} hierarchy - Hierarchy with epics and stories (post-renumbering, with real IDs)
|
|
1207
|
+
* @param {Function} progressCallback - Optional progress callback
|
|
1208
|
+
*/
|
|
1209
|
+
async enrichStoryDocs(hierarchy, progressCallback = null) {
|
|
1210
|
+
this.debugStage(8, 'Enrich Story Docs with Implementation Detail');
|
|
1211
|
+
|
|
1212
|
+
const agentInstructions = loadAgent('story-doc-enricher.md');
|
|
1213
|
+
const provider = await this.getProviderForStageInstance('enrichment');
|
|
1214
|
+
const { model: modelName } = this.getProviderForStage('enrichment');
|
|
1215
|
+
|
|
1216
|
+
this.debug(`Using model for enrichment: ${modelName}`);
|
|
1217
|
+
|
|
1218
|
+
// Collect all story tasks and run all enrichments in parallel
|
|
1219
|
+
const tasks = hierarchy.epics.flatMap(epic =>
|
|
1220
|
+
(epic.stories || []).map(story => ({ epic, story }))
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
const results = await Promise.all(tasks.map(async ({ epic, story }) => {
|
|
1224
|
+
const storyDir = path.join(this.projectPath, epic.id, story.id);
|
|
1225
|
+
const storyDocPath = path.join(storyDir, 'doc.md');
|
|
1226
|
+
|
|
1227
|
+
if (!fs.existsSync(storyDocPath)) {
|
|
1228
|
+
this.debug(`Skipping enrichment for ${story.id} — doc.md not found`);
|
|
1229
|
+
return 'skipped';
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const storyDocContent = fs.readFileSync(storyDocPath, 'utf8');
|
|
1233
|
+
const acceptance = (story.acceptance || []).map((a, i) => `${i + 1}. ${a}`).join('\n') || 'none specified';
|
|
1234
|
+
const prompt = `## Existing Story Doc
|
|
1235
|
+
|
|
1236
|
+
${storyDocContent}
|
|
1237
|
+
|
|
1238
|
+
---
|
|
1239
|
+
|
|
1240
|
+
## Story Work Item
|
|
1241
|
+
|
|
1242
|
+
**Name:** ${story.name}
|
|
1243
|
+
**User Type:** ${story.userType || 'team member'}
|
|
1244
|
+
**Description:** ${story.description || ''}
|
|
1245
|
+
**Acceptance Criteria:**
|
|
1246
|
+
${acceptance}
|
|
1247
|
+
|
|
1248
|
+
---
|
|
1249
|
+
|
|
1250
|
+
## Parent Epic Context
|
|
1251
|
+
|
|
1252
|
+
**Epic:** ${epic.name}
|
|
1253
|
+
**Domain:** ${epic.domain || 'general'}
|
|
1254
|
+
**Description:** ${epic.description || ''}
|
|
1255
|
+
|
|
1256
|
+
---
|
|
1257
|
+
|
|
1258
|
+
Enrich the existing story doc to be fully implementation-ready. Fill any gaps in API contracts, error tables, data model fields, business rules, and authorization. Return JSON with \`enriched_doc\` and \`gaps_filled\` fields.`;
|
|
1259
|
+
|
|
1260
|
+
this.debug(`Enriching story doc: ${story.id} (${story.name})`);
|
|
1261
|
+
await progressCallback?.(null, ` Enriching documentation → ${story.name}`, {});
|
|
1262
|
+
|
|
1263
|
+
const _tsEnrich = Date.now();
|
|
1264
|
+
try {
|
|
1265
|
+
const result = await this._withProgressHeartbeat(
|
|
1266
|
+
() => this.retryWithBackoff(
|
|
1267
|
+
() => provider.generateJSON(prompt, agentInstructions),
|
|
1268
|
+
`enrichment for story: ${story.name}`
|
|
1269
|
+
),
|
|
1270
|
+
(elapsed) => {
|
|
1271
|
+
if (elapsed < 15) return `Enriching ${story.name}…`;
|
|
1272
|
+
if (elapsed < 40) return `Adding implementation detail to ${story.name}…`;
|
|
1273
|
+
return `Still enriching…`;
|
|
1274
|
+
},
|
|
1275
|
+
progressCallback,
|
|
1276
|
+
10000
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
const enrichedDoc = (typeof result.enriched_doc === 'string' && result.enriched_doc.trim())
|
|
1280
|
+
? result.enriched_doc
|
|
1281
|
+
: storyDocContent;
|
|
1282
|
+
|
|
1283
|
+
const gapsFilled = Array.isArray(result.gaps_filled) ? result.gaps_filled : [];
|
|
1284
|
+
|
|
1285
|
+
fs.writeFileSync(storyDocPath, enrichedDoc, 'utf8');
|
|
1286
|
+
|
|
1287
|
+
this.debugTiming(` enrichStory: ${story.id} "${story.name}"`, _tsEnrich);
|
|
1288
|
+
if (gapsFilled.length > 0) {
|
|
1289
|
+
this.debug(`Story ${story.id} enriched: ${gapsFilled.length} gaps filled`, gapsFilled);
|
|
1290
|
+
} else {
|
|
1291
|
+
this.debug(`Story ${story.id}: already implementation-ready, no gaps filled`);
|
|
1292
|
+
}
|
|
1293
|
+
return 'enriched';
|
|
1294
|
+
} catch (err) {
|
|
1295
|
+
this.debugTiming(` enrichStory FAILED: ${story.id} "${story.name}"`, _tsEnrich);
|
|
1296
|
+
this.debug(`Story enrichment failed for ${story.id} — keeping original doc`, { error: err.message });
|
|
1297
|
+
return 'skipped';
|
|
1298
|
+
}
|
|
1299
|
+
}));
|
|
1300
|
+
|
|
1301
|
+
const enrichedCount = results.filter(r => r === 'enriched').length;
|
|
1302
|
+
const skippedCount = results.filter(r => r === 'skipped').length;
|
|
1303
|
+
this.debug(`Story enrichment complete: ${enrichedCount} enriched, ${skippedCount} skipped`);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Distribute documentation content from a parent doc.md to a child item's doc.md.
|
|
1308
|
+
*
|
|
1309
|
+
* Calls the doc-distributor LLM agent which extracts content specifically about
|
|
1310
|
+
* the child from the parent document, builds the child's doc.md with the extracted
|
|
1311
|
+
* content plus elaboration, and returns the parent document with that content removed.
|
|
1312
|
+
*
|
|
1313
|
+
* @param {string} parentDocContent - Current content of the parent doc.md
|
|
1314
|
+
* @param {Object} childItem - Epic or story object from decomposition result
|
|
1315
|
+
* @param {'epic'|'story'} childType - Whether the child is an epic or story
|
|
1316
|
+
* @param {Function} progressCallback - Optional progress callback
|
|
1317
|
+
* @returns {Promise<{childDoc: string, parentDoc: string}>}
|
|
1318
|
+
*/
|
|
1319
|
+
async distributeDocContent(parentDocContent, childItem, childType, progressCallback = null) {
|
|
1320
|
+
this.debugSection(`DOC DISTRIBUTION: ${childType.toUpperCase()} "${childItem.name}"`);
|
|
1321
|
+
|
|
1322
|
+
const agentPath = path.join(this.agentsPath, 'doc-distributor.md');
|
|
1323
|
+
this.debug(`Loading doc-distributor agent: ${agentPath}`);
|
|
1324
|
+
const agentInstructions = fs.readFileSync(agentPath, 'utf8');
|
|
1325
|
+
|
|
1326
|
+
// Build child item description for the prompt
|
|
1327
|
+
let itemDescription;
|
|
1328
|
+
if (childType === 'epic') {
|
|
1329
|
+
const features = (childItem.features || []).join(', ') || 'none specified';
|
|
1330
|
+
const stories = (childItem.stories || []).map(s => `- ${s.name}: ${s.description || ''}`).join('\n') || 'none yet';
|
|
1331
|
+
itemDescription = `Type: epic
|
|
1332
|
+
Name: ${childItem.name}
|
|
1333
|
+
Domain: ${childItem.domain || 'general'}
|
|
1334
|
+
Description: ${childItem.description || ''}
|
|
1335
|
+
Features: ${features}
|
|
1336
|
+
Stories that will belong to this epic:
|
|
1337
|
+
${stories}`;
|
|
1338
|
+
} else {
|
|
1339
|
+
const acceptance = (childItem.acceptance || []).map(a => `- ${a}`).join('\n') || 'none specified';
|
|
1340
|
+
itemDescription = `Type: story
|
|
1341
|
+
Name: ${childItem.name}
|
|
1342
|
+
User type: ${childItem.userType || 'team member'}
|
|
1343
|
+
Description: ${childItem.description || ''}
|
|
1344
|
+
Acceptance criteria:
|
|
1345
|
+
${acceptance}`;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const prompt = `## Parent Document
|
|
1349
|
+
|
|
1350
|
+
${parentDocContent}
|
|
1351
|
+
|
|
1352
|
+
---
|
|
1353
|
+
|
|
1354
|
+
## Child Item to Create Documentation For
|
|
1355
|
+
|
|
1356
|
+
${itemDescription}
|
|
1357
|
+
|
|
1358
|
+
---
|
|
1359
|
+
|
|
1360
|
+
Extract and synthesize content from the parent document that is specifically relevant to this ${childType}, then compose the child's \`doc.md\`. Return JSON with a \`child_doc\` field.`;
|
|
1361
|
+
|
|
1362
|
+
this.debug(`Prompt length: ${prompt.length} chars (parent: ${parentDocContent.length}, item: ${itemDescription.length})`);
|
|
1363
|
+
|
|
1364
|
+
const provider = await this.getProviderForStageInstance('doc-distribution');
|
|
1365
|
+
|
|
1366
|
+
const result = await this._withProgressHeartbeat(
|
|
1367
|
+
() => this.retryWithBackoff(
|
|
1368
|
+
() => provider.generateJSON(prompt, agentInstructions),
|
|
1369
|
+
`doc distribution for ${childType}: ${childItem.name}`
|
|
1370
|
+
),
|
|
1371
|
+
(elapsed) => {
|
|
1372
|
+
if (elapsed < 15) return `Extracting ${childType}-specific content…`;
|
|
1373
|
+
if (elapsed < 40) return `Building ${childItem.name} documentation…`;
|
|
1374
|
+
if (elapsed < 65) return `Refining parent document…`;
|
|
1375
|
+
return `Still distributing…`;
|
|
1376
|
+
},
|
|
1377
|
+
progressCallback,
|
|
1378
|
+
10000
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
const usage = provider.getTokenUsage();
|
|
1382
|
+
this.debug(`Doc distribution tokens: ${usage.inputTokens} in · ${usage.outputTokens} out`);
|
|
1383
|
+
|
|
1384
|
+
// Validate response shape and fall back gracefully on malformed output
|
|
1385
|
+
const childDoc = (typeof result.child_doc === 'string' && result.child_doc.trim())
|
|
1386
|
+
? result.child_doc
|
|
1387
|
+
: `# ${childItem.name}\n\n${childItem.description || ''}\n`;
|
|
1388
|
+
|
|
1389
|
+
this.debug(`Distribution result: child_doc ${childDoc.length} bytes`);
|
|
1390
|
+
|
|
1391
|
+
return { childDoc };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Count total hierarchy (nested structure)
|
|
1395
|
+
countTotalHierarchy() {
|
|
1396
|
+
let totalEpics = 0;
|
|
1397
|
+
let totalStories = 0;
|
|
1398
|
+
|
|
1399
|
+
if (!fs.existsSync(this.projectPath)) {
|
|
1400
|
+
return { totalEpics, totalStories };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const dirs = fs.readdirSync(this.projectPath);
|
|
1404
|
+
|
|
1405
|
+
// Scan top-level directories (epics)
|
|
1406
|
+
for (const dir of dirs) {
|
|
1407
|
+
const epicWorkJsonPath = path.join(this.projectPath, dir, 'work.json');
|
|
1408
|
+
|
|
1409
|
+
if (!fs.existsSync(epicWorkJsonPath)) continue;
|
|
1410
|
+
|
|
1411
|
+
try {
|
|
1412
|
+
const work = JSON.parse(fs.readFileSync(epicWorkJsonPath, 'utf8'));
|
|
1413
|
+
|
|
1414
|
+
if (work.type === 'epic') {
|
|
1415
|
+
totalEpics++;
|
|
1416
|
+
|
|
1417
|
+
// Count nested stories under this epic
|
|
1418
|
+
const epicDir = path.join(this.projectPath, dir);
|
|
1419
|
+
const epicSubdirs = fs.readdirSync(epicDir).filter(subdir => {
|
|
1420
|
+
const subdirPath = path.join(epicDir, subdir);
|
|
1421
|
+
return fs.statSync(subdirPath).isDirectory();
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
for (const storyDir of epicSubdirs) {
|
|
1425
|
+
const storyWorkJsonPath = path.join(epicDir, storyDir, 'work.json');
|
|
1426
|
+
|
|
1427
|
+
if (!fs.existsSync(storyWorkJsonPath)) continue;
|
|
1428
|
+
|
|
1429
|
+
try {
|
|
1430
|
+
const storyWork = JSON.parse(fs.readFileSync(storyWorkJsonPath, 'utf8'));
|
|
1431
|
+
if (storyWork.type === 'story') {
|
|
1432
|
+
totalStories++;
|
|
1433
|
+
}
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
// Ignore parse errors
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
// Ignore parse errors
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
return { totalEpics, totalStories };
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Read the full on-disk hierarchy after writing files.
|
|
1449
|
+
* Returns the same shape as preRunSnapshot for direct comparison.
|
|
1450
|
+
*/
|
|
1451
|
+
readPostRunSnapshot() {
|
|
1452
|
+
if (!fs.existsSync(this.projectPath)) return [];
|
|
1453
|
+
|
|
1454
|
+
const snapshot = [];
|
|
1455
|
+
const dirs = fs.readdirSync(this.projectPath).sort();
|
|
1456
|
+
|
|
1457
|
+
for (const dir of dirs) {
|
|
1458
|
+
const epicWorkJsonPath = path.join(this.projectPath, dir, 'work.json');
|
|
1459
|
+
if (!fs.existsSync(epicWorkJsonPath)) continue;
|
|
1460
|
+
|
|
1461
|
+
try {
|
|
1462
|
+
const work = JSON.parse(fs.readFileSync(epicWorkJsonPath, 'utf8'));
|
|
1463
|
+
if (work.type !== 'epic') continue;
|
|
1464
|
+
|
|
1465
|
+
const epicEntry = {
|
|
1466
|
+
id: work.id,
|
|
1467
|
+
name: work.name,
|
|
1468
|
+
domain: work.domain || '',
|
|
1469
|
+
status: work.status || 'unknown',
|
|
1470
|
+
created: work.metadata?.created || null,
|
|
1471
|
+
ceremony: work.metadata?.ceremony || null,
|
|
1472
|
+
stories: []
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
const epicDir = path.join(this.projectPath, dir);
|
|
1476
|
+
const epicSubdirs = fs.readdirSync(epicDir).filter(subdir =>
|
|
1477
|
+
fs.statSync(path.join(epicDir, subdir)).isDirectory()
|
|
1478
|
+
).sort();
|
|
1479
|
+
|
|
1480
|
+
for (const storyDir of epicSubdirs) {
|
|
1481
|
+
const storyWorkJsonPath = path.join(epicDir, storyDir, 'work.json');
|
|
1482
|
+
if (!fs.existsSync(storyWorkJsonPath)) continue;
|
|
1483
|
+
try {
|
|
1484
|
+
const storyWork = JSON.parse(fs.readFileSync(storyWorkJsonPath, 'utf8'));
|
|
1485
|
+
if (storyWork.type === 'story') {
|
|
1486
|
+
epicEntry.stories.push({
|
|
1487
|
+
id: storyWork.id,
|
|
1488
|
+
name: storyWork.name,
|
|
1489
|
+
status: storyWork.status || 'unknown',
|
|
1490
|
+
created: storyWork.metadata?.created || null
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
} catch (e) { /* ignore */ }
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
snapshot.push(epicEntry);
|
|
1497
|
+
} catch (e) { /* ignore */ }
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
return snapshot;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Main execution method
|
|
1504
|
+
async execute(progressCallback = null) {
|
|
1505
|
+
// Cost threshold protection — wrap callback to check running cost before each progress call
|
|
1506
|
+
if (this._costThreshold != null && progressCallback) {
|
|
1507
|
+
const _origCallback = progressCallback;
|
|
1508
|
+
progressCallback = async (...args) => {
|
|
1509
|
+
if (this._costThreshold != null && this._runningCost >= this._costThreshold) {
|
|
1510
|
+
if (this._costLimitReachedCallback) {
|
|
1511
|
+
this._costThreshold = null; // disable re-triggering
|
|
1512
|
+
await this._costLimitReachedCallback(this._runningCost);
|
|
1513
|
+
// returns → ceremony continues with limit disabled
|
|
1514
|
+
} else {
|
|
1515
|
+
throw new Error(`COST_LIMIT_EXCEEDED:${this._runningCost.toFixed(6)}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return _origCallback(...args);
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Initialize ceremony history
|
|
1523
|
+
const { CeremonyHistory } = await import('./ceremony-history.js');
|
|
1524
|
+
const history = new CeremonyHistory(this.avcPath);
|
|
1525
|
+
history.init();
|
|
1526
|
+
|
|
1527
|
+
// Start execution tracking
|
|
1528
|
+
const executionId = history.startExecution('sprint-planning', 'decomposition');
|
|
1529
|
+
|
|
1530
|
+
try {
|
|
1531
|
+
// Log ceremony execution metadata
|
|
1532
|
+
const runId = Date.now();
|
|
1533
|
+
const runTimestamp = localISO();
|
|
1534
|
+
this.debug('='.repeat(80));
|
|
1535
|
+
this.debug('SPRINT PLANNING CEREMONY - EXECUTION START');
|
|
1536
|
+
this.debug('='.repeat(80));
|
|
1537
|
+
this.debug('Run ID (ms epoch):', runId);
|
|
1538
|
+
this.debug('Timestamp:', runTimestamp);
|
|
1539
|
+
this.debug('Execution ID:', executionId);
|
|
1540
|
+
this.debug('Config', {
|
|
1541
|
+
provider: this._providerName,
|
|
1542
|
+
model: this._modelName,
|
|
1543
|
+
stagesConfig: this.stagesConfig ? JSON.stringify(this.stagesConfig) : 'using defaults',
|
|
1544
|
+
projectPath: this.projectPath,
|
|
1545
|
+
cwd: process.cwd(),
|
|
1546
|
+
nodeVersion: process.version
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
const header = getCeremonyHeader('sprint-planning');
|
|
1550
|
+
sendCeremonyHeader(header.title, header.url);
|
|
1551
|
+
|
|
1552
|
+
const _t0run = Date.now();
|
|
1553
|
+
|
|
1554
|
+
// Stage 1: Validate
|
|
1555
|
+
sendProgress('Validating prerequisites...');
|
|
1556
|
+
await progressCallback?.('Stage 1/6: Validating prerequisites…');
|
|
1557
|
+
let _ts = Date.now();
|
|
1558
|
+
this.validatePrerequisites();
|
|
1559
|
+
this.debugTiming('Stage 1 — validatePrerequisites', _ts);
|
|
1560
|
+
|
|
1561
|
+
// Stage 2: Read existing hierarchy
|
|
1562
|
+
sendProgress('Analyzing existing project structure...');
|
|
1563
|
+
await progressCallback?.('Stage 2/6: Analyzing existing project structure…');
|
|
1564
|
+
_ts = Date.now();
|
|
1565
|
+
const { existingEpics, existingStories, maxEpicNum, maxStoryNums, preRunSnapshot } = this.readExistingHierarchy();
|
|
1566
|
+
this.debugTiming('Stage 2 — readExistingHierarchy', _ts);
|
|
1567
|
+
|
|
1568
|
+
if (existingEpics.size > 0) {
|
|
1569
|
+
this.debug(`Found ${existingEpics.size} existing Epics, ${existingStories.size} existing Stories`);
|
|
1570
|
+
sendInfo(`Found ${existingEpics.size} existing Epics, ${existingStories.size} existing Stories`);
|
|
1571
|
+
} else {
|
|
1572
|
+
this.debug('No existing Epics/Stories found (first expansion)');
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Stage 3: Collect scope
|
|
1576
|
+
sendProgress('Collecting project scope...');
|
|
1577
|
+
await progressCallback?.('Stage 3/6: Collecting project scope…');
|
|
1578
|
+
_ts = Date.now();
|
|
1579
|
+
const scope = await this.collectNewScope();
|
|
1580
|
+
this.debugTiming('Stage 3 — collectNewScope', _ts);
|
|
1581
|
+
|
|
1582
|
+
// Clear screen before decomposition phase
|
|
1583
|
+
process.stdout.write('\x1bc');
|
|
1584
|
+
outputBuffer.clear();
|
|
1585
|
+
|
|
1586
|
+
// Stage 4: Decompose
|
|
1587
|
+
sendProgress('Decomposing scope into Epics and Stories...');
|
|
1588
|
+
await progressCallback?.('Stage 4/6: Decomposing scope into Epics and Stories…');
|
|
1589
|
+
_ts = Date.now();
|
|
1590
|
+
let hierarchy = await this.decomposeIntoEpicsStories(scope, existingEpics, existingStories, progressCallback);
|
|
1591
|
+
this.debugTiming('Stage 4 — decomposeIntoEpicsStories', _ts);
|
|
1592
|
+
|
|
1593
|
+
// Log raw LLM output before any validation/modification
|
|
1594
|
+
this.debugSection('POST-DECOMPOSE: Raw LLM Output (before validation)');
|
|
1595
|
+
this.debugHierarchySnapshot('POST-DECOMPOSE', hierarchy.epics.map(e => ({
|
|
1596
|
+
id: e.id || '(no-id)',
|
|
1597
|
+
name: e.name,
|
|
1598
|
+
stories: (e.stories || []).map(s => ({ id: s.id || '(no-id)', name: s.name }))
|
|
1599
|
+
})));
|
|
1600
|
+
this.debug('LLM validation field', hierarchy.validation || null);
|
|
1601
|
+
|
|
1602
|
+
// Stage 4.5: User selection gate (Kanban UI only; null = run straight through)
|
|
1603
|
+
if (this._selectionCallback) {
|
|
1604
|
+
await progressCallback?.('Stage 4.5/6: Waiting for epic/story selection…');
|
|
1605
|
+
const selection = await this._selectionCallback(hierarchy);
|
|
1606
|
+
if (selection) {
|
|
1607
|
+
const { selectedEpicIds, selectedStoryIds } = selection;
|
|
1608
|
+
hierarchy = this._filterHierarchyBySelection(hierarchy, selectedEpicIds, selectedStoryIds);
|
|
1609
|
+
const epicCount = hierarchy.epics.length;
|
|
1610
|
+
const storyCount = hierarchy.epics.reduce((s, e) => s + (e.stories?.length || 0), 0);
|
|
1611
|
+
this.debug(`Selection applied: ${epicCount} epics, ${storyCount} stories selected`);
|
|
1612
|
+
await progressCallback?.(null, `Confirmed: ${epicCount} epics, ${storyCount} stories selected`, {});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Clear screen before validation phase
|
|
1617
|
+
process.stdout.write('\x1bc');
|
|
1618
|
+
outputBuffer.clear();
|
|
1619
|
+
|
|
1620
|
+
// Stage 5: Multi-Agent Validation
|
|
1621
|
+
const epicCount5 = hierarchy.epics.length;
|
|
1622
|
+
const storyCount5 = hierarchy.epics.reduce((s, e) => s + (e.stories?.length || 0), 0);
|
|
1623
|
+
sendProgress('Validating Epics and Stories with domain experts...');
|
|
1624
|
+
await progressCallback?.(`Stage 5/6: Validating with domain experts (${epicCount5} epics, ${storyCount5} stories)…`);
|
|
1625
|
+
_ts = Date.now();
|
|
1626
|
+
hierarchy = await this.validateHierarchy(hierarchy, progressCallback, scope);
|
|
1627
|
+
this.debugTiming(`Stage 5 — validateHierarchy (${epicCount5} epics, ${storyCount5} stories)`, _ts);
|
|
1628
|
+
|
|
1629
|
+
// Log hierarchy after validation (may have been modified)
|
|
1630
|
+
this.debugSection('POST-VALIDATION: Hierarchy after domain-expert validation');
|
|
1631
|
+
this.debugHierarchySnapshot('POST-VALIDATION', hierarchy.epics.map(e => ({
|
|
1632
|
+
id: e.id || '(no-id)',
|
|
1633
|
+
name: e.name,
|
|
1634
|
+
stories: (e.stories || []).map(s => ({ id: s.id || '(no-id)', name: s.name }))
|
|
1635
|
+
})));
|
|
1636
|
+
|
|
1637
|
+
// Analyze duplicate detection (before renumbering)
|
|
1638
|
+
const duplicateAnalysis = this.analyzeDuplicates(hierarchy, existingEpics, existingStories);
|
|
1639
|
+
|
|
1640
|
+
// Renumber IDs
|
|
1641
|
+
hierarchy = this.renumberHierarchy(hierarchy, maxEpicNum, maxStoryNums);
|
|
1642
|
+
|
|
1643
|
+
// Clear screen before file writing phase
|
|
1644
|
+
process.stdout.write('\x1bc');
|
|
1645
|
+
outputBuffer.clear();
|
|
1646
|
+
|
|
1647
|
+
// Stage 6: Write hierarchy files
|
|
1648
|
+
sendProgress('Writing files and distributing documentation...');
|
|
1649
|
+
await progressCallback?.(`Stage 6/7: Writing files and distributing documentation (${epicCount5} epics, ${storyCount5} stories)…`);
|
|
1650
|
+
_ts = Date.now();
|
|
1651
|
+
const { epicCount, storyCount } = await this.writeHierarchyFiles(hierarchy, progressCallback);
|
|
1652
|
+
this.debugTiming(`Stage 6 — writeHierarchyFiles (${epicCount5} epics, ${storyCount5} stories)`, _ts);
|
|
1653
|
+
|
|
1654
|
+
// Stage 7: Enrich story docs with implementation detail
|
|
1655
|
+
sendProgress('Enriching story documentation with implementation detail...');
|
|
1656
|
+
await progressCallback?.(`Stage 7/7: Enriching story documentation (${storyCount5} stories)…`);
|
|
1657
|
+
_ts = Date.now();
|
|
1658
|
+
await this.enrichStoryDocs(hierarchy, progressCallback);
|
|
1659
|
+
this.debugTiming(`Stage 7 — enrichStoryDocs (${storyCount5} stories)`, _ts);
|
|
1660
|
+
|
|
1661
|
+
// Stage 9: Summary & Cleanup
|
|
1662
|
+
this.debugStage(9, 'Summary & Cleanup');
|
|
1663
|
+
|
|
1664
|
+
const { totalEpics, totalStories } = this.countTotalHierarchy();
|
|
1665
|
+
|
|
1666
|
+
// Capture and log post-run hierarchy snapshot for comparison
|
|
1667
|
+
const postRunSnapshot = this.readPostRunSnapshot();
|
|
1668
|
+
this.debugHierarchySnapshot('POST-RUN', postRunSnapshot);
|
|
1669
|
+
|
|
1670
|
+
this.debugTiming('TOTAL run() end-to-end', _t0run);
|
|
1671
|
+
sendOutput(`Created ${epicCount} Epics, ${storyCount} Stories. Total: ${totalEpics} Epics, ${totalStories} Stories.`);
|
|
1672
|
+
|
|
1673
|
+
// Track token usage — aggregate across all provider instances
|
|
1674
|
+
const aggregated = this._aggregateAllTokenUsage();
|
|
1675
|
+
let tokenUsageSummary = null;
|
|
1676
|
+
if (aggregated.totalCalls > 0 || aggregated.inputTokens > 0) {
|
|
1677
|
+
tokenUsageSummary = aggregated;
|
|
1678
|
+
this.debug('Token usage (all providers)', tokenUsageSummary);
|
|
1679
|
+
|
|
1680
|
+
this.tokenTracker.finalizeRun(this.ceremonyName);
|
|
1681
|
+
this.debug('Token tracking finalized in .avc/token-history.json');
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
sendOutput('Run /seed <story-id> to decompose a Story into Tasks.');
|
|
1685
|
+
|
|
1686
|
+
// Log ceremony execution end with full comparison summary
|
|
1687
|
+
const runDuration = Date.now() - runId;
|
|
1688
|
+
this.debug('\n' + '='.repeat(80));
|
|
1689
|
+
this.debug('SPRINT PLANNING CEREMONY - EXECUTION END');
|
|
1690
|
+
this.debug('='.repeat(80));
|
|
1691
|
+
this.debug('Run ID:', runId);
|
|
1692
|
+
this.debug('Started:', runTimestamp);
|
|
1693
|
+
this.debug('Ended:', localISO());
|
|
1694
|
+
this.debug('Duration:', `${Math.round(runDuration / 1000)} seconds`);
|
|
1695
|
+
|
|
1696
|
+
this.debugSection('RUN COMPARISON SUMMARY (compare this block across runs)');
|
|
1697
|
+
this.debug('PRE-RUN state', {
|
|
1698
|
+
epics: existingEpics.size,
|
|
1699
|
+
stories: existingStories.size,
|
|
1700
|
+
epicNames: Array.from(existingEpics.keys())
|
|
1701
|
+
});
|
|
1702
|
+
this.debug('THIS RUN added', {
|
|
1703
|
+
epics: epicCount,
|
|
1704
|
+
stories: storyCount,
|
|
1705
|
+
epicNames: hierarchy.epics.map(e => e.name),
|
|
1706
|
+
storyNames: hierarchy.epics.flatMap(e => (e.stories || []).map(s => s.name))
|
|
1707
|
+
});
|
|
1708
|
+
this.debug('POST-RUN state', {
|
|
1709
|
+
epics: totalEpics,
|
|
1710
|
+
stories: totalStories
|
|
1711
|
+
});
|
|
1712
|
+
this.debug('Duplicate detection results', {
|
|
1713
|
+
epicsSkippedAsDuplicates: duplicateAnalysis.skippedEpics.length,
|
|
1714
|
+
storiesSkippedAsDuplicates: duplicateAnalysis.skippedStories.length,
|
|
1715
|
+
skippedEpicNames: duplicateAnalysis.skippedEpics.map(s => s.name),
|
|
1716
|
+
skippedStoryNames: duplicateAnalysis.skippedStories.map(s => s.name)
|
|
1717
|
+
});
|
|
1718
|
+
if (tokenUsageSummary) {
|
|
1719
|
+
this.debug('Token usage this run', tokenUsageSummary);
|
|
1720
|
+
}
|
|
1721
|
+
this.debug('='.repeat(80) + '\n');
|
|
1722
|
+
|
|
1723
|
+
// Build return result for kanban integration
|
|
1724
|
+
const returnResult = {
|
|
1725
|
+
epicsCreated: epicCount,
|
|
1726
|
+
storiesCreated: storyCount,
|
|
1727
|
+
totalEpics,
|
|
1728
|
+
totalStories,
|
|
1729
|
+
tokenUsage: {
|
|
1730
|
+
input: tokenUsageSummary?.inputTokens || 0,
|
|
1731
|
+
output: tokenUsageSummary?.outputTokens || 0,
|
|
1732
|
+
total: tokenUsageSummary?.totalTokens || 0,
|
|
1733
|
+
},
|
|
1734
|
+
model: this._modelName,
|
|
1735
|
+
provider: this._providerName,
|
|
1736
|
+
validationIssues: [],
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
// Complete ceremony history tracking
|
|
1740
|
+
const filesGenerated = [];
|
|
1741
|
+
for (const epic of hierarchy.epics) {
|
|
1742
|
+
filesGenerated.push(path.join(this.projectPath, epic.id, 'work.json'));
|
|
1743
|
+
filesGenerated.push(path.join(this.projectPath, epic.id, 'doc.md'));
|
|
1744
|
+
for (const story of epic.stories || []) {
|
|
1745
|
+
filesGenerated.push(path.join(this.projectPath, epic.id, story.id, 'work.json'));
|
|
1746
|
+
filesGenerated.push(path.join(this.projectPath, epic.id, story.id, 'doc.md'));
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
history.completeExecution('sprint-planning', executionId, 'success', {
|
|
1751
|
+
filesGenerated,
|
|
1752
|
+
tokenUsage: tokenUsageSummary ? {
|
|
1753
|
+
input: tokenUsageSummary.inputTokens,
|
|
1754
|
+
output: tokenUsageSummary.outputTokens,
|
|
1755
|
+
total: tokenUsageSummary.totalTokens
|
|
1756
|
+
} : null,
|
|
1757
|
+
model: this._modelName,
|
|
1758
|
+
provider: this._providerName,
|
|
1759
|
+
stage: 'completed',
|
|
1760
|
+
metrics: {
|
|
1761
|
+
epicsCreated: epicCount,
|
|
1762
|
+
storiesCreated: storyCount,
|
|
1763
|
+
totalEpics: totalEpics,
|
|
1764
|
+
totalStories: totalStories
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
return returnResult;
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
const isCancelled = error.message === 'CEREMONY_CANCELLED';
|
|
1771
|
+
|
|
1772
|
+
// Track tokens even for cancelled/error runs — tokens were spent up to this point
|
|
1773
|
+
try {
|
|
1774
|
+
const aggregated = this._aggregateAllTokenUsage();
|
|
1775
|
+
if (aggregated.totalCalls > 0 || aggregated.inputTokens > 0) {
|
|
1776
|
+
this.tokenTracker.finalizeRun(this.ceremonyName);
|
|
1777
|
+
this.debug('Token tracking finalized (partial run)', aggregated);
|
|
1778
|
+
}
|
|
1779
|
+
} catch (trackErr) {
|
|
1780
|
+
this.debug('Could not save token tracking on error', { error: trackErr.message });
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (!isCancelled) {
|
|
1784
|
+
this.debug('\n========== ERROR OCCURRED ==========');
|
|
1785
|
+
this.debug('Error details', {
|
|
1786
|
+
message: error.message,
|
|
1787
|
+
stack: error.stack,
|
|
1788
|
+
name: error.name
|
|
1789
|
+
});
|
|
1790
|
+
this.debug('Application state at failure', {
|
|
1791
|
+
ceremonyName: this.ceremonyName,
|
|
1792
|
+
provider: this._providerName,
|
|
1793
|
+
model: this._modelName,
|
|
1794
|
+
projectPath: this.projectPath,
|
|
1795
|
+
currentWorkingDir: process.cwd(),
|
|
1796
|
+
nodeVersion: process.version,
|
|
1797
|
+
platform: process.platform
|
|
1798
|
+
});
|
|
1799
|
+
sendError(`Project expansion failed: ${error.message}`);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// Mark execution as aborted on error
|
|
1803
|
+
history.completeExecution('sprint-planning', executionId, 'abrupt-termination', {
|
|
1804
|
+
stage: isCancelled ? 'cancelled' : 'error',
|
|
1805
|
+
error: error.message
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
throw error;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
export { SprintPlanningProcessor };
|