@agile-vibe-coding/avc 0.1.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
@@ -0,0 +1,659 @@
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
+ import { validateWithMicroChecks } from './micro-check-validator.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ /**
13
+ * Multi-Agent Epic and Story Validator
14
+ *
15
+ * Orchestrates validation via micro-checks: 370 small YES/NO LLM calls across
16
+ * 15 domain perspectives, deterministic scoring, and atomic per-check fixes.
17
+ */
18
+ class EpicStoryValidator {
19
+ constructor(llmProvider, verificationTracker, stagesConfig = null, useSmartSelection = false, progressCallback = null, projectContext = null) {
20
+ this.llmProvider = llmProvider;
21
+ this.verificationTracker = verificationTracker;
22
+ this.projectContext = projectContext;
23
+
24
+ // Create router with smart selection support and project context
25
+ this.router = new ValidationRouter(llmProvider, useSmartSelection, projectContext);
26
+
27
+ this.agentsPath = path.join(__dirname, 'agents');
28
+ this.validationFeedback = new Map();
29
+
30
+ // Store validation stage configuration (solver stage removed — micro-checks handle fixes).
31
+ this.validationStageConfig = stagesConfig?.validation || null;
32
+
33
+ // Cache for validator-specific providers
34
+ this._validatorProviders = {};
35
+
36
+ // Smart selection flag
37
+ this.useSmartSelection = useSmartSelection;
38
+
39
+ // Progress callback for UI detail emissions
40
+ this.progressCallback = progressCallback;
41
+
42
+ // Per-call token callback (propagated to all created providers)
43
+ this._tokenCallback = null;
44
+
45
+ // Root project context.md string — prepended to all validation prompts when set
46
+ this.rootContextMd = null;
47
+
48
+ }
49
+
50
+ /**
51
+ * Register a callback to be fired after every LLM API call made by this validator.
52
+ * Propagates to all provider instances created by this validator.
53
+ * @param {Function} fn - Receives { input, output, provider, model }
54
+ */
55
+ setTokenCallback(fn) {
56
+ this._tokenCallback = fn;
57
+ }
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
+
189
+ /** Emit a Level-3 detail line to the UI (fire-and-forget safe) */
190
+ async _detail(msg) {
191
+ await this.progressCallback?.(null, null, { detail: msg });
192
+ }
193
+
194
+ /**
195
+ * Wrap an async LLM call with a periodic elapsed-time heartbeat.
196
+ * Emits a detail message every `intervalMs` ms while the call runs,
197
+ * so the UI always shows activity during long LLM operations.
198
+ */
199
+ _withHeartbeat(fn, getMsg, intervalMs = 5000) {
200
+ const startTime = Date.now();
201
+ let lastMsg = null;
202
+ const timer = setInterval(() => {
203
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
204
+ const msg = getMsg(elapsed);
205
+ if (msg != null && msg !== lastMsg) {
206
+ lastMsg = msg;
207
+ this._detail(msg).catch(() => {});
208
+ // Also log to debug output so stuck calls are visible in log files
209
+ console.log(`[HEARTBEAT] ${msg} (${elapsed}s elapsed)`);
210
+ }
211
+ }, intervalMs);
212
+ return fn().finally(() => clearInterval(timer));
213
+ }
214
+
215
+ /**
216
+ * Determine validation type from validator name
217
+ * Returns: 'universal', 'domain', or 'feature'
218
+ */
219
+ getValidationType(validatorName) {
220
+ const role = validatorName.replace(/^validator-(epic|story)-/, '');
221
+
222
+ // Universal validators (always applied)
223
+ const epicUniversal = ['solution-architect', 'developer', 'security'];
224
+ const storyUniversal = ['developer', 'qa', 'test-architect'];
225
+
226
+ if (epicUniversal.includes(role) || storyUniversal.includes(role)) {
227
+ return 'universal';
228
+ }
229
+
230
+ // Domain validators
231
+ const domainValidators = ['devops', 'cloud', 'backend', 'database', 'api',
232
+ 'frontend', 'ui', 'ux', 'mobile', 'data'];
233
+
234
+ if (domainValidators.includes(role)) {
235
+ return 'domain';
236
+ }
237
+
238
+ // Everything else is feature-based
239
+ return 'feature';
240
+ }
241
+
242
+ /**
243
+ * Get provider for a specific validator based on validation type
244
+ * @param {string} validatorName - Validator name (e.g., 'validator-epic-security')
245
+ * @returns {Promise<LLMProvider>} LLM provider instance
246
+ */
247
+ async getProviderForValidator(validatorName) {
248
+ const validationType = this.getValidationType(validatorName);
249
+
250
+ // Check validation-type specific configuration
251
+ const validationTypeConfig = this.validationStageConfig?.validationTypes?.[validationType];
252
+
253
+ let provider, model;
254
+
255
+ if (validationTypeConfig?.provider) {
256
+ // Use validation-type-specific config
257
+ provider = validationTypeConfig.provider;
258
+ model = validationTypeConfig.model;
259
+ } else if (this.validationStageConfig?.provider) {
260
+ // Fallback to validation stage default
261
+ provider = this.validationStageConfig.provider;
262
+ model = this.validationStageConfig.model;
263
+ } else {
264
+ // Fallback to global default (use default llmProvider)
265
+ return this.llmProvider;
266
+ }
267
+
268
+ // Check cache
269
+ const cacheKey = `${validatorName}:${provider}:${model}`;
270
+ if (this._validatorProviders[cacheKey]) {
271
+ return this._validatorProviders[cacheKey];
272
+ }
273
+
274
+ // Create new provider
275
+ const role = this.extractDomain(validatorName);
276
+ const providerInstance = await LLMProvider.create(provider, model);
277
+ if (this._tokenCallback) providerInstance.onCall((delta) => this._tokenCallback(delta, 'validation'));
278
+ if (this._promptLogger) providerInstance.setPromptLogger(this._promptLogger, `validation-${role}`);
279
+ this._validatorProviders[cacheKey] = providerInstance;
280
+
281
+ return providerInstance;
282
+ }
283
+
284
+ /**
285
+ * Get provider for contextual agent selection (agent-selector LLM call).
286
+ * Uses validation stage config; falls back to this.llmProvider.
287
+ * @returns {Promise<LLMProvider>}
288
+ */
289
+ async getProviderForSelection() {
290
+ if (this.validationStageConfig?.provider && this.validationStageConfig?.model) {
291
+ const cacheKey = `selection:${this.validationStageConfig.provider}:${this.validationStageConfig.model}`;
292
+ if (this._validatorProviders[cacheKey]) return this._validatorProviders[cacheKey];
293
+ const instance = await LLMProvider.create(this.validationStageConfig.provider, this.validationStageConfig.model);
294
+ if (this._tokenCallback) instance.onCall((delta) => this._tokenCallback(delta, 'validation'));
295
+ if (this._promptLogger) instance.setPromptLogger(this._promptLogger, 'selection');
296
+ this._validatorProviders[cacheKey] = instance;
297
+ return instance;
298
+ }
299
+ return this.llmProvider;
300
+ }
301
+
302
+ /**
303
+ * Validate an Epic via micro-checks: parallel YES/NO domain checks,
304
+ * cross-reference consistency checks, deterministic scoring, and atomic fixes.
305
+ * @param {Object} epic - Epic work.json object
306
+ * @param {string} epicContext - Epic context.md content
307
+ * @returns {Object} Aggregated validation result
308
+ */
309
+ async validateEpic(epic, epicContext) {
310
+ console.log(`\n🔍 Validating Epic (micro-checks): ${epic.name}`);
311
+ // Determine perspectives from router
312
+ let perspectives;
313
+ if (epic.metadata?.selectedValidators) {
314
+ perspectives = epic.metadata.selectedValidators.map(v => this.extractDomain(v));
315
+ } else {
316
+ const validators = this.router.getValidatorsForEpic(epic);
317
+ if (!epic.metadata) epic.metadata = {};
318
+ epic.metadata.selectedValidators = validators;
319
+ perspectives = validators.map(v => this.extractDomain(v));
320
+ }
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,
331
+ }
332
+ );
333
+ epic.metadata = epic.metadata || {};
334
+ epic.metadata.validationResult = {
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,
343
+ };
344
+ this.storeValidationFeedback(epic.id, mcResult);
345
+ return mcResult;
346
+ }
347
+
348
+ /**
349
+ * Validate a Story via micro-checks: parallel YES/NO domain checks,
350
+ * cross-reference consistency checks, deterministic scoring, and atomic fixes.
351
+ * @param {Object} story - Story work.json object
352
+ * @param {string} storyContext - Story context.md content
353
+ * @param {Object} epic - Parent epic for routing
354
+ * @returns {Object} Aggregated validation result
355
+ */
356
+ async validateStory(story, storyContext, epic) {
357
+ console.log(`\n🔍 Validating Story (micro-checks): ${story.name}`);
358
+ let perspectives;
359
+ if (story.metadata?.selectedValidators) {
360
+ perspectives = story.metadata.selectedValidators.map(v => this.extractDomain(v));
361
+ } else {
362
+ const validators = this.router.getValidatorsForStory(story, epic);
363
+ if (!story.metadata) story.metadata = {};
364
+ story.metadata.selectedValidators = validators;
365
+ perspectives = validators.map(v => this.extractDomain(v));
366
+ }
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,
377
+ }
378
+ );
379
+ story.metadata = story.metadata || {};
380
+ story.metadata.validationResult = {
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,
389
+ };
390
+ this.storeValidationFeedback(story.id, mcResult);
391
+ return mcResult;
392
+ }
393
+
394
+ /**
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}
400
+ * @private
401
+ */
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');
414
+
415
+ return `# Story to Split
416
+
417
+ ## Parent Epic (canonical)
418
+
419
+ ${epicContextMd}
420
+
421
+ ## Original Story (too large — ${(story.acceptance || []).length} acceptance criteria)
422
+
423
+ ${storyContext}
424
+
425
+ ## Validator Issues That Triggered This Split
426
+
427
+ ${issueText || 'Story exceeded the acceptance criteria size limit after multiple validation passes.'}
428
+
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
+ `;
432
+ }
433
+
434
+ /**
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>}
441
+ */
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');
446
+
447
+ const _usageBefore = provider.getTokenUsage();
448
+ const _t0 = Date.now();
449
+ console.log(`[API-START] story-splitter (story="${workingStory.name}", ACs=${(workingStory.acceptance || []).length})`);
450
+
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;
465
+ }
466
+
467
+ const _elapsed = Date.now() - _t0;
468
+ const _usageAfter = provider.getTokenUsage();
469
+ const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
470
+ const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
471
+ console.log(`[API-DONE] story-splitter — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
472
+
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;
477
+ }
478
+
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
+ }
489
+
490
+ if (this.verificationTracker) {
491
+ this.verificationTracker.recordCheck('story-splitter', 'story-splitting', true);
492
+ }
493
+
494
+ return result;
495
+ }
496
+
497
+ /**
498
+ * Aggregate multiple validation results into a single report
499
+ * @private
500
+ */
501
+ aggregateValidationResults(results, type) {
502
+ const aggregated = {
503
+ type: type, // 'epic' or 'story'
504
+ validatorCount: results.length,
505
+ validators: results.map(r => r._validatorName),
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
+ })(),
513
+
514
+ // Aggregate issues by severity
515
+ criticalIssues: [],
516
+ majorIssues: [],
517
+ minorIssues: [],
518
+
519
+ // Aggregate strengths (deduplicated)
520
+ strengths: [],
521
+
522
+ // Aggregate improvement priorities (ranked by frequency)
523
+ improvementPriorities: [],
524
+
525
+ // Per-validator results summary
526
+ validatorResults: results.map(r => ({
527
+ validator: r._validatorName,
528
+ status: r.validationStatus,
529
+ score: r.overallScore || 0,
530
+ issueCount: (r.issues || []).length
531
+ }))
532
+ };
533
+
534
+ // Collect and categorize issues
535
+ results.forEach(result => {
536
+ (result.issues || []).forEach(issue => {
537
+ const enhancedIssue = {
538
+ ...issue,
539
+ validator: result._validatorName,
540
+ domain: this.extractDomain(result._validatorName)
541
+ };
542
+
543
+ if (issue.severity === 'critical') {
544
+ aggregated.criticalIssues.push(enhancedIssue);
545
+ } else if (issue.severity === 'major') {
546
+ aggregated.majorIssues.push(enhancedIssue);
547
+ } else {
548
+ aggregated.minorIssues.push(enhancedIssue);
549
+ }
550
+ });
551
+
552
+ // Collect strengths (deduplicate similar ones)
553
+ (result.strengths || []).forEach(strength => {
554
+ if (!aggregated.strengths.some(s => this.isSimilar(s, strength))) {
555
+ aggregated.strengths.push(strength);
556
+ }
557
+ });
558
+ });
559
+
560
+ // Rank improvement priorities by frequency across validators
561
+ const priorityMap = new Map();
562
+ results.forEach(result => {
563
+ (result.improvementPriorities || []).forEach(priority => {
564
+ priorityMap.set(priority, (priorityMap.get(priority) || 0) + 1);
565
+ });
566
+ });
567
+
568
+ aggregated.improvementPriorities = Array.from(priorityMap.entries())
569
+ .sort((a, b) => b[1] - a[1]) // Sort by frequency
570
+ .slice(0, 5) // Top 5
571
+ .map(([priority, count]) => ({ priority, mentionedBy: count }));
572
+
573
+ return aggregated;
574
+ }
575
+
576
+ /**
577
+ * Determine overall status from multiple validators
578
+ * Uses "highest severity wins" approach
579
+ * @private
580
+ */
581
+ determineOverallStatus(results) {
582
+ const statuses = results.map(r => r.validationStatus);
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
+
590
+ // If any validator says "needs-improvement", overall is "needs-improvement"
591
+ if (statuses.includes('needs-improvement')) {
592
+ return 'needs-improvement';
593
+ }
594
+
595
+ // If all are "excellent", overall is "excellent"
596
+ if (statuses.every(s => s === 'excellent')) {
597
+ return 'excellent';
598
+ }
599
+
600
+ // Otherwise "acceptable"
601
+ return 'acceptable';
602
+ }
603
+
604
+ /**
605
+ * Load agent instructions from .md file
606
+ * @private
607
+ */
608
+ loadAgentInstructions(filename) {
609
+ try {
610
+ return loadAgent(filename);
611
+ } catch (err) {
612
+ throw new Error(`Agent file not found: ${filename}`);
613
+ }
614
+ }
615
+
616
+
617
+ /**
618
+ * Extract domain name from validator or solver name
619
+ * @private
620
+ */
621
+ extractDomain(validatorName) {
622
+ // Extract domain from validator/solver name
623
+ // e.g., "validator-epic-security" → "security"
624
+ // e.g., "solver-epic-security" → "security"
625
+ if (!validatorName) return 'unknown';
626
+ const match = validatorName.match(/(?:validator|solver)-(?:epic|story)-(.+)/);
627
+ return match ? match[1] : 'unknown';
628
+ }
629
+
630
+ /**
631
+ * Check if two strings are similar (for deduplication)
632
+ * @private
633
+ */
634
+ isSimilar(str1, str2) {
635
+ // Simple similarity check (can be enhanced with fuzzy matching)
636
+ const s1 = str1.toLowerCase();
637
+ const s2 = str2.toLowerCase();
638
+ return s1.includes(s2) || s2.includes(s1);
639
+ }
640
+
641
+ /**
642
+ * Store validation feedback for learning/feedback loops
643
+ * @private
644
+ */
645
+ storeValidationFeedback(workItemId, aggregatedResult) {
646
+ this.validationFeedback.set(workItemId, aggregatedResult);
647
+ }
648
+
649
+ /**
650
+ * Get validation feedback for a work item
651
+ * @param {string} workItemId - Work item ID
652
+ * @returns {Object|null} Aggregated validation result or null
653
+ */
654
+ getValidationFeedback(workItemId) {
655
+ return this.validationFeedback.get(workItemId) || null;
656
+ }
657
+ }
658
+
659
+ export { EpicStoryValidator };