@agile-vibe-coding/avc 0.2.3 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +475 -3
- package/cli/agents/agent-selector.md +23 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +318 -39
- package/cli/agents/mission-scope-generator.md +68 -4
- package/cli/agents/mission-scope-validator.md +40 -6
- package/cli/agents/project-context-extractor.md +21 -6
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/validator-documentation.json +31 -0
- package/cli/agents/validator-documentation.md +3 -1
- package/cli/api-reference-tool.js +368 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/epic-story-validator.js +284 -799
- package/cli/index.js +0 -0
- package/cli/init-model-config.js +17 -10
- package/cli/init.js +514 -92
- package/cli/kanban-server-manager.js +1 -2
- package/cli/llm-claude.js +98 -31
- package/cli/llm-gemini.js +29 -5
- package/cli/llm-local.js +493 -0
- package/cli/llm-openai.js +262 -41
- package/cli/llm-provider.js +147 -8
- package/cli/llm-token-limits.js +113 -4
- package/cli/llm-verifier.js +209 -1
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +3 -12
- package/cli/messaging-api.js +6 -12
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +23 -0
- package/cli/model-selector.js +3 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +106 -346
- package/cli/repl-old.js +1 -2
- package/cli/seed-processor.js +194 -24
- package/cli/sprint-planning-processor.js +2638 -289
- package/cli/template-processor.js +50 -3
- package/cli/token-tracker.js +50 -23
- package/cli/tools/generate-story-validators.js +1 -1
- package/cli/validation-router.js +70 -8
- package/cli/worktree-runner.js +654 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +2 -2
- package/kanban/client/src/App.jsx +43 -14
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
- package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
- package/kanban/client/src/components/stats/CostModal.jsx +34 -3
- package/kanban/client/src/hooks/useGrouping.js +59 -0
- package/kanban/client/src/lib/api.js +118 -4
- package/kanban/client/src/lib/status-grouping.js +10 -0
- package/kanban/client/src/store/kanbanStore.js +8 -0
- package/kanban/server/index.js +23 -2
- package/kanban/server/routes/ceremony.js +153 -4
- package/kanban/server/routes/costs.js +9 -3
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/settings.js +447 -14
- package/kanban/server/routes/websocket.js +7 -2
- package/kanban/server/routes/work-items.js +141 -1
- package/kanban/server/services/CeremonyService.js +275 -24
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +14 -6
- package/kanban/server/workers/sprint-planning-worker.js +94 -12
- package/package.json +2 -3
- package/cli/agents/solver-epic-api.json +0 -15
- package/cli/agents/solver-epic-api.md +0 -39
- package/cli/agents/solver-epic-backend.json +0 -15
- package/cli/agents/solver-epic-backend.md +0 -39
- package/cli/agents/solver-epic-cloud.json +0 -15
- package/cli/agents/solver-epic-cloud.md +0 -39
- package/cli/agents/solver-epic-data.json +0 -15
- package/cli/agents/solver-epic-data.md +0 -39
- package/cli/agents/solver-epic-database.json +0 -15
- package/cli/agents/solver-epic-database.md +0 -39
- package/cli/agents/solver-epic-developer.json +0 -15
- package/cli/agents/solver-epic-developer.md +0 -39
- package/cli/agents/solver-epic-devops.json +0 -15
- package/cli/agents/solver-epic-devops.md +0 -39
- package/cli/agents/solver-epic-frontend.json +0 -15
- package/cli/agents/solver-epic-frontend.md +0 -39
- package/cli/agents/solver-epic-mobile.json +0 -15
- package/cli/agents/solver-epic-mobile.md +0 -39
- package/cli/agents/solver-epic-qa.json +0 -15
- package/cli/agents/solver-epic-qa.md +0 -39
- package/cli/agents/solver-epic-security.json +0 -15
- package/cli/agents/solver-epic-security.md +0 -39
- package/cli/agents/solver-epic-solution-architect.json +0 -15
- package/cli/agents/solver-epic-solution-architect.md +0 -39
- package/cli/agents/solver-epic-test-architect.json +0 -15
- package/cli/agents/solver-epic-test-architect.md +0 -39
- package/cli/agents/solver-epic-ui.json +0 -15
- package/cli/agents/solver-epic-ui.md +0 -39
- package/cli/agents/solver-epic-ux.json +0 -15
- package/cli/agents/solver-epic-ux.md +0 -39
- package/cli/agents/solver-story-api.json +0 -15
- package/cli/agents/solver-story-api.md +0 -39
- package/cli/agents/solver-story-backend.json +0 -15
- package/cli/agents/solver-story-backend.md +0 -39
- package/cli/agents/solver-story-cloud.json +0 -15
- package/cli/agents/solver-story-cloud.md +0 -39
- package/cli/agents/solver-story-data.json +0 -15
- package/cli/agents/solver-story-data.md +0 -39
- package/cli/agents/solver-story-database.json +0 -15
- package/cli/agents/solver-story-database.md +0 -39
- package/cli/agents/solver-story-developer.json +0 -15
- package/cli/agents/solver-story-developer.md +0 -39
- package/cli/agents/solver-story-devops.json +0 -15
- package/cli/agents/solver-story-devops.md +0 -39
- package/cli/agents/solver-story-frontend.json +0 -15
- package/cli/agents/solver-story-frontend.md +0 -39
- package/cli/agents/solver-story-mobile.json +0 -15
- package/cli/agents/solver-story-mobile.md +0 -39
- package/cli/agents/solver-story-qa.json +0 -15
- package/cli/agents/solver-story-qa.md +0 -39
- package/cli/agents/solver-story-security.json +0 -15
- package/cli/agents/solver-story-security.md +0 -39
- package/cli/agents/solver-story-solution-architect.json +0 -15
- package/cli/agents/solver-story-solution-architect.md +0 -39
- package/cli/agents/solver-story-test-architect.json +0 -15
- package/cli/agents/solver-story-test-architect.md +0 -39
- package/cli/agents/solver-story-ui.json +0 -15
- package/cli/agents/solver-story-ui.md +0 -39
- package/cli/agents/solver-story-ux.json +0 -15
- package/cli/agents/solver-story-ux.md +0 -39
- package/cli/agents/validator-epic-api.json +0 -93
- package/cli/agents/validator-epic-api.md +0 -137
- package/cli/agents/validator-epic-backend.json +0 -93
- package/cli/agents/validator-epic-backend.md +0 -130
- package/cli/agents/validator-epic-cloud.json +0 -93
- package/cli/agents/validator-epic-cloud.md +0 -137
- package/cli/agents/validator-epic-data.json +0 -93
- package/cli/agents/validator-epic-data.md +0 -130
- package/cli/agents/validator-epic-database.json +0 -93
- package/cli/agents/validator-epic-database.md +0 -137
- package/cli/agents/validator-epic-developer.json +0 -74
- package/cli/agents/validator-epic-developer.md +0 -153
- package/cli/agents/validator-epic-devops.json +0 -74
- package/cli/agents/validator-epic-devops.md +0 -153
- package/cli/agents/validator-epic-frontend.json +0 -74
- package/cli/agents/validator-epic-frontend.md +0 -153
- package/cli/agents/validator-epic-mobile.json +0 -93
- package/cli/agents/validator-epic-mobile.md +0 -130
- package/cli/agents/validator-epic-qa.json +0 -93
- package/cli/agents/validator-epic-qa.md +0 -130
- package/cli/agents/validator-epic-security.json +0 -74
- package/cli/agents/validator-epic-security.md +0 -154
- package/cli/agents/validator-epic-solution-architect.json +0 -74
- package/cli/agents/validator-epic-solution-architect.md +0 -156
- package/cli/agents/validator-epic-test-architect.json +0 -93
- package/cli/agents/validator-epic-test-architect.md +0 -130
- package/cli/agents/validator-epic-ui.json +0 -93
- package/cli/agents/validator-epic-ui.md +0 -130
- package/cli/agents/validator-epic-ux.json +0 -93
- package/cli/agents/validator-epic-ux.md +0 -130
- package/cli/agents/validator-story-api.json +0 -104
- package/cli/agents/validator-story-api.md +0 -152
- package/cli/agents/validator-story-backend.json +0 -104
- package/cli/agents/validator-story-backend.md +0 -152
- package/cli/agents/validator-story-cloud.json +0 -104
- package/cli/agents/validator-story-cloud.md +0 -152
- package/cli/agents/validator-story-data.json +0 -104
- package/cli/agents/validator-story-data.md +0 -152
- package/cli/agents/validator-story-database.json +0 -104
- package/cli/agents/validator-story-database.md +0 -152
- package/cli/agents/validator-story-developer.json +0 -104
- package/cli/agents/validator-story-developer.md +0 -152
- package/cli/agents/validator-story-devops.json +0 -104
- package/cli/agents/validator-story-devops.md +0 -152
- package/cli/agents/validator-story-frontend.json +0 -104
- package/cli/agents/validator-story-frontend.md +0 -152
- package/cli/agents/validator-story-mobile.json +0 -104
- package/cli/agents/validator-story-mobile.md +0 -152
- package/cli/agents/validator-story-qa.json +0 -104
- package/cli/agents/validator-story-qa.md +0 -152
- package/cli/agents/validator-story-security.json +0 -104
- package/cli/agents/validator-story-security.md +0 -152
- package/cli/agents/validator-story-solution-architect.json +0 -104
- package/cli/agents/validator-story-solution-architect.md +0 -152
- package/cli/agents/validator-story-test-architect.json +0 -104
- package/cli/agents/validator-story-test-architect.md +0 -152
- package/cli/agents/validator-story-ui.json +0 -104
- package/cli/agents/validator-story-ui.md +0 -152
- package/cli/agents/validator-story-ux.json +0 -104
- package/cli/agents/validator-story-ux.md +0 -152
- package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
- package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { loadAgent } from './agent-loader.js';
|
|
7
|
+
import { validateWithMicroChecks } from './micro-check-validator.js';
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
@@ -11,10 +12,8 @@ const __dirname = path.dirname(__filename);
|
|
|
11
12
|
/**
|
|
12
13
|
* Multi-Agent Epic and Story Validator
|
|
13
14
|
*
|
|
14
|
-
* Orchestrates validation
|
|
15
|
-
*
|
|
16
|
-
* After each validator, a paired solver agent improves the epic/story if issues are found,
|
|
17
|
-
* then the same validator re-validates — up to maxIterations times.
|
|
15
|
+
* Orchestrates validation via micro-checks: 370 small YES/NO LLM calls across
|
|
16
|
+
* 15 domain perspectives, deterministic scoring, and atomic per-check fixes.
|
|
18
17
|
*/
|
|
19
18
|
class EpicStoryValidator {
|
|
20
19
|
constructor(llmProvider, verificationTracker, stagesConfig = null, useSmartSelection = false, progressCallback = null, projectContext = null) {
|
|
@@ -28,7 +27,7 @@ class EpicStoryValidator {
|
|
|
28
27
|
this.agentsPath = path.join(__dirname, 'agents');
|
|
29
28
|
this.validationFeedback = new Map();
|
|
30
29
|
|
|
31
|
-
// Store validation stage configuration
|
|
30
|
+
// Store validation stage configuration (solver stage removed — micro-checks handle fixes).
|
|
32
31
|
this.validationStageConfig = stagesConfig?.validation || null;
|
|
33
32
|
|
|
34
33
|
// Cache for validator-specific providers
|
|
@@ -42,6 +41,10 @@ class EpicStoryValidator {
|
|
|
42
41
|
|
|
43
42
|
// Per-call token callback (propagated to all created providers)
|
|
44
43
|
this._tokenCallback = null;
|
|
44
|
+
|
|
45
|
+
// Root project context.md string — prepended to all validation prompts when set
|
|
46
|
+
this.rootContextMd = null;
|
|
47
|
+
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/**
|
|
@@ -53,6 +56,136 @@ class EpicStoryValidator {
|
|
|
53
56
|
this._tokenCallback = fn;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Attach a PromptLogger so all providers created by this validator write payloads.
|
|
61
|
+
* @param {import('./prompt-logger.js').PromptLogger} logger
|
|
62
|
+
*/
|
|
63
|
+
setPromptLogger(logger) {
|
|
64
|
+
this._promptLogger = logger;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set the root project context.md string — prepended to all validation prompts.
|
|
69
|
+
* @param {string} md - Canonical root context markdown
|
|
70
|
+
*/
|
|
71
|
+
setRootContextMd(md) {
|
|
72
|
+
this.rootContextMd = md;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set callback invoked when a validator call fails with a quota/rate-limit error.
|
|
77
|
+
* Signature: async ({ validatorName, errMsg, provider, model }) => { newProvider?, newModel? }
|
|
78
|
+
* Returning { newProvider, newModel } switches the validation+solver stage config.
|
|
79
|
+
* Returning null/undefined retries with the same model.
|
|
80
|
+
*/
|
|
81
|
+
setQuotaExceededCallback(fn) {
|
|
82
|
+
this._quotaExceededCallback = fn;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Update validation stage config to a new provider/model and clear provider cache.
|
|
87
|
+
* Called after user selects "Switch Provider" in the quota-limit dialog.
|
|
88
|
+
*/
|
|
89
|
+
_updateValidationStageConfig(newProvider, newModel) {
|
|
90
|
+
const oldProvider = this.validationStageConfig?.provider;
|
|
91
|
+
const oldModel = this.validationStageConfig?.model;
|
|
92
|
+
if (!this.validationStageConfig) this.validationStageConfig = {};
|
|
93
|
+
this.validationStageConfig.provider = newProvider;
|
|
94
|
+
this.validationStageConfig.model = newModel;
|
|
95
|
+
// Surgical cache invalidation: only clear entries matching the old provider/model
|
|
96
|
+
// so providers that were already on the new target are preserved
|
|
97
|
+
if (oldProvider || oldModel) {
|
|
98
|
+
for (const [key, prov] of Object.entries(this._validatorProviders)) {
|
|
99
|
+
if (prov?._providerName === oldProvider || prov?._modelName === oldModel) {
|
|
100
|
+
delete this._validatorProviders[key];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// No previous config — clear everything
|
|
105
|
+
this._validatorProviders = {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns true if the error message indicates a quota or persistent rate-limit failure.
|
|
111
|
+
*/
|
|
112
|
+
_isQuotaOrRateLimit(errMsg) {
|
|
113
|
+
const m = (errMsg || '').toLowerCase();
|
|
114
|
+
return m.includes('429') || m.includes('quota') || m.includes('rate limit') ||
|
|
115
|
+
m.includes('resource exhausted') || m.includes('resource_exhausted') ||
|
|
116
|
+
m.includes('too many requests') ||
|
|
117
|
+
// Anthropic credit-balance exhausted (400 invalid_request_error)
|
|
118
|
+
m.includes('credit balance is too low') || m.includes('credit balance') ||
|
|
119
|
+
m.includes('billing') || m.includes('insufficient_quota');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generate canonical context.md string for an epic from its JSON fields.
|
|
124
|
+
* This is the bounded, structured format passed to validators and solvers.
|
|
125
|
+
* @param {Object} epic
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
generateEpicContextMd(epic) {
|
|
129
|
+
const features = (epic.features || []).map(f => `- ${f}`).join('\n') || '- (none)';
|
|
130
|
+
const deps = epic.dependencies || [];
|
|
131
|
+
const optional = deps.filter(d => /optional/i.test(d));
|
|
132
|
+
const required = deps.filter(d => !/optional/i.test(d));
|
|
133
|
+
const reqLines = required.length ? required.map(d => `- ${d}`).join('\n') : '- (none)';
|
|
134
|
+
const storyCount = (epic.stories || []).length;
|
|
135
|
+
const lines = [
|
|
136
|
+
`# Epic: ${epic.name}`,
|
|
137
|
+
``,
|
|
138
|
+
`## Identity`,
|
|
139
|
+
`- id: ${epic.id || '(pending)'}`,
|
|
140
|
+
`- domain: ${epic.domain}`,
|
|
141
|
+
`- stories: ${storyCount}`,
|
|
142
|
+
``,
|
|
143
|
+
`## Summary`,
|
|
144
|
+
epic.description || '(no description)',
|
|
145
|
+
``,
|
|
146
|
+
`## Features`,
|
|
147
|
+
features,
|
|
148
|
+
``,
|
|
149
|
+
`## Dependencies`,
|
|
150
|
+
``,
|
|
151
|
+
`### Required`,
|
|
152
|
+
reqLines,
|
|
153
|
+
];
|
|
154
|
+
if (optional.length) {
|
|
155
|
+
lines.push('', '### Optional');
|
|
156
|
+
optional.forEach(d => lines.push(`- ${d}`));
|
|
157
|
+
}
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate canonical context.md string for a story from its JSON fields.
|
|
163
|
+
* @param {Object} story
|
|
164
|
+
* @param {Object} epic - Parent epic for identity context
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
generateStoryContextMd(story, epic) {
|
|
168
|
+
const ac = (story.acceptance || []).map((a, i) => `${i + 1}. ${a}`).join('\n') || '1. (none)';
|
|
169
|
+
const deps = (story.dependencies || []).map(d => `- ${d}`).join('\n') || '- (none)';
|
|
170
|
+
return [
|
|
171
|
+
`# Story: ${story.name}`,
|
|
172
|
+
``,
|
|
173
|
+
`## Identity`,
|
|
174
|
+
`- id: ${story.id || '(pending)'}`,
|
|
175
|
+
`- epic: ${epic.id || '(pending)'} (${epic.name})`,
|
|
176
|
+
`- userType: ${story.userType || 'team member'}`,
|
|
177
|
+
``,
|
|
178
|
+
`## Summary`,
|
|
179
|
+
story.description || '(no description)',
|
|
180
|
+
``,
|
|
181
|
+
`## Acceptance Criteria`,
|
|
182
|
+
ac,
|
|
183
|
+
``,
|
|
184
|
+
`## Dependencies`,
|
|
185
|
+
deps,
|
|
186
|
+
].join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
56
189
|
/** Emit a Level-3 detail line to the UI (fire-and-forget safe) */
|
|
57
190
|
async _detail(msg) {
|
|
58
191
|
await this.progressCallback?.(null, null, { detail: msg });
|
|
@@ -139,31 +272,15 @@ class EpicStoryValidator {
|
|
|
139
272
|
}
|
|
140
273
|
|
|
141
274
|
// Create new provider
|
|
275
|
+
const role = this.extractDomain(validatorName);
|
|
142
276
|
const providerInstance = await LLMProvider.create(provider, model);
|
|
143
277
|
if (this._tokenCallback) providerInstance.onCall((delta) => this._tokenCallback(delta, 'validation'));
|
|
278
|
+
if (this._promptLogger) providerInstance.setPromptLogger(this._promptLogger, `validation-${role}`);
|
|
144
279
|
this._validatorProviders[cacheKey] = providerInstance;
|
|
145
280
|
|
|
146
281
|
return providerInstance;
|
|
147
282
|
}
|
|
148
283
|
|
|
149
|
-
/**
|
|
150
|
-
* Get provider for a specific solver based on solver stage config
|
|
151
|
-
* @param {string} role - Role name (e.g., 'security')
|
|
152
|
-
* @returns {Promise<LLMProvider>} LLM provider instance
|
|
153
|
-
*/
|
|
154
|
-
async getProviderForSolver(role) {
|
|
155
|
-
const solverConfig = this.validationStageConfig?.solver;
|
|
156
|
-
if (!solverConfig?.provider) return this.llmProvider;
|
|
157
|
-
|
|
158
|
-
const cacheKey = `solver:${solverConfig.provider}:${solverConfig.model}`;
|
|
159
|
-
if (this._validatorProviders[cacheKey]) return this._validatorProviders[cacheKey];
|
|
160
|
-
|
|
161
|
-
const instance = await LLMProvider.create(solverConfig.provider, solverConfig.model);
|
|
162
|
-
if (this._tokenCallback) instance.onCall((delta) => this._tokenCallback(delta, 'solver'));
|
|
163
|
-
this._validatorProviders[cacheKey] = instance;
|
|
164
|
-
return instance;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
284
|
/**
|
|
168
285
|
* Get provider for contextual agent selection (agent-selector LLM call).
|
|
169
286
|
* Uses validation stage config; falls back to this.llmProvider.
|
|
@@ -175,6 +292,7 @@ class EpicStoryValidator {
|
|
|
175
292
|
if (this._validatorProviders[cacheKey]) return this._validatorProviders[cacheKey];
|
|
176
293
|
const instance = await LLMProvider.create(this.validationStageConfig.provider, this.validationStageConfig.model);
|
|
177
294
|
if (this._tokenCallback) instance.onCall((delta) => this._tokenCallback(delta, 'validation'));
|
|
295
|
+
if (this._promptLogger) instance.setPromptLogger(this._promptLogger, 'selection');
|
|
178
296
|
this._validatorProviders[cacheKey] = instance;
|
|
179
297
|
return instance;
|
|
180
298
|
}
|
|
@@ -182,780 +300,198 @@ class EpicStoryValidator {
|
|
|
182
300
|
}
|
|
183
301
|
|
|
184
302
|
/**
|
|
185
|
-
* Validate an Epic
|
|
186
|
-
*
|
|
187
|
-
* a paired solver improves the epic before the next validator runs.
|
|
303
|
+
* Validate an Epic via micro-checks: parallel YES/NO domain checks,
|
|
304
|
+
* cross-reference consistency checks, deterministic scoring, and atomic fixes.
|
|
188
305
|
* @param {Object} epic - Epic work.json object
|
|
189
306
|
* @param {string} epicContext - Epic context.md content
|
|
190
307
|
* @returns {Object} Aggregated validation result
|
|
191
308
|
*/
|
|
192
309
|
async validateEpic(epic, epicContext) {
|
|
193
|
-
|
|
194
|
-
|
|
310
|
+
console.log(`\n🔍 Validating Epic (micro-checks): ${epic.name}`);
|
|
311
|
+
// Determine perspectives from router
|
|
312
|
+
let perspectives;
|
|
195
313
|
if (epic.metadata?.selectedValidators) {
|
|
196
|
-
|
|
197
|
-
console.log(` Using cached validator selection (${validators.length} validators)`);
|
|
314
|
+
perspectives = epic.metadata.selectedValidators.map(v => this.extractDomain(v));
|
|
198
315
|
} else {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
console.log(`[DEBUG] Epic validator selection: useContextualSelection=${useContextualSelection}, validationStageConfig=${JSON.stringify(this.validationStageConfig)}`);
|
|
202
|
-
if (useContextualSelection) {
|
|
203
|
-
const selectionProvider = await this.getProviderForSelection();
|
|
204
|
-
validators = await this.router.selectValidatorsWithContext(epic, 'epic', selectionProvider);
|
|
205
|
-
} else if (this.useSmartSelection) {
|
|
206
|
-
validators = await this.router.getValidatorsForEpicWithLLM(epic);
|
|
207
|
-
} else {
|
|
208
|
-
validators = this.router.getValidatorsForEpic(epic);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Cache selection in metadata
|
|
212
|
-
if (!epic.metadata) {
|
|
213
|
-
epic.metadata = {};
|
|
214
|
-
}
|
|
316
|
+
const validators = this.router.getValidatorsForEpic(epic);
|
|
317
|
+
if (!epic.metadata) epic.metadata = {};
|
|
215
318
|
epic.metadata.selectedValidators = validators;
|
|
319
|
+
perspectives = validators.map(v => this.extractDomain(v));
|
|
216
320
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
await this._detail(`${validators.length} validator${validators.length !== 1 ? 's' : ''}: ${validators.map(v => this.extractDomain(v)).join(', ')}`);
|
|
228
|
-
|
|
229
|
-
// Working copy — accumulates improvements from solver runs
|
|
230
|
-
// Keep stories array untouched; solver only modifies epic-level fields
|
|
231
|
-
let workingEpic = { ...epic };
|
|
232
|
-
|
|
233
|
-
// ── Phase 1: run all validators in parallel on the initial snapshot ──────────
|
|
234
|
-
await this._detail(`Running ${validators.length} validators in parallel…`);
|
|
235
|
-
console.log(` Running ${validators.length} validators in parallel…`);
|
|
236
|
-
|
|
237
|
-
const _t0Parallel = Date.now();
|
|
238
|
-
const parallelResults = await Promise.all(
|
|
239
|
-
validators.map((validatorName, vi) => {
|
|
240
|
-
const role = this.extractDomain(validatorName);
|
|
241
|
-
return this._withHeartbeat(
|
|
242
|
-
() => this.runEpicValidator(workingEpic, epicContext, validatorName),
|
|
243
|
-
(elapsed) => {
|
|
244
|
-
if (elapsed < 20) return ` [${role}] reviewing requirements…`;
|
|
245
|
-
if (elapsed < 40) return ` [${role}] analyzing concerns…`;
|
|
246
|
-
if (elapsed < 60) return ` [${role}] checking best practices…`;
|
|
247
|
-
return ` [${role}] still validating…`;
|
|
248
|
-
},
|
|
249
|
-
10000
|
|
250
|
-
).catch(err => {
|
|
251
|
-
console.warn(` ⚠ Validator ${validatorName} failed: ${err.message.split('\n')[0]}`);
|
|
252
|
-
return { overallScore: 0, validationStatus: 'error', issues: [], _validatorError: err.message.split('\n')[0] };
|
|
253
|
-
});
|
|
254
|
-
})
|
|
255
|
-
);
|
|
256
|
-
console.log(`[TIMING] epic parallel batch (${validators.length} validators): ${Date.now() - _t0Parallel}ms`);
|
|
257
|
-
|
|
258
|
-
// Log all parallel results
|
|
259
|
-
for (let vi = 0; vi < validators.length; vi++) {
|
|
260
|
-
const validatorName = validators[vi];
|
|
261
|
-
const role = this.extractDomain(validatorName);
|
|
262
|
-
const result = parallelResults[vi];
|
|
263
|
-
const score = result.overallScore ?? 0;
|
|
264
|
-
const allIssues = result.issues || [];
|
|
265
|
-
const critCount = allIssues.filter(i => i.severity === 'critical').length;
|
|
266
|
-
const majCount = allIssues.filter(i => i.severity === 'major').length;
|
|
267
|
-
const acceptable = score >= acceptanceThreshold;
|
|
268
|
-
const issueStr = allIssues.length > 0 ? ` · ${allIssues.length} issue${allIssues.length !== 1 ? 's' : ''}` : '';
|
|
269
|
-
await this._detail(`[${vi + 1}/${validators.length}] ${role}: ${score}/100${issueStr} — ${acceptable ? '✓' : '⚠ below threshold'}`);
|
|
270
|
-
console.log(` [${vi + 1}/${validators.length}] ${validatorName} score=${score}/100 status=${result.validationStatus} (critical=${critCount} major=${majCount} minor=${allIssues.length - critCount - majCount})`);
|
|
271
|
-
allIssues.forEach(issue => {
|
|
272
|
-
const cat = issue.category ? `[${issue.category}] ` : '';
|
|
273
|
-
const sug = issue.suggestion ? ` → ${issue.suggestion}` : '';
|
|
274
|
-
console.log(` [${(issue.severity || 'unknown').toUpperCase()}] ${cat}${issue.description || '(no description)'}${sug}`);
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── Phase 2: parallel solver rounds ──────────────────────────────────────────
|
|
279
|
-
const finalResults = [...parallelResults];
|
|
280
|
-
|
|
281
|
-
// Indices of validators still below threshold
|
|
282
|
-
let needsWork = validators.map((_, vi) => vi).filter(vi =>
|
|
283
|
-
(parallelResults[vi].overallScore ?? 0) < acceptanceThreshold
|
|
284
|
-
);
|
|
285
|
-
if (needsWork.length > 0) {
|
|
286
|
-
console.log(` ${needsWork.length}/${validators.length} validators below threshold — running parallel solver rounds (max ${maxIterations - 1})`);
|
|
287
|
-
await this._detail(`${needsWork.length} below threshold — parallel solver rounds`);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
for (let iter = 1; iter < maxIterations && needsWork.length > 0; iter++) {
|
|
291
|
-
const roundCount = needsWork.length;
|
|
292
|
-
await this._detail(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} in parallel…`);
|
|
293
|
-
console.log(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} running in parallel…`);
|
|
294
|
-
const _t0Round = Date.now();
|
|
295
|
-
|
|
296
|
-
// 1. Run all below-threshold solvers in parallel (all see the same workingEpic snapshot)
|
|
297
|
-
const solverResults = await Promise.all(
|
|
298
|
-
needsWork.map(vi => {
|
|
299
|
-
const validatorName = validators[vi];
|
|
300
|
-
const role = this.extractDomain(validatorName);
|
|
301
|
-
return this._withHeartbeat(
|
|
302
|
-
() => this.runEpicSolver(workingEpic, epicContext, finalResults[vi], validatorName),
|
|
303
|
-
(elapsed) => {
|
|
304
|
-
if (elapsed < 25) return ` ↻ ${role} solver — applying improvements…`;
|
|
305
|
-
if (elapsed < 50) return ` ↻ ${role} solver — refining epic quality…`;
|
|
306
|
-
return ` ↻ ${role} solver — still running…`;
|
|
307
|
-
},
|
|
308
|
-
20000
|
|
309
|
-
).then(improved => ({ vi, improved, error: null }))
|
|
310
|
-
.catch(err => ({ vi, improved: null, error: err }));
|
|
311
|
-
})
|
|
312
|
-
);
|
|
313
|
-
console.log(`[TIMING] Phase 2 round ${iter} solvers: ${Date.now() - _t0Round}ms`);
|
|
314
|
-
|
|
315
|
-
// 2. Merge all solver results — preserve base items, add up to 3 new items per solver
|
|
316
|
-
// Base items are anchored (never dropped); each solver contributes at most 3 genuinely new features.
|
|
317
|
-
// Description taken from the solver whose validator scored lowest (most informed fix).
|
|
318
|
-
const baseFeatures = workingEpic.features || [];
|
|
319
|
-
const baseFeaturesSet = new Set(baseFeatures);
|
|
320
|
-
const newFeatureAdditions = []; // items new beyond base, insertion-ordered, deduplicated
|
|
321
|
-
const allDeps = new Set(workingEpic.dependencies || []);
|
|
322
|
-
let bestDescription = workingEpic.description;
|
|
323
|
-
let worstValidatorScore = Infinity;
|
|
324
|
-
let anyImproved = false;
|
|
325
|
-
|
|
326
|
-
for (const { vi, improved, error } of solverResults) {
|
|
327
|
-
const validatorName = validators[vi];
|
|
328
|
-
const role = this.extractDomain(validatorName);
|
|
329
|
-
if (error) {
|
|
330
|
-
console.warn(` ⚠ Solver failed for ${validatorName}: ${error.message} — keeping current epic`);
|
|
331
|
-
await this._detail(` ⚠ Solver failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
if (improved && improved.id === workingEpic.id) {
|
|
335
|
-
anyImproved = true;
|
|
336
|
-
// Collect only new items not already in base or pending additions (max 3 per solver)
|
|
337
|
-
const existingAll = new Set([...baseFeaturesSet, ...newFeatureAdditions]);
|
|
338
|
-
const solverNew = (improved.features || []).filter(f => !existingAll.has(f)).slice(0, 3);
|
|
339
|
-
newFeatureAdditions.push(...solverNew);
|
|
340
|
-
(improved.dependencies || []).forEach(d => allDeps.add(d));
|
|
341
|
-
// Description: take from the validator with the lowest score (most dissatisfied perspective)
|
|
342
|
-
const vScore = finalResults[vi]?.overallScore ?? 100;
|
|
343
|
-
if (improved.description && improved.description !== workingEpic.description && vScore < worstValidatorScore) {
|
|
344
|
-
worstValidatorScore = vScore;
|
|
345
|
-
bestDescription = improved.description;
|
|
346
|
-
}
|
|
347
|
-
console.log(` ↻ [${role}] solver merged — ${solverNew.length} new features added`);
|
|
348
|
-
await this._detail(` → [${role}] improvements applied`);
|
|
349
|
-
} else {
|
|
350
|
-
console.log(` ↻ [${role}] solver returned no valid improvement (id mismatch or empty)`);
|
|
351
|
-
}
|
|
321
|
+
const workItemText = epicContext || this.generateEpicContextMd(epic);
|
|
322
|
+
const mcResult = await validateWithMicroChecks(
|
|
323
|
+
epic, workItemText, 'epic', perspectives, this.llmProvider,
|
|
324
|
+
{
|
|
325
|
+
concurrency: this.validationStageConfig?.concurrency ?? 5,
|
|
326
|
+
batchSize: this.validationStageConfig?.batchSize ?? 8,
|
|
327
|
+
maxFixAttempts: this.validationStageConfig?.maxFixAttempts ?? 3,
|
|
328
|
+
generateContextFn: (wi) => this.generateEpicContextMd(wi),
|
|
329
|
+
progressCallback: this.progressCallback,
|
|
330
|
+
projectRoot: this.projectContext?.projectRoot,
|
|
352
331
|
}
|
|
353
|
-
|
|
354
|
-
if (anyImproved) {
|
|
355
|
-
// Base items always preserved first; cap only the additions portion
|
|
356
|
-
const merged = [...baseFeatures, ...newFeatureAdditions];
|
|
357
|
-
const hardCap = baseFeatures.length + 12; // allow up to 12 new features beyond original
|
|
358
|
-
const finalFeatures = merged.length > hardCap ? merged.slice(0, hardCap) : merged;
|
|
359
|
-
if (merged.length > hardCap) {
|
|
360
|
-
console.log(` ↻ Epic features capped after merge: ${merged.length} → ${hardCap} (base=${baseFeatures.length} + new=${finalFeatures.length - baseFeatures.length})`);
|
|
361
|
-
}
|
|
362
|
-
const descBefore = (workingEpic.description || '').slice(0, 100);
|
|
363
|
-
workingEpic = {
|
|
364
|
-
...workingEpic,
|
|
365
|
-
description: bestDescription,
|
|
366
|
-
features: finalFeatures,
|
|
367
|
-
dependencies: [...allDeps],
|
|
368
|
-
};
|
|
369
|
-
const descAfter = (workingEpic.description || '').slice(0, 100);
|
|
370
|
-
console.log(` ↻ Parallel merge applied — desc changed: ${descBefore !== descAfter}, features: ${finalFeatures.length} (base=${baseFeatures.length} + new=${newFeatureAdditions.length})`);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// 3. Re-validate all in parallel
|
|
374
|
-
await this._detail(` Re-validating ${roundCount} validator${roundCount !== 1 ? 's' : ''} in parallel (iter ${iter + 1})…`);
|
|
375
|
-
const _t0Revalidate = Date.now();
|
|
376
|
-
const revalidateResults = await Promise.all(
|
|
377
|
-
needsWork.map(vi => {
|
|
378
|
-
const validatorName = validators[vi];
|
|
379
|
-
const role = this.extractDomain(validatorName);
|
|
380
|
-
return this._withHeartbeat(
|
|
381
|
-
() => this.runEpicValidator(workingEpic, epicContext, validatorName),
|
|
382
|
-
(elapsed) => {
|
|
383
|
-
if (elapsed < 20) return ` [${role}] re-reviewing…`;
|
|
384
|
-
if (elapsed < 40) return ` [${role}] re-analyzing…`;
|
|
385
|
-
return ` [${role}] re-validating…`;
|
|
386
|
-
},
|
|
387
|
-
10000
|
|
388
|
-
).then(result => ({ vi, result, error: null }))
|
|
389
|
-
.catch(err => ({ vi, result: null, error: err }));
|
|
390
|
-
})
|
|
391
|
-
);
|
|
392
|
-
console.log(`[TIMING] Phase 2 round ${iter} re-validates: ${Date.now() - _t0Revalidate}ms`);
|
|
393
|
-
|
|
394
|
-
// 4. Update finalResults; determine which validators need another round
|
|
395
|
-
const nextNeedsWork = [];
|
|
396
|
-
for (const { vi, result, error } of revalidateResults) {
|
|
397
|
-
const validatorName = validators[vi];
|
|
398
|
-
const role = this.extractDomain(validatorName);
|
|
399
|
-
if (error) {
|
|
400
|
-
console.warn(` ⚠ Re-validator ${validatorName} failed: ${error.message.split('\n')[0]}`);
|
|
401
|
-
await this._detail(` ⚠ Re-validate failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
|
|
402
|
-
continue; // keep finalResults[vi] as-is; give up on this validator
|
|
403
|
-
}
|
|
404
|
-
finalResults[vi] = result;
|
|
405
|
-
const newScore = result.overallScore ?? 0;
|
|
406
|
-
const acceptable = newScore >= acceptanceThreshold;
|
|
407
|
-
const issueStr2 = (result.issues || []).length > 0 ? ` · ${result.issues.length} issues` : '';
|
|
408
|
-
await this._detail(` [${role}] iter ${iter + 1}: ${newScore}/100${issueStr2} — ${acceptable ? '✓ accepted' : `⚠ below threshold (${acceptanceThreshold})`}`);
|
|
409
|
-
console.log(` [${vi + 1}/${validators.length}] ${validatorName} iter=${iter + 1} score=${newScore}/100 status=${result.validationStatus}`);
|
|
410
|
-
if (!acceptable) {
|
|
411
|
-
nextNeedsWork.push(vi);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
console.log(`[TIMING] Phase 2 round ${iter} total: ${Date.now() - _t0Round}ms — ${nextNeedsWork.length} still need work`);
|
|
415
|
-
needsWork = nextNeedsWork;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const validationResults = finalResults;
|
|
419
|
-
|
|
420
|
-
// Write accumulated improvements back to the original epic object
|
|
421
|
-
// (sprint-planning-processor owns the reference; this mutates it in place)
|
|
422
|
-
epic.description = workingEpic.description;
|
|
423
|
-
epic.features = workingEpic.features;
|
|
424
|
-
epic.dependencies = workingEpic.dependencies;
|
|
425
|
-
|
|
426
|
-
// 3. Aggregate results
|
|
427
|
-
const aggregated = this.aggregateValidationResults(validationResults, 'epic');
|
|
428
|
-
|
|
429
|
-
// 4. Determine overall status
|
|
430
|
-
aggregated.overallStatus = this.determineOverallStatus(validationResults);
|
|
431
|
-
aggregated.readyToPublish = aggregated.overallStatus !== 'needs-improvement';
|
|
432
|
-
|
|
433
|
-
await this._detail(`Overall: ${aggregated.readyToPublish ? '✓ passed' : '⚠ needs improvement'} · avg ${aggregated.averageScore}/100`);
|
|
434
|
-
console.log(` Epic "${epic.name}" summary: avg=${aggregated.averageScore}/100 readyToPublish=${aggregated.readyToPublish} critical=${aggregated.criticalIssues.length} major=${aggregated.majorIssues.length}`);
|
|
435
|
-
aggregated.validatorResults.forEach(vr => {
|
|
436
|
-
console.log(` ${this.extractDomain(vr.validator)}: ${vr.score}/100 (${vr.status})`);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// 5. Store for feedback loop
|
|
440
|
-
this.storeValidationFeedback(epic.id, aggregated);
|
|
441
|
-
|
|
442
|
-
// 6. Persist result into epic metadata so sprint-planning-processor writes it to work.json
|
|
332
|
+
);
|
|
443
333
|
epic.metadata = epic.metadata || {};
|
|
444
334
|
epic.metadata.validationResult = {
|
|
445
|
-
averageScore:
|
|
446
|
-
overallStatus:
|
|
447
|
-
readyToPublish:
|
|
448
|
-
criticalIssues:
|
|
449
|
-
majorIssues:
|
|
450
|
-
minorIssues:
|
|
451
|
-
validatorResults:
|
|
452
|
-
validatedAt:
|
|
335
|
+
averageScore: mcResult.averageScore,
|
|
336
|
+
overallStatus: mcResult.overallStatus,
|
|
337
|
+
readyToPublish: mcResult.readyToPublish,
|
|
338
|
+
criticalIssues: mcResult.criticalIssues,
|
|
339
|
+
majorIssues: mcResult.majorIssues,
|
|
340
|
+
minorIssues: mcResult.minorIssues,
|
|
341
|
+
validatorResults: mcResult.validatorResults,
|
|
342
|
+
validatedAt: mcResult.validatedAt,
|
|
453
343
|
};
|
|
454
|
-
|
|
455
|
-
return
|
|
344
|
+
this.storeValidationFeedback(epic.id, mcResult);
|
|
345
|
+
return mcResult;
|
|
456
346
|
}
|
|
457
347
|
|
|
458
348
|
/**
|
|
459
|
-
* Validate a Story
|
|
460
|
-
*
|
|
461
|
-
* a paired solver improves the story before the next validator runs.
|
|
349
|
+
* Validate a Story via micro-checks: parallel YES/NO domain checks,
|
|
350
|
+
* cross-reference consistency checks, deterministic scoring, and atomic fixes.
|
|
462
351
|
* @param {Object} story - Story work.json object
|
|
463
352
|
* @param {string} storyContext - Story context.md content
|
|
464
353
|
* @param {Object} epic - Parent epic for routing
|
|
465
354
|
* @returns {Object} Aggregated validation result
|
|
466
355
|
*/
|
|
467
356
|
async validateStory(story, storyContext, epic) {
|
|
468
|
-
|
|
469
|
-
let
|
|
357
|
+
console.log(`\n🔍 Validating Story (micro-checks): ${story.name}`);
|
|
358
|
+
let perspectives;
|
|
470
359
|
if (story.metadata?.selectedValidators) {
|
|
471
|
-
|
|
472
|
-
console.log(` Using cached validator selection (${validators.length} validators)`);
|
|
360
|
+
perspectives = story.metadata.selectedValidators.map(v => this.extractDomain(v));
|
|
473
361
|
} else {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (useContextualSelection) {
|
|
477
|
-
const selectionProvider = await this.getProviderForSelection();
|
|
478
|
-
validators = await this.router.selectValidatorsWithContext(story, 'story', selectionProvider, epic);
|
|
479
|
-
} else if (this.useSmartSelection) {
|
|
480
|
-
validators = await this.router.getValidatorsForStoryWithLLM(story, epic);
|
|
481
|
-
} else {
|
|
482
|
-
validators = this.router.getValidatorsForStory(story, epic);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Cache selection in metadata
|
|
486
|
-
if (!story.metadata) {
|
|
487
|
-
story.metadata = {};
|
|
488
|
-
}
|
|
362
|
+
const validators = this.router.getValidatorsForStory(story, epic);
|
|
363
|
+
if (!story.metadata) story.metadata = {};
|
|
489
364
|
story.metadata.selectedValidators = validators;
|
|
365
|
+
perspectives = validators.map(v => this.extractDomain(v));
|
|
490
366
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
await this._detail(`${validators.length} validator${validators.length !== 1 ? 's' : ''}: ${validators.map(v => this.extractDomain(v)).join(', ')}`);
|
|
502
|
-
|
|
503
|
-
// Working copy — accumulates improvements from solver runs
|
|
504
|
-
let workingStory = { ...story };
|
|
505
|
-
|
|
506
|
-
// ── Phase 1: run all validators in parallel on the initial snapshot ──────────
|
|
507
|
-
await this._detail(`Running ${validators.length} validators in parallel…`);
|
|
508
|
-
console.log(` Running ${validators.length} validators in parallel…`);
|
|
509
|
-
|
|
510
|
-
const _t0Parallel = Date.now();
|
|
511
|
-
const parallelResults = await Promise.all(
|
|
512
|
-
validators.map((validatorName, vi) => {
|
|
513
|
-
const role = this.extractDomain(validatorName);
|
|
514
|
-
return this._withHeartbeat(
|
|
515
|
-
() => this.runStoryValidator(workingStory, storyContext, epic, validatorName),
|
|
516
|
-
(elapsed) => {
|
|
517
|
-
if (elapsed < 20) return ` [${role}] reviewing story…`;
|
|
518
|
-
if (elapsed < 40) return ` [${role}] checking acceptance criteria…`;
|
|
519
|
-
if (elapsed < 60) return ` [${role}] validating scope…`;
|
|
520
|
-
return ` [${role}] still validating…`;
|
|
521
|
-
},
|
|
522
|
-
10000
|
|
523
|
-
).catch(err => {
|
|
524
|
-
console.warn(` ⚠ Validator ${validatorName} failed: ${err.message.split('\n')[0]}`);
|
|
525
|
-
return { overallScore: 0, validationStatus: 'error', issues: [], _validatorError: err.message.split('\n')[0] };
|
|
526
|
-
});
|
|
527
|
-
})
|
|
528
|
-
);
|
|
529
|
-
console.log(`[TIMING] story parallel batch (${validators.length} validators): ${Date.now() - _t0Parallel}ms`);
|
|
530
|
-
|
|
531
|
-
// Log all parallel results
|
|
532
|
-
for (let vi = 0; vi < validators.length; vi++) {
|
|
533
|
-
const validatorName = validators[vi];
|
|
534
|
-
const role = this.extractDomain(validatorName);
|
|
535
|
-
const result = parallelResults[vi];
|
|
536
|
-
const score = result.overallScore ?? 0;
|
|
537
|
-
const allIssues = result.issues || [];
|
|
538
|
-
const critCount = allIssues.filter(i => i.severity === 'critical').length;
|
|
539
|
-
const majCount = allIssues.filter(i => i.severity === 'major').length;
|
|
540
|
-
const acceptable = score >= acceptanceThreshold;
|
|
541
|
-
const issueStr = allIssues.length > 0 ? ` · ${allIssues.length} issue${allIssues.length !== 1 ? 's' : ''}` : '';
|
|
542
|
-
await this._detail(`[${vi + 1}/${validators.length}] ${role}: ${score}/100${issueStr} — ${acceptable ? '✓' : '⚠ below threshold'}`);
|
|
543
|
-
console.log(` [${vi + 1}/${validators.length}] ${validatorName} score=${score}/100 status=${result.validationStatus} (critical=${critCount} major=${majCount} minor=${allIssues.length - critCount - majCount})`);
|
|
544
|
-
allIssues.forEach(issue => {
|
|
545
|
-
const cat = issue.category ? `[${issue.category}] ` : '';
|
|
546
|
-
const sug = issue.suggestion ? ` → ${issue.suggestion}` : '';
|
|
547
|
-
console.log(` [${(issue.severity || 'unknown').toUpperCase()}] ${cat}${issue.description || '(no description)'}${sug}`);
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ── Phase 2: parallel solver rounds ──────────────────────────────────────────
|
|
552
|
-
const finalResults = [...parallelResults];
|
|
553
|
-
|
|
554
|
-
// Indices of validators still below threshold
|
|
555
|
-
let needsWork = validators.map((_, vi) => vi).filter(vi =>
|
|
556
|
-
(parallelResults[vi].overallScore ?? 0) < acceptanceThreshold
|
|
557
|
-
);
|
|
558
|
-
if (needsWork.length > 0) {
|
|
559
|
-
console.log(` ${needsWork.length}/${validators.length} validators below threshold — running parallel solver rounds (max ${maxIterations - 1})`);
|
|
560
|
-
await this._detail(`${needsWork.length} below threshold — parallel solver rounds`);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
for (let iter = 1; iter < maxIterations && needsWork.length > 0; iter++) {
|
|
564
|
-
const roundCount = needsWork.length;
|
|
565
|
-
await this._detail(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} in parallel…`);
|
|
566
|
-
console.log(` ↻ Round ${iter}/${maxIterations - 1}: ${roundCount} solver${roundCount !== 1 ? 's' : ''} running in parallel…`);
|
|
567
|
-
const _t0Round = Date.now();
|
|
568
|
-
|
|
569
|
-
// 1. Run all below-threshold solvers in parallel (all see the same workingStory snapshot)
|
|
570
|
-
const solverResults = await Promise.all(
|
|
571
|
-
needsWork.map(vi => {
|
|
572
|
-
const validatorName = validators[vi];
|
|
573
|
-
const role = this.extractDomain(validatorName);
|
|
574
|
-
return this._withHeartbeat(
|
|
575
|
-
() => this.runStorySolver(workingStory, storyContext, epic, finalResults[vi], validatorName),
|
|
576
|
-
(elapsed) => {
|
|
577
|
-
if (elapsed < 25) return ` ↻ ${role} solver — improving story…`;
|
|
578
|
-
if (elapsed < 50) return ` ↻ ${role} solver — refining acceptance criteria…`;
|
|
579
|
-
return ` ↻ ${role} solver — still running…`;
|
|
580
|
-
},
|
|
581
|
-
20000
|
|
582
|
-
).then(improved => ({ vi, improved, error: null }))
|
|
583
|
-
.catch(err => ({ vi, improved: null, error: err }));
|
|
584
|
-
})
|
|
585
|
-
);
|
|
586
|
-
console.log(`[TIMING] Phase 2 round ${iter} solvers: ${Date.now() - _t0Round}ms`);
|
|
587
|
-
|
|
588
|
-
// 2. Merge all solver results — preserve base items, add up to 3 new items per solver
|
|
589
|
-
// Base items are anchored (never dropped); each solver contributes at most 3 genuinely new AC.
|
|
590
|
-
// Description taken from the solver whose validator scored lowest (most informed fix).
|
|
591
|
-
const baseAC = workingStory.acceptance || [];
|
|
592
|
-
const baseACSet = new Set(baseAC);
|
|
593
|
-
const newACAdditions = []; // items new beyond base, insertion-ordered, deduplicated
|
|
594
|
-
const allDeps = new Set(workingStory.dependencies || []);
|
|
595
|
-
let bestDescription = workingStory.description;
|
|
596
|
-
let worstValidatorScore = Infinity;
|
|
597
|
-
let anyImproved = false;
|
|
598
|
-
|
|
599
|
-
for (const { vi, improved, error } of solverResults) {
|
|
600
|
-
const validatorName = validators[vi];
|
|
601
|
-
const role = this.extractDomain(validatorName);
|
|
602
|
-
if (error) {
|
|
603
|
-
console.warn(` ⚠ Solver failed for ${validatorName}: ${error.message} — keeping current story`);
|
|
604
|
-
await this._detail(` ⚠ Solver failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
if (improved && improved.id === workingStory.id) {
|
|
608
|
-
anyImproved = true;
|
|
609
|
-
// Collect only new items not already in base or pending additions (max 3 per solver)
|
|
610
|
-
const existingAll = new Set([...baseACSet, ...newACAdditions]);
|
|
611
|
-
const solverNew = (improved.acceptance || []).filter(a => !existingAll.has(a)).slice(0, 3);
|
|
612
|
-
newACAdditions.push(...solverNew);
|
|
613
|
-
(improved.dependencies || []).forEach(d => allDeps.add(d));
|
|
614
|
-
// Description: take from the validator with the lowest score (most dissatisfied perspective)
|
|
615
|
-
const vScore = finalResults[vi]?.overallScore ?? 100;
|
|
616
|
-
if (improved.description && improved.description !== workingStory.description && vScore < worstValidatorScore) {
|
|
617
|
-
worstValidatorScore = vScore;
|
|
618
|
-
bestDescription = improved.description;
|
|
619
|
-
}
|
|
620
|
-
console.log(` ↻ [${role}] solver merged — ${solverNew.length} new AC added`);
|
|
621
|
-
await this._detail(` → [${role}] improvements applied`);
|
|
622
|
-
} else {
|
|
623
|
-
console.log(` ↻ [${role}] solver returned no valid improvement (id mismatch or empty)`);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (anyImproved) {
|
|
628
|
-
// Base items always preserved first; cap only the additions portion
|
|
629
|
-
const merged = [...baseAC, ...newACAdditions];
|
|
630
|
-
const hardCap = baseAC.length + 10; // allow up to 10 new AC beyond original
|
|
631
|
-
const finalAC = merged.length > hardCap ? merged.slice(0, hardCap) : merged;
|
|
632
|
-
if (merged.length > hardCap) {
|
|
633
|
-
console.log(` ↻ Story AC capped after merge: ${merged.length} → ${hardCap} (base=${baseAC.length} + new=${finalAC.length - baseAC.length})`);
|
|
634
|
-
}
|
|
635
|
-
const descBefore = (workingStory.description || '').slice(0, 100);
|
|
636
|
-
workingStory = {
|
|
637
|
-
...workingStory,
|
|
638
|
-
description: bestDescription,
|
|
639
|
-
acceptance: finalAC,
|
|
640
|
-
dependencies: [...allDeps],
|
|
641
|
-
};
|
|
642
|
-
const descAfter = (workingStory.description || '').slice(0, 100);
|
|
643
|
-
console.log(` ↻ Parallel merge applied — desc changed: ${descBefore !== descAfter}, AC: ${finalAC.length} (base=${baseAC.length} + new=${newACAdditions.length})`);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// 3. Re-validate all in parallel
|
|
647
|
-
await this._detail(` Re-validating ${roundCount} validator${roundCount !== 1 ? 's' : ''} in parallel (iter ${iter + 1})…`);
|
|
648
|
-
const _t0Revalidate = Date.now();
|
|
649
|
-
const revalidateResults = await Promise.all(
|
|
650
|
-
needsWork.map(vi => {
|
|
651
|
-
const validatorName = validators[vi];
|
|
652
|
-
const role = this.extractDomain(validatorName);
|
|
653
|
-
return this._withHeartbeat(
|
|
654
|
-
() => this.runStoryValidator(workingStory, storyContext, epic, validatorName),
|
|
655
|
-
(elapsed) => {
|
|
656
|
-
if (elapsed < 20) return ` [${role}] re-reviewing story…`;
|
|
657
|
-
if (elapsed < 40) return ` [${role}] re-checking acceptance criteria…`;
|
|
658
|
-
return ` [${role}] re-validating…`;
|
|
659
|
-
},
|
|
660
|
-
10000
|
|
661
|
-
).then(result => ({ vi, result, error: null }))
|
|
662
|
-
.catch(err => ({ vi, result: null, error: err }));
|
|
663
|
-
})
|
|
664
|
-
);
|
|
665
|
-
console.log(`[TIMING] Phase 2 round ${iter} re-validates: ${Date.now() - _t0Revalidate}ms`);
|
|
666
|
-
|
|
667
|
-
// 4. Update finalResults; determine which validators need another round
|
|
668
|
-
const nextNeedsWork = [];
|
|
669
|
-
for (const { vi, result, error } of revalidateResults) {
|
|
670
|
-
const validatorName = validators[vi];
|
|
671
|
-
const role = this.extractDomain(validatorName);
|
|
672
|
-
if (error) {
|
|
673
|
-
console.warn(` ⚠ Re-validator ${validatorName} failed: ${error.message.split('\n')[0]}`);
|
|
674
|
-
await this._detail(` ⚠ Re-validate failed (${role}): ${error.message.split('\n')[0].slice(0, 120)}`);
|
|
675
|
-
continue; // keep finalResults[vi] as-is; give up on this validator
|
|
676
|
-
}
|
|
677
|
-
finalResults[vi] = result;
|
|
678
|
-
const newScore = result.overallScore ?? 0;
|
|
679
|
-
const acceptable = newScore >= acceptanceThreshold;
|
|
680
|
-
const issueStr2 = (result.issues || []).length > 0 ? ` · ${result.issues.length} issues` : '';
|
|
681
|
-
await this._detail(` [${role}] iter ${iter + 1}: ${newScore}/100${issueStr2} — ${acceptable ? '✓ accepted' : `⚠ below threshold (${acceptanceThreshold})`}`);
|
|
682
|
-
console.log(` [${vi + 1}/${validators.length}] ${validatorName} iter=${iter + 1} score=${newScore}/100 status=${result.validationStatus}`);
|
|
683
|
-
if (!acceptable) {
|
|
684
|
-
nextNeedsWork.push(vi);
|
|
685
|
-
}
|
|
367
|
+
const workItemText = storyContext || this.generateStoryContextMd(story, epic);
|
|
368
|
+
const mcResult = await validateWithMicroChecks(
|
|
369
|
+
story, workItemText, 'story', perspectives, this.llmProvider,
|
|
370
|
+
{
|
|
371
|
+
concurrency: this.validationStageConfig?.concurrency ?? 5,
|
|
372
|
+
batchSize: this.validationStageConfig?.batchSize ?? 8,
|
|
373
|
+
maxFixAttempts: this.validationStageConfig?.maxFixAttempts ?? 3,
|
|
374
|
+
generateContextFn: (wi) => this.generateStoryContextMd(wi, epic),
|
|
375
|
+
progressCallback: this.progressCallback,
|
|
376
|
+
projectRoot: this.projectContext?.projectRoot,
|
|
686
377
|
}
|
|
687
|
-
|
|
688
|
-
needsWork = nextNeedsWork;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const validationResults = finalResults;
|
|
692
|
-
|
|
693
|
-
// Write accumulated improvements back to the original story object
|
|
694
|
-
story.description = workingStory.description;
|
|
695
|
-
story.acceptance = workingStory.acceptance;
|
|
696
|
-
story.dependencies = workingStory.dependencies;
|
|
697
|
-
|
|
698
|
-
// 3. Aggregate results
|
|
699
|
-
const aggregated = this.aggregateValidationResults(validationResults, 'story');
|
|
700
|
-
|
|
701
|
-
// 4. Determine overall status
|
|
702
|
-
aggregated.overallStatus = this.determineOverallStatus(validationResults);
|
|
703
|
-
aggregated.readyToPublish = aggregated.overallStatus !== 'needs-improvement';
|
|
704
|
-
|
|
705
|
-
await this._detail(`Overall: ${aggregated.readyToPublish ? '✓ passed' : '⚠ needs improvement'} · avg ${aggregated.averageScore}/100`);
|
|
706
|
-
console.log(` Story "${story.name}" summary: avg=${aggregated.averageScore}/100 readyToPublish=${aggregated.readyToPublish} critical=${aggregated.criticalIssues.length} major=${aggregated.majorIssues.length}`);
|
|
707
|
-
aggregated.validatorResults.forEach(vr => {
|
|
708
|
-
console.log(` ${this.extractDomain(vr.validator)}: ${vr.score}/100 (${vr.status})`);
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
// 5. Store for feedback loop
|
|
712
|
-
this.storeValidationFeedback(story.id, aggregated);
|
|
713
|
-
|
|
714
|
-
// 6. Persist result into story metadata so sprint-planning-processor writes it to work.json
|
|
378
|
+
);
|
|
715
379
|
story.metadata = story.metadata || {};
|
|
716
380
|
story.metadata.validationResult = {
|
|
717
|
-
averageScore:
|
|
718
|
-
overallStatus:
|
|
719
|
-
readyToPublish:
|
|
720
|
-
criticalIssues:
|
|
721
|
-
majorIssues:
|
|
722
|
-
minorIssues:
|
|
723
|
-
validatorResults:
|
|
724
|
-
validatedAt:
|
|
381
|
+
averageScore: mcResult.averageScore,
|
|
382
|
+
overallStatus: mcResult.overallStatus,
|
|
383
|
+
readyToPublish: mcResult.readyToPublish,
|
|
384
|
+
criticalIssues: mcResult.criticalIssues,
|
|
385
|
+
majorIssues: mcResult.majorIssues,
|
|
386
|
+
minorIssues: mcResult.minorIssues,
|
|
387
|
+
validatorResults: mcResult.validatorResults,
|
|
388
|
+
validatedAt: mcResult.validatedAt,
|
|
725
389
|
};
|
|
726
|
-
|
|
727
|
-
return
|
|
390
|
+
this.storeValidationFeedback(story.id, mcResult);
|
|
391
|
+
return mcResult;
|
|
728
392
|
}
|
|
729
393
|
|
|
730
394
|
/**
|
|
731
|
-
*
|
|
395
|
+
* Build the prompt for the story-splitter agent.
|
|
396
|
+
* @param {Object} story - The oversized story to split
|
|
397
|
+
* @param {Object} epic - Parent epic for context
|
|
398
|
+
* @param {Array} allIssues - MAJOR+CRITICAL issues from all validators
|
|
399
|
+
* @returns {string}
|
|
732
400
|
* @private
|
|
733
401
|
*/
|
|
734
|
-
|
|
735
|
-
const
|
|
402
|
+
_buildStorySplitterPrompt(story, epic, allIssues) {
|
|
403
|
+
const fullEpicContext = this.generateEpicContextMd(epic);
|
|
404
|
+
const epicContextMd = fullEpicContext.length > 3000
|
|
405
|
+
? fullEpicContext.substring(0, 3000) + '\n… (truncated)'
|
|
406
|
+
: fullEpicContext;
|
|
407
|
+
const storyContext = this.generateStoryContextMd(story, epic);
|
|
408
|
+
const critMajor = allIssues.filter(i => i.severity === 'critical' || i.severity === 'major');
|
|
409
|
+
const issueText = critMajor.map((issue, i) => {
|
|
410
|
+
const cat = issue.category ? `[${issue.category}] ` : '';
|
|
411
|
+
const sug = issue.suggestion ? `\n Required fix: ${issue.suggestion}` : '';
|
|
412
|
+
return `${i + 1}. [${(issue.severity || 'major').toUpperCase()}] ${cat}${issue.description || ''}${sug}`;
|
|
413
|
+
}).join('\n');
|
|
736
414
|
|
|
737
|
-
|
|
738
|
-
const prompt = this.buildEpicValidationPrompt(epic, epicContext);
|
|
415
|
+
return `# Story to Split
|
|
739
416
|
|
|
740
|
-
|
|
741
|
-
const provider = await this.getProviderForValidator(validatorName);
|
|
417
|
+
## Parent Epic (canonical)
|
|
742
418
|
|
|
743
|
-
|
|
744
|
-
const _usageBefore = provider.getTokenUsage();
|
|
745
|
-
const _t0 = Date.now();
|
|
746
|
-
console.log(`[API-START] ${validatorName} (epic="${epic.name}", promptLen=${prompt.length})`);
|
|
747
|
-
const rawResult = await provider.generateJSON(prompt, agentInstructions);
|
|
748
|
-
const _elapsed = Date.now() - _t0;
|
|
749
|
-
const _usageAfter = provider.getTokenUsage();
|
|
750
|
-
const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
|
|
751
|
-
const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
|
|
752
|
-
console.log(`[API-DONE] ${validatorName} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
|
|
419
|
+
${epicContextMd}
|
|
753
420
|
|
|
754
|
-
|
|
755
|
-
if (!rawResult || typeof rawResult !== 'object') {
|
|
756
|
-
throw new Error(`Invalid validation result from ${validatorName}: expected object`);
|
|
757
|
-
}
|
|
421
|
+
## Original Story (too large — ${(story.acceptance || []).length} acceptance criteria)
|
|
758
422
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
423
|
+
${storyContext}
|
|
424
|
+
|
|
425
|
+
## Validator Issues That Triggered This Split
|
|
763
426
|
|
|
764
|
-
|
|
765
|
-
rawResult._validatorName = validatorName;
|
|
427
|
+
${issueText || 'Story exceeded the acceptance criteria size limit after multiple validation passes.'}
|
|
766
428
|
|
|
767
|
-
|
|
429
|
+
Split this story into 2-3 smaller independently deliverable stories.
|
|
430
|
+
Return a JSON array of story objects as specified in your instructions.
|
|
431
|
+
`;
|
|
768
432
|
}
|
|
769
433
|
|
|
770
434
|
/**
|
|
771
|
-
*
|
|
772
|
-
*
|
|
435
|
+
* Call the story-splitter agent to decompose an oversized story into 2-3 smaller stories.
|
|
436
|
+
* Returns an array of 2-3 new story objects, or null if the split failed/produced invalid output.
|
|
437
|
+
* @param {Object} workingStory - The fully-validated but too-large story
|
|
438
|
+
* @param {Object} epic - Parent epic for context
|
|
439
|
+
* @param {Array} allIssues - MAJOR+CRITICAL issues from all validators
|
|
440
|
+
* @returns {Promise<Array<Object>|null>}
|
|
773
441
|
*/
|
|
774
|
-
async
|
|
775
|
-
const agentInstructions = this.loadAgentInstructions(
|
|
442
|
+
async _splitStory(workingStory, epic, allIssues) {
|
|
443
|
+
const agentInstructions = this.loadAgentInstructions('story-splitter.md');
|
|
444
|
+
const prompt = this._buildStorySplitterPrompt(workingStory, epic, allIssues);
|
|
445
|
+
const provider = await this.getProviderForValidator('story-splitter');
|
|
776
446
|
|
|
777
|
-
// Build validation prompt
|
|
778
|
-
const prompt = this.buildStoryValidationPrompt(story, storyContext, epic);
|
|
779
|
-
|
|
780
|
-
// Get validator-specific provider based on validation type
|
|
781
|
-
const provider = await this.getProviderForValidator(validatorName);
|
|
782
|
-
|
|
783
|
-
// Call LLM with validator agent instructions
|
|
784
447
|
const _usageBefore = provider.getTokenUsage();
|
|
785
448
|
const _t0 = Date.now();
|
|
786
|
-
console.log(`[API-START]
|
|
787
|
-
const rawResult = await provider.generateJSON(prompt, agentInstructions);
|
|
788
|
-
const _elapsed = Date.now() - _t0;
|
|
789
|
-
const _usageAfter = provider.getTokenUsage();
|
|
790
|
-
const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
|
|
791
|
-
const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
|
|
792
|
-
console.log(`[API-DONE] ${validatorName} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
|
|
793
|
-
|
|
794
|
-
// Basic validation of result structure
|
|
795
|
-
if (!rawResult || typeof rawResult !== 'object') {
|
|
796
|
-
throw new Error(`Invalid validation result from ${validatorName}: expected object`);
|
|
797
|
-
}
|
|
449
|
+
console.log(`[API-START] story-splitter (story="${workingStory.name}", ACs=${(workingStory.acceptance || []).length})`);
|
|
798
450
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
this.
|
|
451
|
+
let result;
|
|
452
|
+
try {
|
|
453
|
+
result = await this._withHeartbeat(
|
|
454
|
+
() => provider.generateJSON(prompt, agentInstructions),
|
|
455
|
+
(elapsed) => {
|
|
456
|
+
if (elapsed < 20) return ` ✂ splitting story — analyzing scope boundaries…`;
|
|
457
|
+
if (elapsed < 40) return ` ✂ splitting story — assigning acceptance criteria…`;
|
|
458
|
+
return ` ✂ splitting story — still running…`;
|
|
459
|
+
},
|
|
460
|
+
10000
|
|
461
|
+
);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
console.warn(` ⚠ story-splitter failed: ${err.message.split('\n')[0]}`);
|
|
464
|
+
return null;
|
|
802
465
|
}
|
|
803
466
|
|
|
804
|
-
// Add metadata
|
|
805
|
-
rawResult._validatorName = validatorName;
|
|
806
|
-
|
|
807
|
-
return rawResult;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
* Run a solver agent to improve an epic based on validation issues
|
|
812
|
-
* @private
|
|
813
|
-
*/
|
|
814
|
-
async runEpicSolver(epic, epicContext, validationResult, validatorName) {
|
|
815
|
-
const role = this.extractDomain(validatorName);
|
|
816
|
-
const agentInstructions = this.loadAgentInstructions(`solver-epic-${role}.md`);
|
|
817
|
-
const prompt = this.buildEpicSolverPrompt(epic, epicContext, validationResult, validatorName);
|
|
818
|
-
const provider = await this.getProviderForSolver(role);
|
|
819
|
-
|
|
820
|
-
const _usageBefore = provider.getTokenUsage();
|
|
821
|
-
const _t0 = Date.now();
|
|
822
|
-
console.log(`[API-START] solver-epic-${role} (epic="${epic.name}", promptLen=${prompt.length})`);
|
|
823
|
-
const improved = await provider.generateJSON(prompt, agentInstructions);
|
|
824
467
|
const _elapsed = Date.now() - _t0;
|
|
825
468
|
const _usageAfter = provider.getTokenUsage();
|
|
826
469
|
const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
|
|
827
470
|
const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
|
|
828
|
-
console.log(`[API-DONE]
|
|
471
|
+
console.log(`[API-DONE] story-splitter — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
|
|
829
472
|
|
|
830
|
-
|
|
831
|
-
|
|
473
|
+
// Validate: result must be an array of 2-3 stories
|
|
474
|
+
if (!Array.isArray(result) || result.length < 2 || result.length > 3) {
|
|
475
|
+
console.warn(` ⚠ story-splitter returned unexpected shape (expected array of 2-3, got ${Array.isArray(result) ? result.length : typeof result}) — skipping split`);
|
|
476
|
+
return null;
|
|
832
477
|
}
|
|
833
478
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const prompt = this.buildStorySolverPrompt(story, storyContext, epic, validationResult, validatorName);
|
|
845
|
-
const provider = await this.getProviderForSolver(role);
|
|
846
|
-
|
|
847
|
-
const _usageBefore = provider.getTokenUsage();
|
|
848
|
-
const _t0 = Date.now();
|
|
849
|
-
console.log(`[API-START] solver-story-${role} (story="${story.name}", promptLen=${prompt.length})`);
|
|
850
|
-
const improved = await provider.generateJSON(prompt, agentInstructions);
|
|
851
|
-
const _elapsed = Date.now() - _t0;
|
|
852
|
-
const _usageAfter = provider.getTokenUsage();
|
|
853
|
-
const _deltaIn = _usageAfter.inputTokens - _usageBefore.inputTokens;
|
|
854
|
-
const _deltaOut = _usageAfter.outputTokens - _usageBefore.outputTokens;
|
|
855
|
-
console.log(`[API-DONE] solver-story-${role} — ${_elapsed}ms | in=${_deltaIn} out=${_deltaOut} tokens`);
|
|
479
|
+
// Validate each split story has required fields; cap ACs to 8
|
|
480
|
+
for (const s of result) {
|
|
481
|
+
if (!s.id || !s.name || !Array.isArray(s.acceptance)) {
|
|
482
|
+
console.warn(` ⚠ story-splitter returned malformed story (missing id/name/acceptance) — skipping split`);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
if (s.acceptance.length > 8) {
|
|
486
|
+
s.acceptance = s.acceptance.slice(0, 8);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
856
489
|
|
|
857
490
|
if (this.verificationTracker) {
|
|
858
|
-
this.verificationTracker.recordCheck(
|
|
491
|
+
this.verificationTracker.recordCheck('story-splitter', 'story-splitting', true);
|
|
859
492
|
}
|
|
860
493
|
|
|
861
|
-
return
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* Build solver prompt for an Epic
|
|
866
|
-
* @private
|
|
867
|
-
*/
|
|
868
|
-
buildEpicSolverPrompt(epic, epicContext, validationResult, validatorName) {
|
|
869
|
-
const allIssues = validationResult.issues || [];
|
|
870
|
-
const critMajor = allIssues.filter(i => i.severity === 'critical' || i.severity === 'major');
|
|
871
|
-
// When no critical/major issues exist, include minor issues so the solver has specific guidance
|
|
872
|
-
const issues = critMajor.length > 0 ? critMajor : allIssues;
|
|
873
|
-
|
|
874
|
-
const role = this.extractDomain(validatorName);
|
|
875
|
-
|
|
876
|
-
const issueText = issues.map((issue, i) =>
|
|
877
|
-
`${i + 1}. [${issue.severity.toUpperCase()}] ${issue.category}: ${issue.description}\n Fix: ${issue.suggestion}`
|
|
878
|
-
).join('\n');
|
|
879
|
-
|
|
880
|
-
return `# Epic to Improve
|
|
881
|
-
|
|
882
|
-
**Epic ID:** ${epic.id}
|
|
883
|
-
**Epic Name:** ${epic.name}
|
|
884
|
-
**Domain:** ${epic.domain}
|
|
885
|
-
**Current Description:** ${epic.description}
|
|
886
|
-
|
|
887
|
-
**Current Features:**
|
|
888
|
-
${(epic.features || []).map(f => `- ${f}`).join('\n')}
|
|
889
|
-
|
|
890
|
-
**Current Dependencies:**
|
|
891
|
-
${(epic.dependencies || []).length > 0 ? epic.dependencies.join(', ') : 'None'}
|
|
892
|
-
|
|
893
|
-
**Epic Context:**
|
|
894
|
-
\`\`\`
|
|
895
|
-
${epicContext}
|
|
896
|
-
\`\`\`
|
|
897
|
-
|
|
898
|
-
## Issues to Fix (from ${role} review):
|
|
899
|
-
|
|
900
|
-
${issueText || 'No critical/major issues — improve overall quality.'}
|
|
901
|
-
|
|
902
|
-
Improve this Epic to address the issues above. Return the complete improved Epic JSON.
|
|
903
|
-
|
|
904
|
-
**IMPORTANT CONSTRAINTS:**
|
|
905
|
-
- Do NOT remove or consolidate existing features — only add new ones to address the issues above.
|
|
906
|
-
- Each feature must be a single concise sentence (max 30 words). Do not expand them into paragraphs.
|
|
907
|
-
`;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* Build solver prompt for a Story
|
|
912
|
-
* @private
|
|
913
|
-
*/
|
|
914
|
-
buildStorySolverPrompt(story, storyContext, epic, validationResult, validatorName) {
|
|
915
|
-
const allIssues = validationResult.issues || [];
|
|
916
|
-
const critMajor = allIssues.filter(i => i.severity === 'critical' || i.severity === 'major');
|
|
917
|
-
// When no critical/major issues exist, include minor issues so the solver has specific guidance
|
|
918
|
-
const issues = critMajor.length > 0 ? critMajor : allIssues;
|
|
919
|
-
|
|
920
|
-
const role = this.extractDomain(validatorName);
|
|
921
|
-
|
|
922
|
-
const issueText = issues.map((issue, i) =>
|
|
923
|
-
`${i + 1}. [${issue.severity.toUpperCase()}] ${issue.category}: ${issue.description}\n Fix: ${issue.suggestion}`
|
|
924
|
-
).join('\n');
|
|
925
|
-
|
|
926
|
-
return `# Story to Improve
|
|
927
|
-
|
|
928
|
-
**Story ID:** ${story.id}
|
|
929
|
-
**Story Name:** ${story.name}
|
|
930
|
-
**User Type:** ${story.userType}
|
|
931
|
-
**Current Description:** ${story.description}
|
|
932
|
-
|
|
933
|
-
**Current Acceptance Criteria:**
|
|
934
|
-
${(story.acceptance || []).map((ac, i) => `${i + 1}. ${ac}`).join('\n')}
|
|
935
|
-
|
|
936
|
-
**Current Dependencies:**
|
|
937
|
-
${(story.dependencies || []).length > 0 ? story.dependencies.join(', ') : 'None'}
|
|
938
|
-
|
|
939
|
-
**Parent Epic:**
|
|
940
|
-
- Name: ${epic.name}
|
|
941
|
-
- Domain: ${epic.domain}
|
|
942
|
-
- Features: ${(epic.features || []).join(', ')}
|
|
943
|
-
|
|
944
|
-
**Story Context:**
|
|
945
|
-
\`\`\`
|
|
946
|
-
${storyContext}
|
|
947
|
-
\`\`\`
|
|
948
|
-
|
|
949
|
-
## Issues to Fix (from ${role} review):
|
|
950
|
-
|
|
951
|
-
${issueText || 'No critical/major issues — improve overall quality.'}
|
|
952
|
-
|
|
953
|
-
Improve this Story to address the issues above. Return the complete improved Story JSON.
|
|
954
|
-
|
|
955
|
-
**IMPORTANT CONSTRAINTS:**
|
|
956
|
-
- Do NOT remove or consolidate existing acceptance criteria — only add new ones to address the issues above.
|
|
957
|
-
- Each AC must be a single concrete, testable sentence (max 40 words). Do not expand them into paragraphs.
|
|
958
|
-
`;
|
|
494
|
+
return result;
|
|
959
495
|
}
|
|
960
496
|
|
|
961
497
|
/**
|
|
@@ -967,9 +503,13 @@ Improve this Story to address the issues above. Return the complete improved Sto
|
|
|
967
503
|
type: type, // 'epic' or 'story'
|
|
968
504
|
validatorCount: results.length,
|
|
969
505
|
validators: results.map(r => r._validatorName),
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
)
|
|
506
|
+
// Exclude errored validators (API failures) from the average — a 0 from a
|
|
507
|
+
// network failure is not a real score and would corrupt the aggregate.
|
|
508
|
+
averageScore: (() => {
|
|
509
|
+
const scorable = results.filter(r => !r._validatorError);
|
|
510
|
+
if (scorable.length === 0) return 0;
|
|
511
|
+
return Math.round(scorable.reduce((sum, r) => sum + (r.overallScore || 0), 0) / scorable.length);
|
|
512
|
+
})(),
|
|
973
513
|
|
|
974
514
|
// Aggregate issues by severity
|
|
975
515
|
criticalIssues: [],
|
|
@@ -1041,6 +581,12 @@ Improve this Story to address the issues above. Return the complete improved Sto
|
|
|
1041
581
|
determineOverallStatus(results) {
|
|
1042
582
|
const statuses = results.map(r => r.validationStatus);
|
|
1043
583
|
|
|
584
|
+
// If any validator errored (API failure), mark as needs-improvement so
|
|
585
|
+
// readyToPublish stays false — validation is incomplete, not "acceptable"
|
|
586
|
+
if (statuses.includes('error')) {
|
|
587
|
+
return 'needs-improvement';
|
|
588
|
+
}
|
|
589
|
+
|
|
1044
590
|
// If any validator says "needs-improvement", overall is "needs-improvement"
|
|
1045
591
|
if (statuses.includes('needs-improvement')) {
|
|
1046
592
|
return 'needs-improvement';
|
|
@@ -1055,68 +601,6 @@ Improve this Story to address the issues above. Return the complete improved Sto
|
|
|
1055
601
|
return 'acceptable';
|
|
1056
602
|
}
|
|
1057
603
|
|
|
1058
|
-
/**
|
|
1059
|
-
* Build validation prompt for Epic
|
|
1060
|
-
* @private
|
|
1061
|
-
*/
|
|
1062
|
-
buildEpicValidationPrompt(epic, epicContext) {
|
|
1063
|
-
return `# Epic to Validate
|
|
1064
|
-
|
|
1065
|
-
**Epic ID:** ${epic.id}
|
|
1066
|
-
**Epic Name:** ${epic.name}
|
|
1067
|
-
**Domain:** ${epic.domain}
|
|
1068
|
-
**Description:** ${epic.description}
|
|
1069
|
-
|
|
1070
|
-
**Features:**
|
|
1071
|
-
${(epic.features || []).map(f => `- ${f}`).join('\n')}
|
|
1072
|
-
|
|
1073
|
-
**Dependencies:**
|
|
1074
|
-
${(epic.dependencies || []).length > 0 ? epic.dependencies.join(', ') : 'None'}
|
|
1075
|
-
|
|
1076
|
-
**Stories:**
|
|
1077
|
-
${(epic.children || []).length} stories defined
|
|
1078
|
-
|
|
1079
|
-
**Epic Context:**
|
|
1080
|
-
\`\`\`
|
|
1081
|
-
${epicContext}
|
|
1082
|
-
\`\`\`
|
|
1083
|
-
|
|
1084
|
-
Validate this Epic from your domain expertise perspective and return JSON validation results following the specified format.
|
|
1085
|
-
`;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Build validation prompt for Story
|
|
1090
|
-
* @private
|
|
1091
|
-
*/
|
|
1092
|
-
buildStoryValidationPrompt(story, storyContext, epic) {
|
|
1093
|
-
return `# Story to Validate
|
|
1094
|
-
|
|
1095
|
-
**Story ID:** ${story.id}
|
|
1096
|
-
**Story Name:** ${story.name}
|
|
1097
|
-
**User Type:** ${story.userType}
|
|
1098
|
-
**Description:** ${story.description}
|
|
1099
|
-
|
|
1100
|
-
**Acceptance Criteria:**
|
|
1101
|
-
${(story.acceptance || []).map((ac, i) => `${i + 1}. ${ac}`).join('\n')}
|
|
1102
|
-
|
|
1103
|
-
**Dependencies:**
|
|
1104
|
-
${(story.dependencies || []).length > 0 ? story.dependencies.join(', ') : 'None'}
|
|
1105
|
-
|
|
1106
|
-
**Parent Epic:**
|
|
1107
|
-
- Name: ${epic.name}
|
|
1108
|
-
- Domain: ${epic.domain}
|
|
1109
|
-
- Features: ${(epic.features || []).join(', ')}
|
|
1110
|
-
|
|
1111
|
-
**Story Context:**
|
|
1112
|
-
\`\`\`
|
|
1113
|
-
${storyContext}
|
|
1114
|
-
\`\`\`
|
|
1115
|
-
|
|
1116
|
-
Validate this Story from your domain expertise perspective and return JSON validation results following the specified format.
|
|
1117
|
-
`;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
604
|
/**
|
|
1121
605
|
* Load agent instructions from .md file
|
|
1122
606
|
* @private
|
|
@@ -1138,6 +622,7 @@ Validate this Story from your domain expertise perspective and return JSON valid
|
|
|
1138
622
|
// Extract domain from validator/solver name
|
|
1139
623
|
// e.g., "validator-epic-security" → "security"
|
|
1140
624
|
// e.g., "solver-epic-security" → "security"
|
|
625
|
+
if (!validatorName) return 'unknown';
|
|
1141
626
|
const match = validatorName.match(/(?:validator|solver)-(?:epic|story)-(.+)/);
|
|
1142
627
|
return match ? match[1] : 'unknown';
|
|
1143
628
|
}
|