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