@agile-vibe-coding/avc 0.2.3 → 0.3.2

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 (262) hide show
  1. package/README.md +475 -3
  2. package/cli/agents/agent-selector.md +23 -0
  3. package/cli/agents/code-implementer.md +117 -0
  4. package/cli/agents/code-validator.md +80 -0
  5. package/cli/agents/context-reviewer-epic.md +101 -0
  6. package/cli/agents/context-reviewer-story.md +92 -0
  7. package/cli/agents/context-writer-epic.md +145 -0
  8. package/cli/agents/context-writer-story.md +111 -0
  9. package/cli/agents/doc-writer-epic.md +42 -0
  10. package/cli/agents/doc-writer-story.md +43 -0
  11. package/cli/agents/duplicate-detector.md +110 -0
  12. package/cli/agents/epic-story-decomposer.md +318 -39
  13. package/cli/agents/mission-scope-generator.md +68 -4
  14. package/cli/agents/mission-scope-validator.md +40 -6
  15. package/cli/agents/project-context-extractor.md +21 -6
  16. package/cli/agents/scaffolding-generator.md +99 -0
  17. package/cli/agents/seed-validator.md +71 -0
  18. package/cli/agents/story-scope-reviewer.md +147 -0
  19. package/cli/agents/story-splitter.md +83 -0
  20. package/cli/agents/validator-documentation.json +31 -0
  21. package/cli/agents/validator-documentation.md +3 -1
  22. package/cli/api-reference-tool.js +368 -0
  23. package/cli/checks/catalog.json +76 -0
  24. package/cli/checks/code/quality.json +26 -0
  25. package/cli/checks/code/testing.json +14 -0
  26. package/cli/checks/code/traceability.json +26 -0
  27. package/cli/checks/cross-refs/epic.json +171 -0
  28. package/cli/checks/cross-refs/story.json +149 -0
  29. package/cli/checks/epic/api.json +114 -0
  30. package/cli/checks/epic/backend.json +126 -0
  31. package/cli/checks/epic/cloud.json +126 -0
  32. package/cli/checks/epic/data.json +102 -0
  33. package/cli/checks/epic/database.json +114 -0
  34. package/cli/checks/epic/developer.json +182 -0
  35. package/cli/checks/epic/devops.json +174 -0
  36. package/cli/checks/epic/frontend.json +162 -0
  37. package/cli/checks/epic/mobile.json +102 -0
  38. package/cli/checks/epic/qa.json +90 -0
  39. package/cli/checks/epic/security.json +184 -0
  40. package/cli/checks/epic/solution-architect.json +192 -0
  41. package/cli/checks/epic/test-architect.json +90 -0
  42. package/cli/checks/epic/ui.json +102 -0
  43. package/cli/checks/epic/ux.json +90 -0
  44. package/cli/checks/fixes/epic-fix-template.md +10 -0
  45. package/cli/checks/fixes/story-fix-template.md +10 -0
  46. package/cli/checks/story/api.json +186 -0
  47. package/cli/checks/story/backend.json +102 -0
  48. package/cli/checks/story/cloud.json +102 -0
  49. package/cli/checks/story/data.json +210 -0
  50. package/cli/checks/story/database.json +102 -0
  51. package/cli/checks/story/developer.json +168 -0
  52. package/cli/checks/story/devops.json +102 -0
  53. package/cli/checks/story/frontend.json +174 -0
  54. package/cli/checks/story/mobile.json +102 -0
  55. package/cli/checks/story/qa.json +210 -0
  56. package/cli/checks/story/security.json +198 -0
  57. package/cli/checks/story/solution-architect.json +230 -0
  58. package/cli/checks/story/test-architect.json +210 -0
  59. package/cli/checks/story/ui.json +102 -0
  60. package/cli/checks/story/ux.json +102 -0
  61. package/cli/coding-order.js +401 -0
  62. package/cli/dependency-checker.js +72 -0
  63. package/cli/epic-story-validator.js +284 -799
  64. package/cli/index.js +0 -0
  65. package/cli/init-model-config.js +17 -10
  66. package/cli/init.js +514 -92
  67. package/cli/kanban-server-manager.js +1 -2
  68. package/cli/llm-claude.js +98 -31
  69. package/cli/llm-gemini.js +29 -5
  70. package/cli/llm-local.js +493 -0
  71. package/cli/llm-openai.js +262 -41
  72. package/cli/llm-provider.js +147 -8
  73. package/cli/llm-token-limits.js +113 -4
  74. package/cli/llm-verifier.js +209 -1
  75. package/cli/llm-xiaomi.js +143 -0
  76. package/cli/message-constants.js +3 -12
  77. package/cli/messaging-api.js +6 -12
  78. package/cli/micro-check-fixer.js +335 -0
  79. package/cli/micro-check-runner.js +449 -0
  80. package/cli/micro-check-scorer.js +148 -0
  81. package/cli/micro-check-validator.js +538 -0
  82. package/cli/model-pricing.js +23 -0
  83. package/cli/model-selector.js +3 -2
  84. package/cli/prompt-logger.js +57 -0
  85. package/cli/repl-ink.js +106 -346
  86. package/cli/repl-old.js +1 -2
  87. package/cli/seed-processor.js +194 -24
  88. package/cli/sprint-planning-processor.js +2638 -289
  89. package/cli/template-processor.js +50 -3
  90. package/cli/token-tracker.js +50 -23
  91. package/cli/tools/generate-story-validators.js +1 -1
  92. package/cli/validation-router.js +70 -8
  93. package/cli/worktree-runner.js +654 -0
  94. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  95. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  96. package/kanban/client/dist/index.html +2 -2
  97. package/kanban/client/src/App.jsx +43 -14
  98. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  99. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  100. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  101. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  102. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  103. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  104. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  105. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  106. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  107. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  108. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  109. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  110. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  111. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  112. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  113. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  114. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  115. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  116. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  117. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  118. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  119. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  120. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  121. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  122. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  123. package/kanban/client/src/hooks/useGrouping.js +59 -0
  124. package/kanban/client/src/lib/api.js +118 -4
  125. package/kanban/client/src/lib/status-grouping.js +10 -0
  126. package/kanban/client/src/store/kanbanStore.js +8 -0
  127. package/kanban/server/index.js +23 -2
  128. package/kanban/server/routes/ceremony.js +153 -4
  129. package/kanban/server/routes/costs.js +9 -3
  130. package/kanban/server/routes/openai-oauth.js +366 -0
  131. package/kanban/server/routes/settings.js +447 -14
  132. package/kanban/server/routes/websocket.js +7 -2
  133. package/kanban/server/routes/work-items.js +141 -1
  134. package/kanban/server/services/CeremonyService.js +275 -24
  135. package/kanban/server/services/TaskRunnerService.js +261 -0
  136. package/kanban/server/workers/run-task-worker.js +121 -0
  137. package/kanban/server/workers/seed-worker.js +94 -0
  138. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  139. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  140. package/package.json +2 -3
  141. package/cli/agents/solver-epic-api.json +0 -15
  142. package/cli/agents/solver-epic-api.md +0 -39
  143. package/cli/agents/solver-epic-backend.json +0 -15
  144. package/cli/agents/solver-epic-backend.md +0 -39
  145. package/cli/agents/solver-epic-cloud.json +0 -15
  146. package/cli/agents/solver-epic-cloud.md +0 -39
  147. package/cli/agents/solver-epic-data.json +0 -15
  148. package/cli/agents/solver-epic-data.md +0 -39
  149. package/cli/agents/solver-epic-database.json +0 -15
  150. package/cli/agents/solver-epic-database.md +0 -39
  151. package/cli/agents/solver-epic-developer.json +0 -15
  152. package/cli/agents/solver-epic-developer.md +0 -39
  153. package/cli/agents/solver-epic-devops.json +0 -15
  154. package/cli/agents/solver-epic-devops.md +0 -39
  155. package/cli/agents/solver-epic-frontend.json +0 -15
  156. package/cli/agents/solver-epic-frontend.md +0 -39
  157. package/cli/agents/solver-epic-mobile.json +0 -15
  158. package/cli/agents/solver-epic-mobile.md +0 -39
  159. package/cli/agents/solver-epic-qa.json +0 -15
  160. package/cli/agents/solver-epic-qa.md +0 -39
  161. package/cli/agents/solver-epic-security.json +0 -15
  162. package/cli/agents/solver-epic-security.md +0 -39
  163. package/cli/agents/solver-epic-solution-architect.json +0 -15
  164. package/cli/agents/solver-epic-solution-architect.md +0 -39
  165. package/cli/agents/solver-epic-test-architect.json +0 -15
  166. package/cli/agents/solver-epic-test-architect.md +0 -39
  167. package/cli/agents/solver-epic-ui.json +0 -15
  168. package/cli/agents/solver-epic-ui.md +0 -39
  169. package/cli/agents/solver-epic-ux.json +0 -15
  170. package/cli/agents/solver-epic-ux.md +0 -39
  171. package/cli/agents/solver-story-api.json +0 -15
  172. package/cli/agents/solver-story-api.md +0 -39
  173. package/cli/agents/solver-story-backend.json +0 -15
  174. package/cli/agents/solver-story-backend.md +0 -39
  175. package/cli/agents/solver-story-cloud.json +0 -15
  176. package/cli/agents/solver-story-cloud.md +0 -39
  177. package/cli/agents/solver-story-data.json +0 -15
  178. package/cli/agents/solver-story-data.md +0 -39
  179. package/cli/agents/solver-story-database.json +0 -15
  180. package/cli/agents/solver-story-database.md +0 -39
  181. package/cli/agents/solver-story-developer.json +0 -15
  182. package/cli/agents/solver-story-developer.md +0 -39
  183. package/cli/agents/solver-story-devops.json +0 -15
  184. package/cli/agents/solver-story-devops.md +0 -39
  185. package/cli/agents/solver-story-frontend.json +0 -15
  186. package/cli/agents/solver-story-frontend.md +0 -39
  187. package/cli/agents/solver-story-mobile.json +0 -15
  188. package/cli/agents/solver-story-mobile.md +0 -39
  189. package/cli/agents/solver-story-qa.json +0 -15
  190. package/cli/agents/solver-story-qa.md +0 -39
  191. package/cli/agents/solver-story-security.json +0 -15
  192. package/cli/agents/solver-story-security.md +0 -39
  193. package/cli/agents/solver-story-solution-architect.json +0 -15
  194. package/cli/agents/solver-story-solution-architect.md +0 -39
  195. package/cli/agents/solver-story-test-architect.json +0 -15
  196. package/cli/agents/solver-story-test-architect.md +0 -39
  197. package/cli/agents/solver-story-ui.json +0 -15
  198. package/cli/agents/solver-story-ui.md +0 -39
  199. package/cli/agents/solver-story-ux.json +0 -15
  200. package/cli/agents/solver-story-ux.md +0 -39
  201. package/cli/agents/validator-epic-api.json +0 -93
  202. package/cli/agents/validator-epic-api.md +0 -137
  203. package/cli/agents/validator-epic-backend.json +0 -93
  204. package/cli/agents/validator-epic-backend.md +0 -130
  205. package/cli/agents/validator-epic-cloud.json +0 -93
  206. package/cli/agents/validator-epic-cloud.md +0 -137
  207. package/cli/agents/validator-epic-data.json +0 -93
  208. package/cli/agents/validator-epic-data.md +0 -130
  209. package/cli/agents/validator-epic-database.json +0 -93
  210. package/cli/agents/validator-epic-database.md +0 -137
  211. package/cli/agents/validator-epic-developer.json +0 -74
  212. package/cli/agents/validator-epic-developer.md +0 -153
  213. package/cli/agents/validator-epic-devops.json +0 -74
  214. package/cli/agents/validator-epic-devops.md +0 -153
  215. package/cli/agents/validator-epic-frontend.json +0 -74
  216. package/cli/agents/validator-epic-frontend.md +0 -153
  217. package/cli/agents/validator-epic-mobile.json +0 -93
  218. package/cli/agents/validator-epic-mobile.md +0 -130
  219. package/cli/agents/validator-epic-qa.json +0 -93
  220. package/cli/agents/validator-epic-qa.md +0 -130
  221. package/cli/agents/validator-epic-security.json +0 -74
  222. package/cli/agents/validator-epic-security.md +0 -154
  223. package/cli/agents/validator-epic-solution-architect.json +0 -74
  224. package/cli/agents/validator-epic-solution-architect.md +0 -156
  225. package/cli/agents/validator-epic-test-architect.json +0 -93
  226. package/cli/agents/validator-epic-test-architect.md +0 -130
  227. package/cli/agents/validator-epic-ui.json +0 -93
  228. package/cli/agents/validator-epic-ui.md +0 -130
  229. package/cli/agents/validator-epic-ux.json +0 -93
  230. package/cli/agents/validator-epic-ux.md +0 -130
  231. package/cli/agents/validator-story-api.json +0 -104
  232. package/cli/agents/validator-story-api.md +0 -152
  233. package/cli/agents/validator-story-backend.json +0 -104
  234. package/cli/agents/validator-story-backend.md +0 -152
  235. package/cli/agents/validator-story-cloud.json +0 -104
  236. package/cli/agents/validator-story-cloud.md +0 -152
  237. package/cli/agents/validator-story-data.json +0 -104
  238. package/cli/agents/validator-story-data.md +0 -152
  239. package/cli/agents/validator-story-database.json +0 -104
  240. package/cli/agents/validator-story-database.md +0 -152
  241. package/cli/agents/validator-story-developer.json +0 -104
  242. package/cli/agents/validator-story-developer.md +0 -152
  243. package/cli/agents/validator-story-devops.json +0 -104
  244. package/cli/agents/validator-story-devops.md +0 -152
  245. package/cli/agents/validator-story-frontend.json +0 -104
  246. package/cli/agents/validator-story-frontend.md +0 -152
  247. package/cli/agents/validator-story-mobile.json +0 -104
  248. package/cli/agents/validator-story-mobile.md +0 -152
  249. package/cli/agents/validator-story-qa.json +0 -104
  250. package/cli/agents/validator-story-qa.md +0 -152
  251. package/cli/agents/validator-story-security.json +0 -104
  252. package/cli/agents/validator-story-security.md +0 -152
  253. package/cli/agents/validator-story-solution-architect.json +0 -104
  254. package/cli/agents/validator-story-solution-architect.md +0 -152
  255. package/cli/agents/validator-story-test-architect.json +0 -104
  256. package/cli/agents/validator-story-test-architect.md +0 -152
  257. package/cli/agents/validator-story-ui.json +0 -104
  258. package/cli/agents/validator-story-ui.md +0 -152
  259. package/cli/agents/validator-story-ux.json +0 -104
  260. package/cli/agents/validator-story-ux.md +0 -152
  261. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  262. package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import fs from 'fs';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { loadAgent } from './agent-loader.js';
7
+ import { validateWithMicroChecks } from './micro-check-validator.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
@@ -11,10 +12,8 @@ const __dirname = path.dirname(__filename);
11
12
  /**
12
13
  * Multi-Agent Epic and Story Validator
13
14
  *
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.
15
+ * Orchestrates validation via micro-checks: 370 small YES/NO LLM calls across
16
+ * 15 domain perspectives, deterministic scoring, and atomic per-check fixes.
18
17
  */
19
18
  class EpicStoryValidator {
20
19
  constructor(llmProvider, verificationTracker, stagesConfig = null, useSmartSelection = false, progressCallback = null, projectContext = null) {
@@ -28,7 +27,7 @@ class EpicStoryValidator {
28
27
  this.agentsPath = path.join(__dirname, 'agents');
29
28
  this.validationFeedback = new Map();
30
29
 
31
- // Store validation stage configuration
30
+ // Store validation stage configuration (solver stage removed — micro-checks handle fixes).
32
31
  this.validationStageConfig = stagesConfig?.validation || null;
33
32
 
34
33
  // Cache for validator-specific providers
@@ -42,6 +41,10 @@ class EpicStoryValidator {
42
41
 
43
42
  // Per-call token callback (propagated to all created providers)
44
43
  this._tokenCallback = null;
44
+
45
+ // Root project context.md string — prepended to all validation prompts when set
46
+ this.rootContextMd = null;
47
+
45
48
  }
46
49
 
47
50
  /**
@@ -53,6 +56,136 @@ class EpicStoryValidator {
53
56
  this._tokenCallback = fn;
54
57
  }
55
58
 
59
+ /**
60
+ * Attach a PromptLogger so all providers created by this validator write payloads.
61
+ * @param {import('./prompt-logger.js').PromptLogger} logger
62
+ */
63
+ setPromptLogger(logger) {
64
+ this._promptLogger = logger;
65
+ }
66
+
67
+ /**
68
+ * Set the root project context.md string — prepended to all validation prompts.
69
+ * @param {string} md - Canonical root context markdown
70
+ */
71
+ setRootContextMd(md) {
72
+ this.rootContextMd = md;
73
+ }
74
+
75
+ /**
76
+ * Set callback invoked when a validator call fails with a quota/rate-limit error.
77
+ * Signature: async ({ validatorName, errMsg, provider, model }) => { newProvider?, newModel? }
78
+ * Returning { newProvider, newModel } switches the validation+solver stage config.
79
+ * Returning null/undefined retries with the same model.
80
+ */
81
+ setQuotaExceededCallback(fn) {
82
+ this._quotaExceededCallback = fn;
83
+ }
84
+
85
+ /**
86
+ * Update validation stage config to a new provider/model and clear provider cache.
87
+ * Called after user selects "Switch Provider" in the quota-limit dialog.
88
+ */
89
+ _updateValidationStageConfig(newProvider, newModel) {
90
+ const oldProvider = this.validationStageConfig?.provider;
91
+ const oldModel = this.validationStageConfig?.model;
92
+ if (!this.validationStageConfig) this.validationStageConfig = {};
93
+ this.validationStageConfig.provider = newProvider;
94
+ this.validationStageConfig.model = newModel;
95
+ // Surgical cache invalidation: only clear entries matching the old provider/model
96
+ // so providers that were already on the new target are preserved
97
+ if (oldProvider || oldModel) {
98
+ for (const [key, prov] of Object.entries(this._validatorProviders)) {
99
+ if (prov?._providerName === oldProvider || prov?._modelName === oldModel) {
100
+ delete this._validatorProviders[key];
101
+ }
102
+ }
103
+ } else {
104
+ // No previous config — clear everything
105
+ this._validatorProviders = {};
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Returns true if the error message indicates a quota or persistent rate-limit failure.
111
+ */
112
+ _isQuotaOrRateLimit(errMsg) {
113
+ const m = (errMsg || '').toLowerCase();
114
+ return m.includes('429') || m.includes('quota') || m.includes('rate limit') ||
115
+ m.includes('resource exhausted') || m.includes('resource_exhausted') ||
116
+ m.includes('too many requests') ||
117
+ // Anthropic credit-balance exhausted (400 invalid_request_error)
118
+ m.includes('credit balance is too low') || m.includes('credit balance') ||
119
+ m.includes('billing') || m.includes('insufficient_quota');
120
+ }
121
+
122
+ /**
123
+ * Generate canonical context.md string for an epic from its JSON fields.
124
+ * This is the bounded, structured format passed to validators and solvers.
125
+ * @param {Object} epic
126
+ * @returns {string}
127
+ */
128
+ generateEpicContextMd(epic) {
129
+ const features = (epic.features || []).map(f => `- ${f}`).join('\n') || '- (none)';
130
+ const deps = epic.dependencies || [];
131
+ const optional = deps.filter(d => /optional/i.test(d));
132
+ const required = deps.filter(d => !/optional/i.test(d));
133
+ const reqLines = required.length ? required.map(d => `- ${d}`).join('\n') : '- (none)';
134
+ const storyCount = (epic.stories || []).length;
135
+ const lines = [
136
+ `# Epic: ${epic.name}`,
137
+ ``,
138
+ `## Identity`,
139
+ `- id: ${epic.id || '(pending)'}`,
140
+ `- domain: ${epic.domain}`,
141
+ `- stories: ${storyCount}`,
142
+ ``,
143
+ `## Summary`,
144
+ epic.description || '(no description)',
145
+ ``,
146
+ `## Features`,
147
+ features,
148
+ ``,
149
+ `## Dependencies`,
150
+ ``,
151
+ `### Required`,
152
+ reqLines,
153
+ ];
154
+ if (optional.length) {
155
+ lines.push('', '### Optional');
156
+ optional.forEach(d => lines.push(`- ${d}`));
157
+ }
158
+ return lines.join('\n');
159
+ }
160
+
161
+ /**
162
+ * Generate canonical context.md string for a story from its JSON fields.
163
+ * @param {Object} story
164
+ * @param {Object} epic - Parent epic for identity context
165
+ * @returns {string}
166
+ */
167
+ generateStoryContextMd(story, epic) {
168
+ const ac = (story.acceptance || []).map((a, i) => `${i + 1}. ${a}`).join('\n') || '1. (none)';
169
+ const deps = (story.dependencies || []).map(d => `- ${d}`).join('\n') || '- (none)';
170
+ return [
171
+ `# Story: ${story.name}`,
172
+ ``,
173
+ `## Identity`,
174
+ `- id: ${story.id || '(pending)'}`,
175
+ `- epic: ${epic.id || '(pending)'} (${epic.name})`,
176
+ `- userType: ${story.userType || 'team member'}`,
177
+ ``,
178
+ `## Summary`,
179
+ story.description || '(no description)',
180
+ ``,
181
+ `## Acceptance Criteria`,
182
+ ac,
183
+ ``,
184
+ `## Dependencies`,
185
+ deps,
186
+ ].join('\n');
187
+ }
188
+
56
189
  /** Emit a Level-3 detail line to the UI (fire-and-forget safe) */
57
190
  async _detail(msg) {
58
191
  await this.progressCallback?.(null, null, { detail: msg });
@@ -139,31 +272,15 @@ class EpicStoryValidator {
139
272
  }
140
273
 
141
274
  // Create new provider
275
+ const role = this.extractDomain(validatorName);
142
276
  const providerInstance = await LLMProvider.create(provider, model);
143
277
  if (this._tokenCallback) providerInstance.onCall((delta) => this._tokenCallback(delta, 'validation'));
278
+ if (this._promptLogger) providerInstance.setPromptLogger(this._promptLogger, `validation-${role}`);
144
279
  this._validatorProviders[cacheKey] = providerInstance;
145
280
 
146
281
  return providerInstance;
147
282
  }
148
283
 
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
284
  /**
168
285
  * Get provider for contextual agent selection (agent-selector LLM call).
169
286
  * Uses validation stage config; falls back to this.llmProvider.
@@ -175,6 +292,7 @@ class EpicStoryValidator {
175
292
  if (this._validatorProviders[cacheKey]) return this._validatorProviders[cacheKey];
176
293
  const instance = await LLMProvider.create(this.validationStageConfig.provider, this.validationStageConfig.model);
177
294
  if (this._tokenCallback) instance.onCall((delta) => this._tokenCallback(delta, 'validation'));
295
+ if (this._promptLogger) instance.setPromptLogger(this._promptLogger, 'selection');
178
296
  this._validatorProviders[cacheKey] = instance;
179
297
  return instance;
180
298
  }
@@ -182,780 +300,198 @@ class EpicStoryValidator {
182
300
  }
183
301
 
184
302
  /**
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.
303
+ * Validate an Epic via micro-checks: parallel YES/NO domain checks,
304
+ * cross-reference consistency checks, deterministic scoring, and atomic fixes.
188
305
  * @param {Object} epic - Epic work.json object
189
306
  * @param {string} epicContext - Epic context.md content
190
307
  * @returns {Object} Aggregated validation result
191
308
  */
192
309
  async validateEpic(epic, epicContext) {
193
- // 1. Check cache for previously selected validators
194
- let validators;
310
+ console.log(`\n🔍 Validating Epic (micro-checks): ${epic.name}`);
311
+ // Determine perspectives from router
312
+ let perspectives;
195
313
  if (epic.metadata?.selectedValidators) {
196
- validators = epic.metadata.selectedValidators;
197
- console.log(` Using cached validator selection (${validators.length} validators)`);
314
+ perspectives = epic.metadata.selectedValidators.map(v => this.extractDomain(v));
198
315
  } 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
- }
316
+ const validators = this.router.getValidatorsForEpic(epic);
317
+ if (!epic.metadata) epic.metadata = {};
215
318
  epic.metadata.selectedValidators = validators;
319
+ perspectives = validators.map(v => this.extractDomain(v));
216
320
  }
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
- }
321
+ const workItemText = epicContext || this.generateEpicContextMd(epic);
322
+ const mcResult = await validateWithMicroChecks(
323
+ epic, workItemText, 'epic', perspectives, this.llmProvider,
324
+ {
325
+ concurrency: this.validationStageConfig?.concurrency ?? 5,
326
+ batchSize: this.validationStageConfig?.batchSize ?? 8,
327
+ maxFixAttempts: this.validationStageConfig?.maxFixAttempts ?? 3,
328
+ generateContextFn: (wi) => this.generateEpicContextMd(wi),
329
+ progressCallback: this.progressCallback,
330
+ projectRoot: this.projectContext?.projectRoot,
352
331
  }
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
332
+ );
443
333
  epic.metadata = epic.metadata || {};
444
334
  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(),
335
+ averageScore: mcResult.averageScore,
336
+ overallStatus: mcResult.overallStatus,
337
+ readyToPublish: mcResult.readyToPublish,
338
+ criticalIssues: mcResult.criticalIssues,
339
+ majorIssues: mcResult.majorIssues,
340
+ minorIssues: mcResult.minorIssues,
341
+ validatorResults: mcResult.validatorResults,
342
+ validatedAt: mcResult.validatedAt,
453
343
  };
454
-
455
- return aggregated;
344
+ this.storeValidationFeedback(epic.id, mcResult);
345
+ return mcResult;
456
346
  }
457
347
 
458
348
  /**
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.
349
+ * Validate a Story via micro-checks: parallel YES/NO domain checks,
350
+ * cross-reference consistency checks, deterministic scoring, and atomic fixes.
462
351
  * @param {Object} story - Story work.json object
463
352
  * @param {string} storyContext - Story context.md content
464
353
  * @param {Object} epic - Parent epic for routing
465
354
  * @returns {Object} Aggregated validation result
466
355
  */
467
356
  async validateStory(story, storyContext, epic) {
468
- // 1. Check cache for previously selected validators
469
- let validators;
357
+ console.log(`\n🔍 Validating Story (micro-checks): ${story.name}`);
358
+ let perspectives;
470
359
  if (story.metadata?.selectedValidators) {
471
- validators = story.metadata.selectedValidators;
472
- console.log(` Using cached validator selection (${validators.length} validators)`);
360
+ perspectives = story.metadata.selectedValidators.map(v => this.extractDomain(v));
473
361
  } 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
- }
362
+ const validators = this.router.getValidatorsForStory(story, epic);
363
+ if (!story.metadata) story.metadata = {};
489
364
  story.metadata.selectedValidators = validators;
365
+ perspectives = validators.map(v => this.extractDomain(v));
490
366
  }
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
- }
367
+ const workItemText = storyContext || this.generateStoryContextMd(story, epic);
368
+ const mcResult = await validateWithMicroChecks(
369
+ story, workItemText, 'story', perspectives, this.llmProvider,
370
+ {
371
+ concurrency: this.validationStageConfig?.concurrency ?? 5,
372
+ batchSize: this.validationStageConfig?.batchSize ?? 8,
373
+ maxFixAttempts: this.validationStageConfig?.maxFixAttempts ?? 3,
374
+ generateContextFn: (wi) => this.generateStoryContextMd(wi, epic),
375
+ progressCallback: this.progressCallback,
376
+ projectRoot: this.projectContext?.projectRoot,
686
377
  }
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
378
+ );
715
379
  story.metadata = story.metadata || {};
716
380
  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(),
381
+ averageScore: mcResult.averageScore,
382
+ overallStatus: mcResult.overallStatus,
383
+ readyToPublish: mcResult.readyToPublish,
384
+ criticalIssues: mcResult.criticalIssues,
385
+ majorIssues: mcResult.majorIssues,
386
+ minorIssues: mcResult.minorIssues,
387
+ validatorResults: mcResult.validatorResults,
388
+ validatedAt: mcResult.validatedAt,
725
389
  };
726
-
727
- return aggregated;
390
+ this.storeValidationFeedback(story.id, mcResult);
391
+ return mcResult;
728
392
  }
729
393
 
730
394
  /**
731
- * Run a single epic validator agent
395
+ * Build the prompt for the story-splitter agent.
396
+ * @param {Object} story - The oversized story to split
397
+ * @param {Object} epic - Parent epic for context
398
+ * @param {Array} allIssues - MAJOR+CRITICAL issues from all validators
399
+ * @returns {string}
732
400
  * @private
733
401
  */
734
- async runEpicValidator(epic, epicContext, validatorName) {
735
- const agentInstructions = this.loadAgentInstructions(`${validatorName}.md`);
402
+ _buildStorySplitterPrompt(story, epic, allIssues) {
403
+ const fullEpicContext = this.generateEpicContextMd(epic);
404
+ const epicContextMd = fullEpicContext.length > 3000
405
+ ? fullEpicContext.substring(0, 3000) + '\n… (truncated)'
406
+ : fullEpicContext;
407
+ const storyContext = this.generateStoryContextMd(story, epic);
408
+ const critMajor = allIssues.filter(i => i.severity === 'critical' || i.severity === 'major');
409
+ const issueText = critMajor.map((issue, i) => {
410
+ const cat = issue.category ? `[${issue.category}] ` : '';
411
+ const sug = issue.suggestion ? `\n Required fix: ${issue.suggestion}` : '';
412
+ return `${i + 1}. [${(issue.severity || 'major').toUpperCase()}] ${cat}${issue.description || ''}${sug}`;
413
+ }).join('\n');
736
414
 
737
- // Build validation prompt
738
- const prompt = this.buildEpicValidationPrompt(epic, epicContext);
415
+ return `# Story to Split
739
416
 
740
- // Get validator-specific provider based on validation type
741
- const provider = await this.getProviderForValidator(validatorName);
417
+ ## Parent Epic (canonical)
742
418
 
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`);
419
+ ${epicContextMd}
753
420
 
754
- // Basic validation of result structure
755
- if (!rawResult || typeof rawResult !== 'object') {
756
- throw new Error(`Invalid validation result from ${validatorName}: expected object`);
757
- }
421
+ ## Original Story (too large — ${(story.acceptance || []).length} acceptance criteria)
758
422
 
759
- // Track validation session
760
- if (this.verificationTracker) {
761
- this.verificationTracker.recordCheck(validatorName, 'epic-validation', true);
762
- }
423
+ ${storyContext}
424
+
425
+ ## Validator Issues That Triggered This Split
763
426
 
764
- // Add metadata
765
- rawResult._validatorName = validatorName;
427
+ ${issueText || 'Story exceeded the acceptance criteria size limit after multiple validation passes.'}
766
428
 
767
- return rawResult;
429
+ Split this story into 2-3 smaller independently deliverable stories.
430
+ Return a JSON array of story objects as specified in your instructions.
431
+ `;
768
432
  }
769
433
 
770
434
  /**
771
- * Run a single story validator agent
772
- * @private
435
+ * Call the story-splitter agent to decompose an oversized story into 2-3 smaller stories.
436
+ * Returns an array of 2-3 new story objects, or null if the split failed/produced invalid output.
437
+ * @param {Object} workingStory - The fully-validated but too-large story
438
+ * @param {Object} epic - Parent epic for context
439
+ * @param {Array} allIssues - MAJOR+CRITICAL issues from all validators
440
+ * @returns {Promise<Array<Object>|null>}
773
441
  */
774
- async runStoryValidator(story, storyContext, epic, validatorName) {
775
- const agentInstructions = this.loadAgentInstructions(`${validatorName}.md`);
442
+ async _splitStory(workingStory, epic, allIssues) {
443
+ const agentInstructions = this.loadAgentInstructions('story-splitter.md');
444
+ const prompt = this._buildStorySplitterPrompt(workingStory, epic, allIssues);
445
+ const provider = await this.getProviderForValidator('story-splitter');
776
446
 
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
447
  const _usageBefore = provider.getTokenUsage();
785
448
  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
- }
449
+ console.log(`[API-START] story-splitter (story="${workingStory.name}", ACs=${(workingStory.acceptance || []).length})`);
798
450
 
799
- // Track validation session
800
- if (this.verificationTracker) {
801
- this.verificationTracker.recordCheck(validatorName, 'story-validation', true);
451
+ let result;
452
+ try {
453
+ result = await this._withHeartbeat(
454
+ () => provider.generateJSON(prompt, agentInstructions),
455
+ (elapsed) => {
456
+ if (elapsed < 20) return ` ✂ splitting story — analyzing scope boundaries…`;
457
+ if (elapsed < 40) return ` ✂ splitting story — assigning acceptance criteria…`;
458
+ return ` ✂ splitting story — still running…`;
459
+ },
460
+ 10000
461
+ );
462
+ } catch (err) {
463
+ console.warn(` ⚠ story-splitter failed: ${err.message.split('\n')[0]}`);
464
+ return null;
802
465
  }
803
466
 
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
467
  const _elapsed = Date.now() - _t0;
825
468
  const _usageAfter = provider.getTokenUsage();
826
469
  const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
827
470
  const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
828
- console.log(`[API-DONE] solver-epic-${role} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
471
+ console.log(`[API-DONE] story-splitter — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
829
472
 
830
- if (this.verificationTracker) {
831
- this.verificationTracker.recordCheck(`solver-epic-${role}`, 'epic-solving', true);
473
+ // Validate: result must be an array of 2-3 stories
474
+ if (!Array.isArray(result) || result.length < 2 || result.length > 3) {
475
+ console.warn(` ⚠ story-splitter returned unexpected shape (expected array of 2-3, got ${Array.isArray(result) ? result.length : typeof result}) — skipping split`);
476
+ return null;
832
477
  }
833
478
 
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`);
479
+ // Validate each split story has required fields; cap ACs to 8
480
+ for (const s of result) {
481
+ if (!s.id || !s.name || !Array.isArray(s.acceptance)) {
482
+ console.warn(` ⚠ story-splitter returned malformed story (missing id/name/acceptance) — skipping split`);
483
+ return null;
484
+ }
485
+ if (s.acceptance.length > 8) {
486
+ s.acceptance = s.acceptance.slice(0, 8);
487
+ }
488
+ }
856
489
 
857
490
  if (this.verificationTracker) {
858
- this.verificationTracker.recordCheck(`solver-story-${role}`, 'story-solving', true);
491
+ this.verificationTracker.recordCheck('story-splitter', 'story-splitting', true);
859
492
  }
860
493
 
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
- `;
494
+ return result;
959
495
  }
960
496
 
961
497
  /**
@@ -967,9 +503,13 @@ Improve this Story to address the issues above. Return the complete improved Sto
967
503
  type: type, // 'epic' or 'story'
968
504
  validatorCount: results.length,
969
505
  validators: results.map(r => r._validatorName),
970
- averageScore: Math.round(
971
- results.reduce((sum, r) => sum + (r.overallScore || 0), 0) / results.length
972
- ),
506
+ // Exclude errored validators (API failures) from the average — a 0 from a
507
+ // network failure is not a real score and would corrupt the aggregate.
508
+ averageScore: (() => {
509
+ const scorable = results.filter(r => !r._validatorError);
510
+ if (scorable.length === 0) return 0;
511
+ return Math.round(scorable.reduce((sum, r) => sum + (r.overallScore || 0), 0) / scorable.length);
512
+ })(),
973
513
 
974
514
  // Aggregate issues by severity
975
515
  criticalIssues: [],
@@ -1041,6 +581,12 @@ Improve this Story to address the issues above. Return the complete improved Sto
1041
581
  determineOverallStatus(results) {
1042
582
  const statuses = results.map(r => r.validationStatus);
1043
583
 
584
+ // If any validator errored (API failure), mark as needs-improvement so
585
+ // readyToPublish stays false — validation is incomplete, not "acceptable"
586
+ if (statuses.includes('error')) {
587
+ return 'needs-improvement';
588
+ }
589
+
1044
590
  // If any validator says "needs-improvement", overall is "needs-improvement"
1045
591
  if (statuses.includes('needs-improvement')) {
1046
592
  return 'needs-improvement';
@@ -1055,68 +601,6 @@ Improve this Story to address the issues above. Return the complete improved Sto
1055
601
  return 'acceptable';
1056
602
  }
1057
603
 
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
604
  /**
1121
605
  * Load agent instructions from .md file
1122
606
  * @private
@@ -1138,6 +622,7 @@ Validate this Story from your domain expertise perspective and return JSON valid
1138
622
  // Extract domain from validator/solver name
1139
623
  // e.g., "validator-epic-security" → "security"
1140
624
  // e.g., "solver-epic-security" → "security"
625
+ if (!validatorName) return 'unknown';
1141
626
  const match = validatorName.match(/(?:validator|solver)-(?:epic|story)-(.+)/);
1142
627
  return match ? match[1] : 'unknown';
1143
628
  }