@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.
Files changed (290) hide show
  1. package/README.md +2 -0
  2. package/cli/agent-loader.js +21 -0
  3. package/cli/agents/agent-selector.md +129 -0
  4. package/cli/agents/architecture-recommender.md +418 -0
  5. package/cli/agents/database-deep-dive.md +470 -0
  6. package/cli/agents/database-recommender.md +634 -0
  7. package/cli/agents/doc-distributor.md +176 -0
  8. package/cli/agents/documentation-updater.md +203 -0
  9. package/cli/agents/epic-story-decomposer.md +280 -0
  10. package/cli/agents/feature-context-generator.md +91 -0
  11. package/cli/agents/gap-checker-epic.md +52 -0
  12. package/cli/agents/impact-checker-story.md +51 -0
  13. package/cli/agents/migration-guide-generator.md +305 -0
  14. package/cli/agents/mission-scope-generator.md +79 -0
  15. package/cli/agents/mission-scope-validator.md +112 -0
  16. package/cli/agents/project-context-extractor.md +107 -0
  17. package/cli/agents/project-documentation-creator.json +226 -0
  18. package/cli/agents/project-documentation-creator.md +595 -0
  19. package/cli/agents/question-prefiller.md +269 -0
  20. package/cli/agents/refiner-epic.md +39 -0
  21. package/cli/agents/refiner-story.md +42 -0
  22. package/cli/agents/solver-epic-api.json +15 -0
  23. package/cli/agents/solver-epic-api.md +39 -0
  24. package/cli/agents/solver-epic-backend.json +15 -0
  25. package/cli/agents/solver-epic-backend.md +39 -0
  26. package/cli/agents/solver-epic-cloud.json +15 -0
  27. package/cli/agents/solver-epic-cloud.md +39 -0
  28. package/cli/agents/solver-epic-data.json +15 -0
  29. package/cli/agents/solver-epic-data.md +39 -0
  30. package/cli/agents/solver-epic-database.json +15 -0
  31. package/cli/agents/solver-epic-database.md +39 -0
  32. package/cli/agents/solver-epic-developer.json +15 -0
  33. package/cli/agents/solver-epic-developer.md +39 -0
  34. package/cli/agents/solver-epic-devops.json +15 -0
  35. package/cli/agents/solver-epic-devops.md +39 -0
  36. package/cli/agents/solver-epic-frontend.json +15 -0
  37. package/cli/agents/solver-epic-frontend.md +39 -0
  38. package/cli/agents/solver-epic-mobile.json +15 -0
  39. package/cli/agents/solver-epic-mobile.md +39 -0
  40. package/cli/agents/solver-epic-qa.json +15 -0
  41. package/cli/agents/solver-epic-qa.md +39 -0
  42. package/cli/agents/solver-epic-security.json +15 -0
  43. package/cli/agents/solver-epic-security.md +39 -0
  44. package/cli/agents/solver-epic-solution-architect.json +15 -0
  45. package/cli/agents/solver-epic-solution-architect.md +39 -0
  46. package/cli/agents/solver-epic-test-architect.json +15 -0
  47. package/cli/agents/solver-epic-test-architect.md +39 -0
  48. package/cli/agents/solver-epic-ui.json +15 -0
  49. package/cli/agents/solver-epic-ui.md +39 -0
  50. package/cli/agents/solver-epic-ux.json +15 -0
  51. package/cli/agents/solver-epic-ux.md +39 -0
  52. package/cli/agents/solver-story-api.json +15 -0
  53. package/cli/agents/solver-story-api.md +39 -0
  54. package/cli/agents/solver-story-backend.json +15 -0
  55. package/cli/agents/solver-story-backend.md +39 -0
  56. package/cli/agents/solver-story-cloud.json +15 -0
  57. package/cli/agents/solver-story-cloud.md +39 -0
  58. package/cli/agents/solver-story-data.json +15 -0
  59. package/cli/agents/solver-story-data.md +39 -0
  60. package/cli/agents/solver-story-database.json +15 -0
  61. package/cli/agents/solver-story-database.md +39 -0
  62. package/cli/agents/solver-story-developer.json +15 -0
  63. package/cli/agents/solver-story-developer.md +39 -0
  64. package/cli/agents/solver-story-devops.json +15 -0
  65. package/cli/agents/solver-story-devops.md +39 -0
  66. package/cli/agents/solver-story-frontend.json +15 -0
  67. package/cli/agents/solver-story-frontend.md +39 -0
  68. package/cli/agents/solver-story-mobile.json +15 -0
  69. package/cli/agents/solver-story-mobile.md +39 -0
  70. package/cli/agents/solver-story-qa.json +15 -0
  71. package/cli/agents/solver-story-qa.md +39 -0
  72. package/cli/agents/solver-story-security.json +15 -0
  73. package/cli/agents/solver-story-security.md +39 -0
  74. package/cli/agents/solver-story-solution-architect.json +15 -0
  75. package/cli/agents/solver-story-solution-architect.md +39 -0
  76. package/cli/agents/solver-story-test-architect.json +15 -0
  77. package/cli/agents/solver-story-test-architect.md +39 -0
  78. package/cli/agents/solver-story-ui.json +15 -0
  79. package/cli/agents/solver-story-ui.md +39 -0
  80. package/cli/agents/solver-story-ux.json +15 -0
  81. package/cli/agents/solver-story-ux.md +39 -0
  82. package/cli/agents/story-doc-enricher.md +133 -0
  83. package/cli/agents/suggestion-business-analyst.md +88 -0
  84. package/cli/agents/suggestion-deployment-architect.md +263 -0
  85. package/cli/agents/suggestion-product-manager.md +129 -0
  86. package/cli/agents/suggestion-security-specialist.md +156 -0
  87. package/cli/agents/suggestion-technical-architect.md +269 -0
  88. package/cli/agents/suggestion-ux-researcher.md +93 -0
  89. package/cli/agents/task-subtask-decomposer.md +188 -0
  90. package/cli/agents/validator-documentation.json +152 -0
  91. package/cli/agents/validator-documentation.md +453 -0
  92. package/cli/agents/validator-epic-api.json +93 -0
  93. package/cli/agents/validator-epic-api.md +137 -0
  94. package/cli/agents/validator-epic-backend.json +93 -0
  95. package/cli/agents/validator-epic-backend.md +130 -0
  96. package/cli/agents/validator-epic-cloud.json +93 -0
  97. package/cli/agents/validator-epic-cloud.md +137 -0
  98. package/cli/agents/validator-epic-data.json +93 -0
  99. package/cli/agents/validator-epic-data.md +130 -0
  100. package/cli/agents/validator-epic-database.json +93 -0
  101. package/cli/agents/validator-epic-database.md +137 -0
  102. package/cli/agents/validator-epic-developer.json +74 -0
  103. package/cli/agents/validator-epic-developer.md +153 -0
  104. package/cli/agents/validator-epic-devops.json +74 -0
  105. package/cli/agents/validator-epic-devops.md +153 -0
  106. package/cli/agents/validator-epic-frontend.json +74 -0
  107. package/cli/agents/validator-epic-frontend.md +153 -0
  108. package/cli/agents/validator-epic-mobile.json +93 -0
  109. package/cli/agents/validator-epic-mobile.md +130 -0
  110. package/cli/agents/validator-epic-qa.json +93 -0
  111. package/cli/agents/validator-epic-qa.md +130 -0
  112. package/cli/agents/validator-epic-security.json +74 -0
  113. package/cli/agents/validator-epic-security.md +154 -0
  114. package/cli/agents/validator-epic-solution-architect.json +74 -0
  115. package/cli/agents/validator-epic-solution-architect.md +156 -0
  116. package/cli/agents/validator-epic-test-architect.json +93 -0
  117. package/cli/agents/validator-epic-test-architect.md +130 -0
  118. package/cli/agents/validator-epic-ui.json +93 -0
  119. package/cli/agents/validator-epic-ui.md +130 -0
  120. package/cli/agents/validator-epic-ux.json +93 -0
  121. package/cli/agents/validator-epic-ux.md +130 -0
  122. package/cli/agents/validator-selector.md +211 -0
  123. package/cli/agents/validator-story-api.json +104 -0
  124. package/cli/agents/validator-story-api.md +152 -0
  125. package/cli/agents/validator-story-backend.json +104 -0
  126. package/cli/agents/validator-story-backend.md +152 -0
  127. package/cli/agents/validator-story-cloud.json +104 -0
  128. package/cli/agents/validator-story-cloud.md +152 -0
  129. package/cli/agents/validator-story-data.json +104 -0
  130. package/cli/agents/validator-story-data.md +152 -0
  131. package/cli/agents/validator-story-database.json +104 -0
  132. package/cli/agents/validator-story-database.md +152 -0
  133. package/cli/agents/validator-story-developer.json +104 -0
  134. package/cli/agents/validator-story-developer.md +152 -0
  135. package/cli/agents/validator-story-devops.json +104 -0
  136. package/cli/agents/validator-story-devops.md +152 -0
  137. package/cli/agents/validator-story-frontend.json +104 -0
  138. package/cli/agents/validator-story-frontend.md +152 -0
  139. package/cli/agents/validator-story-mobile.json +104 -0
  140. package/cli/agents/validator-story-mobile.md +152 -0
  141. package/cli/agents/validator-story-qa.json +104 -0
  142. package/cli/agents/validator-story-qa.md +152 -0
  143. package/cli/agents/validator-story-security.json +104 -0
  144. package/cli/agents/validator-story-security.md +152 -0
  145. package/cli/agents/validator-story-solution-architect.json +104 -0
  146. package/cli/agents/validator-story-solution-architect.md +152 -0
  147. package/cli/agents/validator-story-test-architect.json +104 -0
  148. package/cli/agents/validator-story-test-architect.md +152 -0
  149. package/cli/agents/validator-story-ui.json +104 -0
  150. package/cli/agents/validator-story-ui.md +152 -0
  151. package/cli/agents/validator-story-ux.json +104 -0
  152. package/cli/agents/validator-story-ux.md +152 -0
  153. package/cli/ansi-colors.js +21 -0
  154. package/cli/build-docs.js +298 -0
  155. package/cli/ceremony-history.js +369 -0
  156. package/cli/command-logger.js +245 -0
  157. package/cli/components/static-output.js +63 -0
  158. package/cli/console-output-manager.js +94 -0
  159. package/cli/docs-sync.js +306 -0
  160. package/cli/epic-story-validator.js +1174 -0
  161. package/cli/evaluation-prompts.js +1008 -0
  162. package/cli/execution-context.js +195 -0
  163. package/cli/generate-summary-table.js +340 -0
  164. package/cli/index.js +3 -25
  165. package/cli/init-model-config.js +697 -0
  166. package/cli/init.js +1765 -100
  167. package/cli/kanban-server-manager.js +228 -0
  168. package/cli/llm-claude.js +109 -0
  169. package/cli/llm-gemini.js +115 -0
  170. package/cli/llm-mock.js +233 -0
  171. package/cli/llm-openai.js +233 -0
  172. package/cli/llm-provider.js +300 -0
  173. package/cli/llm-token-limits.js +102 -0
  174. package/cli/llm-verifier.js +454 -0
  175. package/cli/logger.js +32 -5
  176. package/cli/message-constants.js +58 -0
  177. package/cli/message-manager.js +334 -0
  178. package/cli/message-types.js +96 -0
  179. package/cli/messaging-api.js +297 -0
  180. package/cli/model-pricing.js +169 -0
  181. package/cli/model-query-engine.js +468 -0
  182. package/cli/model-recommendation-analyzer.js +495 -0
  183. package/cli/model-selector.js +269 -0
  184. package/cli/output-buffer.js +107 -0
  185. package/cli/process-manager.js +332 -0
  186. package/cli/repl-ink.js +5840 -504
  187. package/cli/repl-old.js +4 -4
  188. package/cli/seed-processor.js +792 -0
  189. package/cli/sprint-planning-processor.js +1813 -0
  190. package/cli/template-processor.js +2306 -108
  191. package/cli/templates/project.md +25 -8
  192. package/cli/templates/vitepress-config.mts.template +34 -0
  193. package/cli/token-tracker.js +520 -0
  194. package/cli/tools/generate-story-validators.js +317 -0
  195. package/cli/tools/generate-validators.js +669 -0
  196. package/cli/update-checker.js +19 -17
  197. package/cli/update-notifier.js +4 -4
  198. package/cli/validation-router.js +605 -0
  199. package/cli/verification-tracker.js +563 -0
  200. package/kanban/README.md +386 -0
  201. package/kanban/client/README.md +205 -0
  202. package/kanban/client/components.json +20 -0
  203. package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
  204. package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
  205. package/kanban/client/dist/index.html +16 -0
  206. package/kanban/client/dist/vite.svg +1 -0
  207. package/kanban/client/index.html +15 -0
  208. package/kanban/client/package-lock.json +9442 -0
  209. package/kanban/client/package.json +44 -0
  210. package/kanban/client/postcss.config.js +6 -0
  211. package/kanban/client/public/vite.svg +1 -0
  212. package/kanban/client/src/App.jsx +622 -0
  213. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  214. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
  215. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
  216. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
  217. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  218. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
  219. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
  220. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  221. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
  222. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  223. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  224. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  225. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
  226. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
  227. package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
  228. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  229. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  230. package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
  231. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  232. package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
  233. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  234. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
  235. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  236. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  237. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  238. package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
  239. package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
  240. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
  241. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
  242. package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
  243. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  244. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  245. package/kanban/client/src/components/stats/CostModal.jsx +353 -0
  246. package/kanban/client/src/components/ui/badge.jsx +27 -0
  247. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  248. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  249. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  250. package/kanban/client/src/hooks/useGrouping.js +118 -0
  251. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  252. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  253. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  254. package/kanban/client/src/lib/api.js +401 -0
  255. package/kanban/client/src/lib/status-grouping.js +144 -0
  256. package/kanban/client/src/lib/utils.js +11 -0
  257. package/kanban/client/src/main.jsx +10 -0
  258. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  259. package/kanban/client/src/store/ceremonyStore.js +172 -0
  260. package/kanban/client/src/store/filterStore.js +201 -0
  261. package/kanban/client/src/store/kanbanStore.js +115 -0
  262. package/kanban/client/src/store/processStore.js +65 -0
  263. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  264. package/kanban/client/src/styles/globals.css +59 -0
  265. package/kanban/client/tailwind.config.js +77 -0
  266. package/kanban/client/vite.config.js +28 -0
  267. package/kanban/client/vitest.config.js +28 -0
  268. package/kanban/dev-start.sh +47 -0
  269. package/kanban/package.json +12 -0
  270. package/kanban/server/index.js +516 -0
  271. package/kanban/server/routes/ceremony.js +305 -0
  272. package/kanban/server/routes/costs.js +157 -0
  273. package/kanban/server/routes/processes.js +50 -0
  274. package/kanban/server/routes/settings.js +303 -0
  275. package/kanban/server/routes/websocket.js +276 -0
  276. package/kanban/server/routes/work-items.js +347 -0
  277. package/kanban/server/services/CeremonyService.js +1190 -0
  278. package/kanban/server/services/FileSystemScanner.js +95 -0
  279. package/kanban/server/services/FileWatcher.js +144 -0
  280. package/kanban/server/services/HierarchyBuilder.js +196 -0
  281. package/kanban/server/services/ProcessRegistry.js +122 -0
  282. package/kanban/server/services/WorkItemReader.js +123 -0
  283. package/kanban/server/services/WorkItemRefineService.js +510 -0
  284. package/kanban/server/start.js +49 -0
  285. package/kanban/server/utils/kanban-logger.js +132 -0
  286. package/kanban/server/utils/markdown.js +91 -0
  287. package/kanban/server/utils/status-grouping.js +107 -0
  288. package/kanban/server/workers/sponsor-call-worker.js +84 -0
  289. package/kanban/server/workers/sprint-planning-worker.js +130 -0
  290. package/package.json +34 -7
@@ -0,0 +1,1174 @@
1
+ import { ValidationRouter } from './validation-router.js';
2
+ import { LLMProvider } from './llm-provider.js';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import { loadAgent } from './agent-loader.js';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ /**
12
+ * Multi-Agent Epic and Story Validator
13
+ *
14
+ * Orchestrates validation across multiple domain-specific validator agents.
15
+ * Each epic/story is reviewed by 2-8 specialized validators based on domain and features.
16
+ * After each validator, a paired solver agent improves the epic/story if issues are found,
17
+ * then the same validator re-validates — up to maxIterations times.
18
+ */
19
+ class EpicStoryValidator {
20
+ constructor(llmProvider, verificationTracker, stagesConfig = null, useSmartSelection = false, progressCallback = null, projectContext = null) {
21
+ this.llmProvider = llmProvider;
22
+ this.verificationTracker = verificationTracker;
23
+ this.projectContext = projectContext;
24
+
25
+ // Create router with smart selection support and project context
26
+ this.router = new ValidationRouter(llmProvider, useSmartSelection, projectContext);
27
+
28
+ this.agentsPath = path.join(__dirname, 'agents');
29
+ this.validationFeedback = new Map();
30
+
31
+ // Store validation stage configuration
32
+ this.validationStageConfig = stagesConfig?.validation || null;
33
+
34
+ // Cache for validator-specific providers
35
+ this._validatorProviders = {};
36
+
37
+ // Smart selection flag
38
+ this.useSmartSelection = useSmartSelection;
39
+
40
+ // Progress callback for UI detail emissions
41
+ this.progressCallback = progressCallback;
42
+
43
+ // Per-call token callback (propagated to all created providers)
44
+ this._tokenCallback = null;
45
+ }
46
+
47
+ /**
48
+ * Register a callback to be fired after every LLM API call made by this validator.
49
+ * Propagates to all provider instances created by this validator.
50
+ * @param {Function} fn - Receives { input, output, provider, model }
51
+ */
52
+ setTokenCallback(fn) {
53
+ this._tokenCallback = fn;
54
+ }
55
+
56
+ /** Emit a Level-3 detail line to the UI (fire-and-forget safe) */
57
+ async _detail(msg) {
58
+ await this.progressCallback?.(null, null, { detail: msg });
59
+ }
60
+
61
+ /**
62
+ * Wrap an async LLM call with a periodic elapsed-time heartbeat.
63
+ * Emits a detail message every `intervalMs` ms while the call runs,
64
+ * so the UI always shows activity during long LLM operations.
65
+ */
66
+ _withHeartbeat(fn, getMsg, intervalMs = 5000) {
67
+ const startTime = Date.now();
68
+ let lastMsg = null;
69
+ const timer = setInterval(() => {
70
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
71
+ const msg = getMsg(elapsed);
72
+ if (msg != null && msg !== lastMsg) {
73
+ lastMsg = msg;
74
+ this._detail(msg).catch(() => {});
75
+ // Also log to debug output so stuck calls are visible in log files
76
+ console.log(`[HEARTBEAT] ${msg} (${elapsed}s elapsed)`);
77
+ }
78
+ }, intervalMs);
79
+ return fn().finally(() => clearInterval(timer));
80
+ }
81
+
82
+ /**
83
+ * Determine validation type from validator name
84
+ * Returns: 'universal', 'domain', or 'feature'
85
+ */
86
+ getValidationType(validatorName) {
87
+ const role = validatorName.replace(/^validator-(epic|story)-/, '');
88
+
89
+ // Universal validators (always applied)
90
+ const epicUniversal = ['solution-architect', 'developer', 'security'];
91
+ const storyUniversal = ['developer', 'qa', 'test-architect'];
92
+
93
+ if (epicUniversal.includes(role) || storyUniversal.includes(role)) {
94
+ return 'universal';
95
+ }
96
+
97
+ // Domain validators
98
+ const domainValidators = ['devops', 'cloud', 'backend', 'database', 'api',
99
+ 'frontend', 'ui', 'ux', 'mobile', 'data'];
100
+
101
+ if (domainValidators.includes(role)) {
102
+ return 'domain';
103
+ }
104
+
105
+ // Everything else is feature-based
106
+ return 'feature';
107
+ }
108
+
109
+ /**
110
+ * Get provider for a specific validator based on validation type
111
+ * @param {string} validatorName - Validator name (e.g., 'validator-epic-security')
112
+ * @returns {Promise<LLMProvider>} LLM provider instance
113
+ */
114
+ async getProviderForValidator(validatorName) {
115
+ const validationType = this.getValidationType(validatorName);
116
+
117
+ // Check validation-type specific configuration
118
+ const validationTypeConfig = this.validationStageConfig?.validationTypes?.[validationType];
119
+
120
+ let provider, model;
121
+
122
+ if (validationTypeConfig?.provider) {
123
+ // Use validation-type-specific config
124
+ provider = validationTypeConfig.provider;
125
+ model = validationTypeConfig.model;
126
+ } else if (this.validationStageConfig?.provider) {
127
+ // Fallback to validation stage default
128
+ provider = this.validationStageConfig.provider;
129
+ model = this.validationStageConfig.model;
130
+ } else {
131
+ // Fallback to global default (use default llmProvider)
132
+ return this.llmProvider;
133
+ }
134
+
135
+ // Check cache
136
+ const cacheKey = `${validatorName}:${provider}:${model}`;
137
+ if (this._validatorProviders[cacheKey]) {
138
+ return this._validatorProviders[cacheKey];
139
+ }
140
+
141
+ // Create new provider
142
+ const providerInstance = await LLMProvider.create(provider, model);
143
+ if (this._tokenCallback) providerInstance.onCall((delta) => this._tokenCallback(delta, 'validation'));
144
+ this._validatorProviders[cacheKey] = providerInstance;
145
+
146
+ return providerInstance;
147
+ }
148
+
149
+ /**
150
+ * Get provider for a specific solver based on solver stage config
151
+ * @param {string} role - Role name (e.g., 'security')
152
+ * @returns {Promise<LLMProvider>} LLM provider instance
153
+ */
154
+ async getProviderForSolver(role) {
155
+ const solverConfig = this.validationStageConfig?.solver;
156
+ if (!solverConfig?.provider) return this.llmProvider;
157
+
158
+ const cacheKey = `solver:${solverConfig.provider}:${solverConfig.model}`;
159
+ if (this._validatorProviders[cacheKey]) return this._validatorProviders[cacheKey];
160
+
161
+ const instance = await LLMProvider.create(solverConfig.provider, solverConfig.model);
162
+ if (this._tokenCallback) instance.onCall((delta) => this._tokenCallback(delta, 'solver'));
163
+ this._validatorProviders[cacheKey] = instance;
164
+ return instance;
165
+ }
166
+
167
+ /**
168
+ * Get provider for contextual agent selection (agent-selector LLM call).
169
+ * Uses validation stage config; falls back to this.llmProvider.
170
+ * @returns {Promise<LLMProvider>}
171
+ */
172
+ async getProviderForSelection() {
173
+ if (this.validationStageConfig?.provider && this.validationStageConfig?.model) {
174
+ const cacheKey = `selection:${this.validationStageConfig.provider}:${this.validationStageConfig.model}`;
175
+ if (this._validatorProviders[cacheKey]) return this._validatorProviders[cacheKey];
176
+ const instance = await LLMProvider.create(this.validationStageConfig.provider, this.validationStageConfig.model);
177
+ if (this._tokenCallback) instance.onCall((delta) => this._tokenCallback(delta, 'validation'));
178
+ this._validatorProviders[cacheKey] = instance;
179
+ return instance;
180
+ }
181
+ return this.llmProvider;
182
+ }
183
+
184
+ /**
185
+ * Validate an Epic with multiple domain validators
186
+ * Runs validators sequentially; after each validator, if issues exist,
187
+ * a paired solver improves the epic before the next validator runs.
188
+ * @param {Object} epic - Epic work.json object
189
+ * @param {string} epicContext - Epic context.md content
190
+ * @returns {Object} Aggregated validation result
191
+ */
192
+ async validateEpic(epic, epicContext) {
193
+ // 1. Check cache for previously selected validators
194
+ let validators;
195
+ if (epic.metadata?.selectedValidators) {
196
+ validators = epic.metadata.selectedValidators;
197
+ console.log(` Using cached validator selection (${validators.length} validators)`);
198
+ } else {
199
+ // Get applicable validators for this epic
200
+ const useContextualSelection = this.validationStageConfig?.useContextualSelection || false;
201
+ console.log(`[DEBUG] Epic validator selection: useContextualSelection=${useContextualSelection}, validationStageConfig=${JSON.stringify(this.validationStageConfig)}`);
202
+ if (useContextualSelection) {
203
+ const selectionProvider = await this.getProviderForSelection();
204
+ validators = await this.router.selectValidatorsWithContext(epic, 'epic', selectionProvider);
205
+ } else if (this.useSmartSelection) {
206
+ validators = await this.router.getValidatorsForEpicWithLLM(epic);
207
+ } else {
208
+ validators = this.router.getValidatorsForEpic(epic);
209
+ }
210
+
211
+ // Cache selection in metadata
212
+ if (!epic.metadata) {
213
+ epic.metadata = {};
214
+ }
215
+ epic.metadata.selectedValidators = validators;
216
+ }
217
+
218
+ console.log(`\n🔍 Validating Epic: ${epic.name}`);
219
+ console.log(` Domain: ${epic.domain}`);
220
+ console.log(` Validators (${validators.length}): ${validators.map(v => this.extractDomain(v)).join(', ')}\n`);
221
+
222
+ // Read solver iteration settings
223
+ const solverConfig = this.validationStageConfig?.solver || {};
224
+ const maxIterations = solverConfig.maxIterations ?? 3;
225
+ const acceptanceThreshold = solverConfig.acceptanceThreshold ?? 95;
226
+
227
+ await this._detail(`${validators.length} validator${validators.length !== 1 ? 's' : ''}: ${validators.map(v => this.extractDomain(v)).join(', ')}`);
228
+
229
+ // Working copy — accumulates improvements from solver runs
230
+ // Keep stories array untouched; solver only modifies epic-level fields
231
+ let workingEpic = { ...epic };
232
+
233
+ // ── Phase 1: run all validators in parallel on the initial snapshot ──────────
234
+ await this._detail(`Running ${validators.length} validators in parallel…`);
235
+ console.log(` Running ${validators.length} validators in parallel…`);
236
+
237
+ const _t0Parallel = Date.now();
238
+ const parallelResults = await Promise.all(
239
+ validators.map((validatorName, vi) => {
240
+ const role = this.extractDomain(validatorName);
241
+ return this._withHeartbeat(
242
+ () => this.runEpicValidator(workingEpic, epicContext, validatorName),
243
+ (elapsed) => {
244
+ if (elapsed < 20) return ` [${role}] reviewing requirements…`;
245
+ if (elapsed < 40) return ` [${role}] analyzing concerns…`;
246
+ if (elapsed < 60) return ` [${role}] checking best practices…`;
247
+ return ` [${role}] still validating…`;
248
+ },
249
+ 10000
250
+ ).catch(err => {
251
+ console.warn(` ⚠ Validator ${validatorName} failed: ${err.message.split('\n')[0]}`);
252
+ return { overallScore: 0, validationStatus: 'error', issues: [], _validatorError: err.message.split('\n')[0] };
253
+ });
254
+ })
255
+ );
256
+ console.log(`[TIMING] epic parallel batch (${validators.length} validators): ${Date.now() - _t0Parallel}ms`);
257
+
258
+ // Log all parallel results
259
+ for (let vi = 0; vi < validators.length; vi++) {
260
+ const validatorName = validators[vi];
261
+ const role = this.extractDomain(validatorName);
262
+ const result = parallelResults[vi];
263
+ const score = result.overallScore ?? 0;
264
+ const allIssues = result.issues || [];
265
+ const critCount = allIssues.filter(i => i.severity === 'critical').length;
266
+ const majCount = allIssues.filter(i => i.severity === 'major').length;
267
+ const acceptable = score >= acceptanceThreshold;
268
+ const issueStr = allIssues.length > 0 ? ` · ${allIssues.length} issue${allIssues.length !== 1 ? 's' : ''}` : '';
269
+ await this._detail(`[${vi + 1}/${validators.length}] ${role}: ${score}/100${issueStr} — ${acceptable ? '✓' : '⚠ below threshold'}`);
270
+ console.log(` [${vi + 1}/${validators.length}] ${validatorName} score=${score}/100 status=${result.validationStatus} (critical=${critCount} major=${majCount} minor=${allIssues.length - critCount - majCount})`);
271
+ allIssues.forEach(issue => {
272
+ const cat = issue.category ? `[${issue.category}] ` : '';
273
+ const sug = issue.suggestion ? ` → ${issue.suggestion}` : '';
274
+ console.log(` [${(issue.severity || 'unknown').toUpperCase()}] ${cat}${issue.description || '(no description)'}${sug}`);
275
+ });
276
+ }
277
+
278
+ // ── Phase 2: parallel solver rounds ──────────────────────────────────────────
279
+ const finalResults = [...parallelResults];
280
+
281
+ // Indices of validators still below threshold
282
+ let needsWork = validators.map((_, vi) => vi).filter(vi =>
283
+ (parallelResults[vi].overallScore ?? 0) < acceptanceThreshold
284
+ );
285
+ if (needsWork.length > 0) {
286
+ console.log(` ${needsWork.length}/${validators.length} validators below threshold — running parallel solver rounds (max ${maxIterations - 1})`);
287
+ await this._detail(`${needsWork.length} below threshold — parallel solver rounds`);
288
+ }
289
+
290
+ for (let iter = 1; iter < maxIterations && needsWork.length > 0; iter++) {
291
+ const roundCount = needsWork.length;
292
+ await this._detail(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} in parallel…`);
293
+ console.log(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} running in parallel…`);
294
+ const _t0Round = Date.now();
295
+
296
+ // 1. Run all below-threshold solvers in parallel (all see the same workingEpic snapshot)
297
+ const solverResults = await Promise.all(
298
+ needsWork.map(vi => {
299
+ const validatorName = validators[vi];
300
+ const role = this.extractDomain(validatorName);
301
+ return this._withHeartbeat(
302
+ () => this.runEpicSolver(workingEpic, epicContext, finalResults[vi], validatorName),
303
+ (elapsed) => {
304
+ if (elapsed < 25) return ` ↻ ${role} solver — applying improvements…`;
305
+ if (elapsed < 50) return ` ↻ ${role} solver — refining epic quality…`;
306
+ return ` ↻ ${role} solver — still running…`;
307
+ },
308
+ 20000
309
+ ).then(improved => ({ vi, improved, error: null }))
310
+ .catch(err => ({ vi, improved: null, error: err }));
311
+ })
312
+ );
313
+ console.log(`[TIMING] Phase 2 round ${iter} solvers: ${Date.now() - _t0Round}ms`);
314
+
315
+ // 2. Merge all solver results — preserve base items, add up to 3 new items per solver
316
+ // Base items are anchored (never dropped); each solver contributes at most 3 genuinely new features.
317
+ // Description taken from the solver whose validator scored lowest (most informed fix).
318
+ const baseFeatures = workingEpic.features || [];
319
+ const baseFeaturesSet = new Set(baseFeatures);
320
+ const newFeatureAdditions = []; // items new beyond base, insertion-ordered, deduplicated
321
+ const allDeps = new Set(workingEpic.dependencies || []);
322
+ let bestDescription = workingEpic.description;
323
+ let worstValidatorScore = Infinity;
324
+ let anyImproved = false;
325
+
326
+ for (const { vi, improved, error } of solverResults) {
327
+ const validatorName = validators[vi];
328
+ const role = this.extractDomain(validatorName);
329
+ if (error) {
330
+ console.warn(` ⚠ Solver failed for ${validatorName}: ${error.message} — keeping current epic`);
331
+ await this._detail(` ⚠ Solver failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
332
+ continue;
333
+ }
334
+ if (improved && improved.id === workingEpic.id) {
335
+ anyImproved = true;
336
+ // Collect only new items not already in base or pending additions (max 3 per solver)
337
+ const existingAll = new Set([...baseFeaturesSet, ...newFeatureAdditions]);
338
+ const solverNew = (improved.features || []).filter(f => !existingAll.has(f)).slice(0, 3);
339
+ newFeatureAdditions.push(...solverNew);
340
+ (improved.dependencies || []).forEach(d => allDeps.add(d));
341
+ // Description: take from the validator with the lowest score (most dissatisfied perspective)
342
+ const vScore = finalResults[vi]?.overallScore ?? 100;
343
+ if (improved.description && improved.description !== workingEpic.description && vScore < worstValidatorScore) {
344
+ worstValidatorScore = vScore;
345
+ bestDescription = improved.description;
346
+ }
347
+ console.log(` ↻ [${role}] solver merged — ${solverNew.length} new features added`);
348
+ await this._detail(` → [${role}] improvements applied`);
349
+ } else {
350
+ console.log(` ↻ [${role}] solver returned no valid improvement (id mismatch or empty)`);
351
+ }
352
+ }
353
+
354
+ if (anyImproved) {
355
+ // Base items always preserved first; cap only the additions portion
356
+ const merged = [...baseFeatures, ...newFeatureAdditions];
357
+ const hardCap = baseFeatures.length + 12; // allow up to 12 new features beyond original
358
+ const finalFeatures = merged.length > hardCap ? merged.slice(0, hardCap) : merged;
359
+ if (merged.length > hardCap) {
360
+ console.log(` ↻ Epic features capped after merge: ${merged.length} → ${hardCap} (base=${baseFeatures.length} + new=${finalFeatures.length - baseFeatures.length})`);
361
+ }
362
+ const descBefore = (workingEpic.description || '').slice(0, 100);
363
+ workingEpic = {
364
+ ...workingEpic,
365
+ description: bestDescription,
366
+ features: finalFeatures,
367
+ dependencies: [...allDeps],
368
+ };
369
+ const descAfter = (workingEpic.description || '').slice(0, 100);
370
+ console.log(` ↻ Parallel merge applied — desc changed: ${descBefore !== descAfter}, features: ${finalFeatures.length} (base=${baseFeatures.length} + new=${newFeatureAdditions.length})`);
371
+ }
372
+
373
+ // 3. Re-validate all in parallel
374
+ await this._detail(` Re-validating ${roundCount} validator${roundCount !== 1 ? 's' : ''} in parallel (iter ${iter + 1})…`);
375
+ const _t0Revalidate = Date.now();
376
+ const revalidateResults = await Promise.all(
377
+ needsWork.map(vi => {
378
+ const validatorName = validators[vi];
379
+ const role = this.extractDomain(validatorName);
380
+ return this._withHeartbeat(
381
+ () => this.runEpicValidator(workingEpic, epicContext, validatorName),
382
+ (elapsed) => {
383
+ if (elapsed < 20) return ` [${role}] re-reviewing…`;
384
+ if (elapsed < 40) return ` [${role}] re-analyzing…`;
385
+ return ` [${role}] re-validating…`;
386
+ },
387
+ 10000
388
+ ).then(result => ({ vi, result, error: null }))
389
+ .catch(err => ({ vi, result: null, error: err }));
390
+ })
391
+ );
392
+ console.log(`[TIMING] Phase 2 round ${iter} re-validates: ${Date.now() - _t0Revalidate}ms`);
393
+
394
+ // 4. Update finalResults; determine which validators need another round
395
+ const nextNeedsWork = [];
396
+ for (const { vi, result, error } of revalidateResults) {
397
+ const validatorName = validators[vi];
398
+ const role = this.extractDomain(validatorName);
399
+ if (error) {
400
+ console.warn(` ⚠ Re-validator ${validatorName} failed: ${error.message.split('\n')[0]}`);
401
+ await this._detail(` ⚠ Re-validate failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
402
+ continue; // keep finalResults[vi] as-is; give up on this validator
403
+ }
404
+ finalResults[vi] = result;
405
+ const newScore = result.overallScore ?? 0;
406
+ const acceptable = newScore >= acceptanceThreshold;
407
+ const issueStr2 = (result.issues || []).length > 0 ? ` · ${result.issues.length} issues` : '';
408
+ await this._detail(` [${role}] iter ${iter + 1}: ${newScore}/100${issueStr2} — ${acceptable ? '✓ accepted' : `⚠ below threshold (${acceptanceThreshold})`}`);
409
+ console.log(` [${vi + 1}/${validators.length}] ${validatorName} iter=${iter + 1} score=${newScore}/100 status=${result.validationStatus}`);
410
+ if (!acceptable) {
411
+ nextNeedsWork.push(vi);
412
+ }
413
+ }
414
+ console.log(`[TIMING] Phase 2 round ${iter} total: ${Date.now() - _t0Round}ms — ${nextNeedsWork.length} still need work`);
415
+ needsWork = nextNeedsWork;
416
+ }
417
+
418
+ const validationResults = finalResults;
419
+
420
+ // Write accumulated improvements back to the original epic object
421
+ // (sprint-planning-processor owns the reference; this mutates it in place)
422
+ epic.description = workingEpic.description;
423
+ epic.features = workingEpic.features;
424
+ epic.dependencies = workingEpic.dependencies;
425
+
426
+ // 3. Aggregate results
427
+ const aggregated = this.aggregateValidationResults(validationResults, 'epic');
428
+
429
+ // 4. Determine overall status
430
+ aggregated.overallStatus = this.determineOverallStatus(validationResults);
431
+ aggregated.readyToPublish = aggregated.overallStatus !== 'needs-improvement';
432
+
433
+ await this._detail(`Overall: ${aggregated.readyToPublish ? '✓ passed' : '⚠ needs improvement'} · avg ${aggregated.averageScore}/100`);
434
+ console.log(` Epic "${epic.name}" summary: avg=${aggregated.averageScore}/100 readyToPublish=${aggregated.readyToPublish} critical=${aggregated.criticalIssues.length} major=${aggregated.majorIssues.length}`);
435
+ aggregated.validatorResults.forEach(vr => {
436
+ console.log(` ${this.extractDomain(vr.validator)}: ${vr.score}/100 (${vr.status})`);
437
+ });
438
+
439
+ // 5. Store for feedback loop
440
+ this.storeValidationFeedback(epic.id, aggregated);
441
+
442
+ // 6. Persist result into epic metadata so sprint-planning-processor writes it to work.json
443
+ epic.metadata = epic.metadata || {};
444
+ epic.metadata.validationResult = {
445
+ averageScore: aggregated.averageScore,
446
+ overallStatus: aggregated.overallStatus,
447
+ readyToPublish: aggregated.readyToPublish,
448
+ criticalIssues: aggregated.criticalIssues,
449
+ majorIssues: aggregated.majorIssues,
450
+ minorIssues: aggregated.minorIssues,
451
+ validatorResults: aggregated.validatorResults,
452
+ validatedAt: new Date().toISOString(),
453
+ };
454
+
455
+ return aggregated;
456
+ }
457
+
458
+ /**
459
+ * Validate a Story with multiple domain validators
460
+ * Runs validators sequentially; after each validator, if issues exist,
461
+ * a paired solver improves the story before the next validator runs.
462
+ * @param {Object} story - Story work.json object
463
+ * @param {string} storyContext - Story context.md content
464
+ * @param {Object} epic - Parent epic for routing
465
+ * @returns {Object} Aggregated validation result
466
+ */
467
+ async validateStory(story, storyContext, epic) {
468
+ // 1. Check cache for previously selected validators
469
+ let validators;
470
+ if (story.metadata?.selectedValidators) {
471
+ validators = story.metadata.selectedValidators;
472
+ console.log(` Using cached validator selection (${validators.length} validators)`);
473
+ } else {
474
+ // Get applicable validators for this story
475
+ const useContextualSelection = this.validationStageConfig?.useContextualSelection || false;
476
+ if (useContextualSelection) {
477
+ const selectionProvider = await this.getProviderForSelection();
478
+ validators = await this.router.selectValidatorsWithContext(story, 'story', selectionProvider, epic);
479
+ } else if (this.useSmartSelection) {
480
+ validators = await this.router.getValidatorsForStoryWithLLM(story, epic);
481
+ } else {
482
+ validators = this.router.getValidatorsForStory(story, epic);
483
+ }
484
+
485
+ // Cache selection in metadata
486
+ if (!story.metadata) {
487
+ story.metadata = {};
488
+ }
489
+ story.metadata.selectedValidators = validators;
490
+ }
491
+
492
+ console.log(`\n🔍 Validating Story: ${story.name}`);
493
+ console.log(` Epic: ${epic.name} (${epic.domain})`);
494
+ console.log(` Validators (${validators.length}): ${validators.map(v => this.extractDomain(v)).join(', ')}\n`);
495
+
496
+ // Read solver iteration settings
497
+ const solverConfig = this.validationStageConfig?.solver || {};
498
+ const maxIterations = solverConfig.maxIterations ?? 3;
499
+ const acceptanceThreshold = solverConfig.acceptanceThreshold ?? 95;
500
+
501
+ await this._detail(`${validators.length} validator${validators.length !== 1 ? 's' : ''}: ${validators.map(v => this.extractDomain(v)).join(', ')}`);
502
+
503
+ // Working copy — accumulates improvements from solver runs
504
+ let workingStory = { ...story };
505
+
506
+ // ── Phase 1: run all validators in parallel on the initial snapshot ──────────
507
+ await this._detail(`Running ${validators.length} validators in parallel…`);
508
+ console.log(` Running ${validators.length} validators in parallel…`);
509
+
510
+ const _t0Parallel = Date.now();
511
+ const parallelResults = await Promise.all(
512
+ validators.map((validatorName, vi) => {
513
+ const role = this.extractDomain(validatorName);
514
+ return this._withHeartbeat(
515
+ () => this.runStoryValidator(workingStory, storyContext, epic, validatorName),
516
+ (elapsed) => {
517
+ if (elapsed < 20) return ` [${role}] reviewing story…`;
518
+ if (elapsed < 40) return ` [${role}] checking acceptance criteria…`;
519
+ if (elapsed < 60) return ` [${role}] validating scope…`;
520
+ return ` [${role}] still validating…`;
521
+ },
522
+ 10000
523
+ ).catch(err => {
524
+ console.warn(` ⚠ Validator ${validatorName} failed: ${err.message.split('\n')[0]}`);
525
+ return { overallScore: 0, validationStatus: 'error', issues: [], _validatorError: err.message.split('\n')[0] };
526
+ });
527
+ })
528
+ );
529
+ console.log(`[TIMING] story parallel batch (${validators.length} validators): ${Date.now() - _t0Parallel}ms`);
530
+
531
+ // Log all parallel results
532
+ for (let vi = 0; vi < validators.length; vi++) {
533
+ const validatorName = validators[vi];
534
+ const role = this.extractDomain(validatorName);
535
+ const result = parallelResults[vi];
536
+ const score = result.overallScore ?? 0;
537
+ const allIssues = result.issues || [];
538
+ const critCount = allIssues.filter(i => i.severity === 'critical').length;
539
+ const majCount = allIssues.filter(i => i.severity === 'major').length;
540
+ const acceptable = score >= acceptanceThreshold;
541
+ const issueStr = allIssues.length > 0 ? ` · ${allIssues.length} issue${allIssues.length !== 1 ? 's' : ''}` : '';
542
+ await this._detail(`[${vi + 1}/${validators.length}] ${role}: ${score}/100${issueStr} — ${acceptable ? '✓' : '⚠ below threshold'}`);
543
+ console.log(` [${vi + 1}/${validators.length}] ${validatorName} score=${score}/100 status=${result.validationStatus} (critical=${critCount} major=${majCount} minor=${allIssues.length - critCount - majCount})`);
544
+ allIssues.forEach(issue => {
545
+ const cat = issue.category ? `[${issue.category}] ` : '';
546
+ const sug = issue.suggestion ? ` → ${issue.suggestion}` : '';
547
+ console.log(` [${(issue.severity || 'unknown').toUpperCase()}] ${cat}${issue.description || '(no description)'}${sug}`);
548
+ });
549
+ }
550
+
551
+ // ── Phase 2: parallel solver rounds ──────────────────────────────────────────
552
+ const finalResults = [...parallelResults];
553
+
554
+ // Indices of validators still below threshold
555
+ let needsWork = validators.map((_, vi) => vi).filter(vi =>
556
+ (parallelResults[vi].overallScore ?? 0) < acceptanceThreshold
557
+ );
558
+ if (needsWork.length > 0) {
559
+ console.log(` ${needsWork.length}/${validators.length} validators below threshold — running parallel solver rounds (max ${maxIterations - 1})`);
560
+ await this._detail(`${needsWork.length} below threshold — parallel solver rounds`);
561
+ }
562
+
563
+ for (let iter = 1; iter < maxIterations && needsWork.length > 0; iter++) {
564
+ const roundCount = needsWork.length;
565
+ await this._detail(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} in parallel…`);
566
+ console.log(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} running in parallel…`);
567
+ const _t0Round = Date.now();
568
+
569
+ // 1. Run all below-threshold solvers in parallel (all see the same workingStory snapshot)
570
+ const solverResults = await Promise.all(
571
+ needsWork.map(vi => {
572
+ const validatorName = validators[vi];
573
+ const role = this.extractDomain(validatorName);
574
+ return this._withHeartbeat(
575
+ () => this.runStorySolver(workingStory, storyContext, epic, finalResults[vi], validatorName),
576
+ (elapsed) => {
577
+ if (elapsed < 25) return ` ↻ ${role} solver — improving story…`;
578
+ if (elapsed < 50) return ` ↻ ${role} solver — refining acceptance criteria…`;
579
+ return ` ↻ ${role} solver — still running…`;
580
+ },
581
+ 20000
582
+ ).then(improved => ({ vi, improved, error: null }))
583
+ .catch(err => ({ vi, improved: null, error: err }));
584
+ })
585
+ );
586
+ console.log(`[TIMING] Phase 2 round ${iter} solvers: ${Date.now() - _t0Round}ms`);
587
+
588
+ // 2. Merge all solver results — preserve base items, add up to 3 new items per solver
589
+ // Base items are anchored (never dropped); each solver contributes at most 3 genuinely new AC.
590
+ // Description taken from the solver whose validator scored lowest (most informed fix).
591
+ const baseAC = workingStory.acceptance || [];
592
+ const baseACSet = new Set(baseAC);
593
+ const newACAdditions = []; // items new beyond base, insertion-ordered, deduplicated
594
+ const allDeps = new Set(workingStory.dependencies || []);
595
+ let bestDescription = workingStory.description;
596
+ let worstValidatorScore = Infinity;
597
+ let anyImproved = false;
598
+
599
+ for (const { vi, improved, error } of solverResults) {
600
+ const validatorName = validators[vi];
601
+ const role = this.extractDomain(validatorName);
602
+ if (error) {
603
+ console.warn(` ⚠ Solver failed for ${validatorName}: ${error.message} — keeping current story`);
604
+ await this._detail(` ⚠ Solver failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
605
+ continue;
606
+ }
607
+ if (improved && improved.id === workingStory.id) {
608
+ anyImproved = true;
609
+ // Collect only new items not already in base or pending additions (max 3 per solver)
610
+ const existingAll = new Set([...baseACSet, ...newACAdditions]);
611
+ const solverNew = (improved.acceptance || []).filter(a => !existingAll.has(a)).slice(0, 3);
612
+ newACAdditions.push(...solverNew);
613
+ (improved.dependencies || []).forEach(d => allDeps.add(d));
614
+ // Description: take from the validator with the lowest score (most dissatisfied perspective)
615
+ const vScore = finalResults[vi]?.overallScore ?? 100;
616
+ if (improved.description && improved.description !== workingStory.description && vScore < worstValidatorScore) {
617
+ worstValidatorScore = vScore;
618
+ bestDescription = improved.description;
619
+ }
620
+ console.log(` ↻ [${role}] solver merged — ${solverNew.length} new AC added`);
621
+ await this._detail(` → [${role}] improvements applied`);
622
+ } else {
623
+ console.log(` ↻ [${role}] solver returned no valid improvement (id mismatch or empty)`);
624
+ }
625
+ }
626
+
627
+ if (anyImproved) {
628
+ // Base items always preserved first; cap only the additions portion
629
+ const merged = [...baseAC, ...newACAdditions];
630
+ const hardCap = baseAC.length + 10; // allow up to 10 new AC beyond original
631
+ const finalAC = merged.length > hardCap ? merged.slice(0, hardCap) : merged;
632
+ if (merged.length > hardCap) {
633
+ console.log(` ↻ Story AC capped after merge: ${merged.length} → ${hardCap} (base=${baseAC.length} + new=${finalAC.length - baseAC.length})`);
634
+ }
635
+ const descBefore = (workingStory.description || '').slice(0, 100);
636
+ workingStory = {
637
+ ...workingStory,
638
+ description: bestDescription,
639
+ acceptance: finalAC,
640
+ dependencies: [...allDeps],
641
+ };
642
+ const descAfter = (workingStory.description || '').slice(0, 100);
643
+ console.log(` ↻ Parallel merge applied — desc changed: ${descBefore !== descAfter}, AC: ${finalAC.length} (base=${baseAC.length} + new=${newACAdditions.length})`);
644
+ }
645
+
646
+ // 3. Re-validate all in parallel
647
+ await this._detail(` Re-validating ${roundCount} validator${roundCount !== 1 ? 's' : ''} in parallel (iter ${iter + 1})…`);
648
+ const _t0Revalidate = Date.now();
649
+ const revalidateResults = await Promise.all(
650
+ needsWork.map(vi => {
651
+ const validatorName = validators[vi];
652
+ const role = this.extractDomain(validatorName);
653
+ return this._withHeartbeat(
654
+ () => this.runStoryValidator(workingStory, storyContext, epic, validatorName),
655
+ (elapsed) => {
656
+ if (elapsed < 20) return ` [${role}] re-reviewing story…`;
657
+ if (elapsed < 40) return ` [${role}] re-checking acceptance criteria…`;
658
+ return ` [${role}] re-validating…`;
659
+ },
660
+ 10000
661
+ ).then(result => ({ vi, result, error: null }))
662
+ .catch(err => ({ vi, result: null, error: err }));
663
+ })
664
+ );
665
+ console.log(`[TIMING] Phase 2 round ${iter} re-validates: ${Date.now() - _t0Revalidate}ms`);
666
+
667
+ // 4. Update finalResults; determine which validators need another round
668
+ const nextNeedsWork = [];
669
+ for (const { vi, result, error } of revalidateResults) {
670
+ const validatorName = validators[vi];
671
+ const role = this.extractDomain(validatorName);
672
+ if (error) {
673
+ console.warn(` ⚠ Re-validator ${validatorName} failed: ${error.message.split('\n')[0]}`);
674
+ await this._detail(` ⚠ Re-validate failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
675
+ continue; // keep finalResults[vi] as-is; give up on this validator
676
+ }
677
+ finalResults[vi] = result;
678
+ const newScore = result.overallScore ?? 0;
679
+ const acceptable = newScore >= acceptanceThreshold;
680
+ const issueStr2 = (result.issues || []).length > 0 ? ` · ${result.issues.length} issues` : '';
681
+ await this._detail(` [${role}] iter ${iter + 1}: ${newScore}/100${issueStr2} — ${acceptable ? '✓ accepted' : `⚠ below threshold (${acceptanceThreshold})`}`);
682
+ console.log(` [${vi + 1}/${validators.length}] ${validatorName} iter=${iter + 1} score=${newScore}/100 status=${result.validationStatus}`);
683
+ if (!acceptable) {
684
+ nextNeedsWork.push(vi);
685
+ }
686
+ }
687
+ console.log(`[TIMING] Phase 2 round ${iter} total: ${Date.now() - _t0Round}ms — ${nextNeedsWork.length} still need work`);
688
+ needsWork = nextNeedsWork;
689
+ }
690
+
691
+ const validationResults = finalResults;
692
+
693
+ // Write accumulated improvements back to the original story object
694
+ story.description = workingStory.description;
695
+ story.acceptance = workingStory.acceptance;
696
+ story.dependencies = workingStory.dependencies;
697
+
698
+ // 3. Aggregate results
699
+ const aggregated = this.aggregateValidationResults(validationResults, 'story');
700
+
701
+ // 4. Determine overall status
702
+ aggregated.overallStatus = this.determineOverallStatus(validationResults);
703
+ aggregated.readyToPublish = aggregated.overallStatus !== 'needs-improvement';
704
+
705
+ await this._detail(`Overall: ${aggregated.readyToPublish ? '✓ passed' : '⚠ needs improvement'} · avg ${aggregated.averageScore}/100`);
706
+ console.log(` Story "${story.name}" summary: avg=${aggregated.averageScore}/100 readyToPublish=${aggregated.readyToPublish} critical=${aggregated.criticalIssues.length} major=${aggregated.majorIssues.length}`);
707
+ aggregated.validatorResults.forEach(vr => {
708
+ console.log(` ${this.extractDomain(vr.validator)}: ${vr.score}/100 (${vr.status})`);
709
+ });
710
+
711
+ // 5. Store for feedback loop
712
+ this.storeValidationFeedback(story.id, aggregated);
713
+
714
+ // 6. Persist result into story metadata so sprint-planning-processor writes it to work.json
715
+ story.metadata = story.metadata || {};
716
+ story.metadata.validationResult = {
717
+ averageScore: aggregated.averageScore,
718
+ overallStatus: aggregated.overallStatus,
719
+ readyToPublish: aggregated.readyToPublish,
720
+ criticalIssues: aggregated.criticalIssues,
721
+ majorIssues: aggregated.majorIssues,
722
+ minorIssues: aggregated.minorIssues,
723
+ validatorResults: aggregated.validatorResults,
724
+ validatedAt: new Date().toISOString(),
725
+ };
726
+
727
+ return aggregated;
728
+ }
729
+
730
+ /**
731
+ * Run a single epic validator agent
732
+ * @private
733
+ */
734
+ async runEpicValidator(epic, epicContext, validatorName) {
735
+ const agentInstructions = this.loadAgentInstructions(`${validatorName}.md`);
736
+
737
+ // Build validation prompt
738
+ const prompt = this.buildEpicValidationPrompt(epic, epicContext);
739
+
740
+ // Get validator-specific provider based on validation type
741
+ const provider = await this.getProviderForValidator(validatorName);
742
+
743
+ // Call LLM with validator agent instructions
744
+ const _usageBefore = provider.getTokenUsage();
745
+ const _t0 = Date.now();
746
+ console.log(`[API-START] ${validatorName} (epic="${epic.name}", promptLen=${prompt.length})`);
747
+ const rawResult = await provider.generateJSON(prompt, agentInstructions);
748
+ const _elapsed = Date.now() - _t0;
749
+ const _usageAfter = provider.getTokenUsage();
750
+ const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
751
+ const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
752
+ console.log(`[API-DONE] ${validatorName} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
753
+
754
+ // Basic validation of result structure
755
+ if (!rawResult || typeof rawResult !== 'object') {
756
+ throw new Error(`Invalid validation result from ${validatorName}: expected object`);
757
+ }
758
+
759
+ // Track validation session
760
+ if (this.verificationTracker) {
761
+ this.verificationTracker.recordCheck(validatorName, 'epic-validation', true);
762
+ }
763
+
764
+ // Add metadata
765
+ rawResult._validatorName = validatorName;
766
+
767
+ return rawResult;
768
+ }
769
+
770
+ /**
771
+ * Run a single story validator agent
772
+ * @private
773
+ */
774
+ async runStoryValidator(story, storyContext, epic, validatorName) {
775
+ const agentInstructions = this.loadAgentInstructions(`${validatorName}.md`);
776
+
777
+ // Build validation prompt
778
+ const prompt = this.buildStoryValidationPrompt(story, storyContext, epic);
779
+
780
+ // Get validator-specific provider based on validation type
781
+ const provider = await this.getProviderForValidator(validatorName);
782
+
783
+ // Call LLM with validator agent instructions
784
+ const _usageBefore = provider.getTokenUsage();
785
+ const _t0 = Date.now();
786
+ console.log(`[API-START] ${validatorName} (story="${story.name}", promptLen=${prompt.length})`);
787
+ const rawResult = await provider.generateJSON(prompt, agentInstructions);
788
+ const _elapsed = Date.now() - _t0;
789
+ const _usageAfter = provider.getTokenUsage();
790
+ const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
791
+ const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
792
+ console.log(`[API-DONE] ${validatorName} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
793
+
794
+ // Basic validation of result structure
795
+ if (!rawResult || typeof rawResult !== 'object') {
796
+ throw new Error(`Invalid validation result from ${validatorName}: expected object`);
797
+ }
798
+
799
+ // Track validation session
800
+ if (this.verificationTracker) {
801
+ this.verificationTracker.recordCheck(validatorName, 'story-validation', true);
802
+ }
803
+
804
+ // Add metadata
805
+ rawResult._validatorName = validatorName;
806
+
807
+ return rawResult;
808
+ }
809
+
810
+ /**
811
+ * Run a solver agent to improve an epic based on validation issues
812
+ * @private
813
+ */
814
+ async runEpicSolver(epic, epicContext, validationResult, validatorName) {
815
+ const role = this.extractDomain(validatorName);
816
+ const agentInstructions = this.loadAgentInstructions(`solver-epic-${role}.md`);
817
+ const prompt = this.buildEpicSolverPrompt(epic, epicContext, validationResult, validatorName);
818
+ const provider = await this.getProviderForSolver(role);
819
+
820
+ const _usageBefore = provider.getTokenUsage();
821
+ const _t0 = Date.now();
822
+ console.log(`[API-START] solver-epic-${role} (epic="${epic.name}", promptLen=${prompt.length})`);
823
+ const improved = await provider.generateJSON(prompt, agentInstructions);
824
+ const _elapsed = Date.now() - _t0;
825
+ const _usageAfter = provider.getTokenUsage();
826
+ const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
827
+ const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
828
+ console.log(`[API-DONE] solver-epic-${role} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
829
+
830
+ if (this.verificationTracker) {
831
+ this.verificationTracker.recordCheck(`solver-epic-${role}`, 'epic-solving', true);
832
+ }
833
+
834
+ return improved;
835
+ }
836
+
837
+ /**
838
+ * Run a solver agent to improve a story based on validation issues
839
+ * @private
840
+ */
841
+ async runStorySolver(story, storyContext, epic, validationResult, validatorName) {
842
+ const role = this.extractDomain(validatorName);
843
+ const agentInstructions = this.loadAgentInstructions(`solver-story-${role}.md`);
844
+ const prompt = this.buildStorySolverPrompt(story, storyContext, epic, validationResult, validatorName);
845
+ const provider = await this.getProviderForSolver(role);
846
+
847
+ const _usageBefore = provider.getTokenUsage();
848
+ const _t0 = Date.now();
849
+ console.log(`[API-START] solver-story-${role} (story="${story.name}", promptLen=${prompt.length})`);
850
+ const improved = await provider.generateJSON(prompt, agentInstructions);
851
+ const _elapsed = Date.now() - _t0;
852
+ const _usageAfter = provider.getTokenUsage();
853
+ const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
854
+ const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
855
+ console.log(`[API-DONE] solver-story-${role} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
856
+
857
+ if (this.verificationTracker) {
858
+ this.verificationTracker.recordCheck(`solver-story-${role}`, 'story-solving', true);
859
+ }
860
+
861
+ return improved;
862
+ }
863
+
864
+ /**
865
+ * Build solver prompt for an Epic
866
+ * @private
867
+ */
868
+ buildEpicSolverPrompt(epic, epicContext, validationResult, validatorName) {
869
+ const allIssues = validationResult.issues || [];
870
+ const critMajor = allIssues.filter(i => i.severity === 'critical' || i.severity === 'major');
871
+ // When no critical/major issues exist, include minor issues so the solver has specific guidance
872
+ const issues = critMajor.length > 0 ? critMajor : allIssues;
873
+
874
+ const role = this.extractDomain(validatorName);
875
+
876
+ const issueText = issues.map((issue, i) =>
877
+ `${i + 1}. [${issue.severity.toUpperCase()}] ${issue.category}: ${issue.description}\n Fix: ${issue.suggestion}`
878
+ ).join('\n');
879
+
880
+ return `# Epic to Improve
881
+
882
+ **Epic ID:** ${epic.id}
883
+ **Epic Name:** ${epic.name}
884
+ **Domain:** ${epic.domain}
885
+ **Current Description:** ${epic.description}
886
+
887
+ **Current Features:**
888
+ ${(epic.features || []).map(f => `- ${f}`).join('\n')}
889
+
890
+ **Current Dependencies:**
891
+ ${(epic.dependencies || []).length > 0 ? epic.dependencies.join(', ') : 'None'}
892
+
893
+ **Epic Context:**
894
+ \`\`\`
895
+ ${epicContext}
896
+ \`\`\`
897
+
898
+ ## Issues to Fix (from ${role} review):
899
+
900
+ ${issueText || 'No critical/major issues — improve overall quality.'}
901
+
902
+ Improve this Epic to address the issues above. Return the complete improved Epic JSON.
903
+
904
+ **IMPORTANT CONSTRAINTS:**
905
+ - Do NOT remove or consolidate existing features — only add new ones to address the issues above.
906
+ - Each feature must be a single concise sentence (max 30 words). Do not expand them into paragraphs.
907
+ `;
908
+ }
909
+
910
+ /**
911
+ * Build solver prompt for a Story
912
+ * @private
913
+ */
914
+ buildStorySolverPrompt(story, storyContext, epic, validationResult, validatorName) {
915
+ const allIssues = validationResult.issues || [];
916
+ const critMajor = allIssues.filter(i => i.severity === 'critical' || i.severity === 'major');
917
+ // When no critical/major issues exist, include minor issues so the solver has specific guidance
918
+ const issues = critMajor.length > 0 ? critMajor : allIssues;
919
+
920
+ const role = this.extractDomain(validatorName);
921
+
922
+ const issueText = issues.map((issue, i) =>
923
+ `${i + 1}. [${issue.severity.toUpperCase()}] ${issue.category}: ${issue.description}\n Fix: ${issue.suggestion}`
924
+ ).join('\n');
925
+
926
+ return `# Story to Improve
927
+
928
+ **Story ID:** ${story.id}
929
+ **Story Name:** ${story.name}
930
+ **User Type:** ${story.userType}
931
+ **Current Description:** ${story.description}
932
+
933
+ **Current Acceptance Criteria:**
934
+ ${(story.acceptance || []).map((ac, i) => `${i + 1}. ${ac}`).join('\n')}
935
+
936
+ **Current Dependencies:**
937
+ ${(story.dependencies || []).length > 0 ? story.dependencies.join(', ') : 'None'}
938
+
939
+ **Parent Epic:**
940
+ - Name: ${epic.name}
941
+ - Domain: ${epic.domain}
942
+ - Features: ${(epic.features || []).join(', ')}
943
+
944
+ **Story Context:**
945
+ \`\`\`
946
+ ${storyContext}
947
+ \`\`\`
948
+
949
+ ## Issues to Fix (from ${role} review):
950
+
951
+ ${issueText || 'No critical/major issues — improve overall quality.'}
952
+
953
+ Improve this Story to address the issues above. Return the complete improved Story JSON.
954
+
955
+ **IMPORTANT CONSTRAINTS:**
956
+ - Do NOT remove or consolidate existing acceptance criteria — only add new ones to address the issues above.
957
+ - Each AC must be a single concrete, testable sentence (max 40 words). Do not expand them into paragraphs.
958
+ `;
959
+ }
960
+
961
+ /**
962
+ * Aggregate multiple validation results into a single report
963
+ * @private
964
+ */
965
+ aggregateValidationResults(results, type) {
966
+ const aggregated = {
967
+ type: type, // 'epic' or 'story'
968
+ validatorCount: results.length,
969
+ validators: results.map(r => r._validatorName),
970
+ averageScore: Math.round(
971
+ results.reduce((sum, r) => sum + (r.overallScore || 0), 0) / results.length
972
+ ),
973
+
974
+ // Aggregate issues by severity
975
+ criticalIssues: [],
976
+ majorIssues: [],
977
+ minorIssues: [],
978
+
979
+ // Aggregate strengths (deduplicated)
980
+ strengths: [],
981
+
982
+ // Aggregate improvement priorities (ranked by frequency)
983
+ improvementPriorities: [],
984
+
985
+ // Per-validator results summary
986
+ validatorResults: results.map(r => ({
987
+ validator: r._validatorName,
988
+ status: r.validationStatus,
989
+ score: r.overallScore || 0,
990
+ issueCount: (r.issues || []).length
991
+ }))
992
+ };
993
+
994
+ // Collect and categorize issues
995
+ results.forEach(result => {
996
+ (result.issues || []).forEach(issue => {
997
+ const enhancedIssue = {
998
+ ...issue,
999
+ validator: result._validatorName,
1000
+ domain: this.extractDomain(result._validatorName)
1001
+ };
1002
+
1003
+ if (issue.severity === 'critical') {
1004
+ aggregated.criticalIssues.push(enhancedIssue);
1005
+ } else if (issue.severity === 'major') {
1006
+ aggregated.majorIssues.push(enhancedIssue);
1007
+ } else {
1008
+ aggregated.minorIssues.push(enhancedIssue);
1009
+ }
1010
+ });
1011
+
1012
+ // Collect strengths (deduplicate similar ones)
1013
+ (result.strengths || []).forEach(strength => {
1014
+ if (!aggregated.strengths.some(s => this.isSimilar(s, strength))) {
1015
+ aggregated.strengths.push(strength);
1016
+ }
1017
+ });
1018
+ });
1019
+
1020
+ // Rank improvement priorities by frequency across validators
1021
+ const priorityMap = new Map();
1022
+ results.forEach(result => {
1023
+ (result.improvementPriorities || []).forEach(priority => {
1024
+ priorityMap.set(priority, (priorityMap.get(priority) || 0) + 1);
1025
+ });
1026
+ });
1027
+
1028
+ aggregated.improvementPriorities = Array.from(priorityMap.entries())
1029
+ .sort((a, b) => b[1] - a[1]) // Sort by frequency
1030
+ .slice(0, 5) // Top 5
1031
+ .map(([priority, count]) => ({ priority, mentionedBy: count }));
1032
+
1033
+ return aggregated;
1034
+ }
1035
+
1036
+ /**
1037
+ * Determine overall status from multiple validators
1038
+ * Uses "highest severity wins" approach
1039
+ * @private
1040
+ */
1041
+ determineOverallStatus(results) {
1042
+ const statuses = results.map(r => r.validationStatus);
1043
+
1044
+ // If any validator says "needs-improvement", overall is "needs-improvement"
1045
+ if (statuses.includes('needs-improvement')) {
1046
+ return 'needs-improvement';
1047
+ }
1048
+
1049
+ // If all are "excellent", overall is "excellent"
1050
+ if (statuses.every(s => s === 'excellent')) {
1051
+ return 'excellent';
1052
+ }
1053
+
1054
+ // Otherwise "acceptable"
1055
+ return 'acceptable';
1056
+ }
1057
+
1058
+ /**
1059
+ * Build validation prompt for Epic
1060
+ * @private
1061
+ */
1062
+ buildEpicValidationPrompt(epic, epicContext) {
1063
+ return `# Epic to Validate
1064
+
1065
+ **Epic ID:** ${epic.id}
1066
+ **Epic Name:** ${epic.name}
1067
+ **Domain:** ${epic.domain}
1068
+ **Description:** ${epic.description}
1069
+
1070
+ **Features:**
1071
+ ${(epic.features || []).map(f => `- ${f}`).join('\n')}
1072
+
1073
+ **Dependencies:**
1074
+ ${(epic.dependencies || []).length > 0 ? epic.dependencies.join(', ') : 'None'}
1075
+
1076
+ **Stories:**
1077
+ ${(epic.children || []).length} stories defined
1078
+
1079
+ **Epic Context:**
1080
+ \`\`\`
1081
+ ${epicContext}
1082
+ \`\`\`
1083
+
1084
+ Validate this Epic from your domain expertise perspective and return JSON validation results following the specified format.
1085
+ `;
1086
+ }
1087
+
1088
+ /**
1089
+ * Build validation prompt for Story
1090
+ * @private
1091
+ */
1092
+ buildStoryValidationPrompt(story, storyContext, epic) {
1093
+ return `# Story to Validate
1094
+
1095
+ **Story ID:** ${story.id}
1096
+ **Story Name:** ${story.name}
1097
+ **User Type:** ${story.userType}
1098
+ **Description:** ${story.description}
1099
+
1100
+ **Acceptance Criteria:**
1101
+ ${(story.acceptance || []).map((ac, i) => `${i + 1}. ${ac}`).join('\n')}
1102
+
1103
+ **Dependencies:**
1104
+ ${(story.dependencies || []).length > 0 ? story.dependencies.join(', ') : 'None'}
1105
+
1106
+ **Parent Epic:**
1107
+ - Name: ${epic.name}
1108
+ - Domain: ${epic.domain}
1109
+ - Features: ${(epic.features || []).join(', ')}
1110
+
1111
+ **Story Context:**
1112
+ \`\`\`
1113
+ ${storyContext}
1114
+ \`\`\`
1115
+
1116
+ Validate this Story from your domain expertise perspective and return JSON validation results following the specified format.
1117
+ `;
1118
+ }
1119
+
1120
+ /**
1121
+ * Load agent instructions from .md file
1122
+ * @private
1123
+ */
1124
+ loadAgentInstructions(filename) {
1125
+ try {
1126
+ return loadAgent(filename);
1127
+ } catch (err) {
1128
+ throw new Error(`Agent file not found: ${filename}`);
1129
+ }
1130
+ }
1131
+
1132
+
1133
+ /**
1134
+ * Extract domain name from validator or solver name
1135
+ * @private
1136
+ */
1137
+ extractDomain(validatorName) {
1138
+ // Extract domain from validator/solver name
1139
+ // e.g., "validator-epic-security" → "security"
1140
+ // e.g., "solver-epic-security" → "security"
1141
+ const match = validatorName.match(/(?:validator|solver)-(?:epic|story)-(.+)/);
1142
+ return match ? match[1] : 'unknown';
1143
+ }
1144
+
1145
+ /**
1146
+ * Check if two strings are similar (for deduplication)
1147
+ * @private
1148
+ */
1149
+ isSimilar(str1, str2) {
1150
+ // Simple similarity check (can be enhanced with fuzzy matching)
1151
+ const s1 = str1.toLowerCase();
1152
+ const s2 = str2.toLowerCase();
1153
+ return s1.includes(s2) || s2.includes(s1);
1154
+ }
1155
+
1156
+ /**
1157
+ * Store validation feedback for learning/feedback loops
1158
+ * @private
1159
+ */
1160
+ storeValidationFeedback(workItemId, aggregatedResult) {
1161
+ this.validationFeedback.set(workItemId, aggregatedResult);
1162
+ }
1163
+
1164
+ /**
1165
+ * Get validation feedback for a work item
1166
+ * @param {string} workItemId - Work item ID
1167
+ * @returns {Object|null} Aggregated validation result or null
1168
+ */
1169
+ getValidationFeedback(workItemId) {
1170
+ return this.validationFeedback.get(workItemId) || null;
1171
+ }
1172
+ }
1173
+
1174
+ export { EpicStoryValidator };