@agile-vibe-coding/avc 0.1.1 → 0.3.1

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