@agile-vibe-coding/avc 0.1.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
@@ -0,0 +1,784 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { Wand2, Check, X, ChevronDown, ChevronRight, AlertTriangle } from 'lucide-react';
3
+ import { getModels, getSettings, refineWorkItem, applyWorkItemChanges } from '../../lib/api';
4
+
5
+ function normalizeProvider(p = '') {
6
+ if (p === 'claude') return 'anthropic';
7
+ return p;
8
+ }
9
+
10
+ function ModelSelect({ label, value, onChange, models, disabled }) {
11
+ const providers = [...new Set(models.map((m) => m.provider))];
12
+ const chosen = models.find((m) => m.modelId === value);
13
+ return (
14
+ <div>
15
+ <label className="block text-xs font-medium text-slate-500 mb-1">{label}</label>
16
+ <select
17
+ value={value}
18
+ onChange={(e) => onChange(e.target.value)}
19
+ disabled={disabled || models.length === 0}
20
+ className="w-full rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-500 disabled:opacity-60 bg-white"
21
+ >
22
+ {models.length === 0 && <option value="">Loading…</option>}
23
+ {providers.map((p) => (
24
+ <optgroup key={p} label={p.charAt(0).toUpperCase() + p.slice(1)}>
25
+ {models.filter((m) => m.provider === p).map((m) => (
26
+ <option key={m.modelId} value={m.modelId}>
27
+ {m.displayName}{!m.hasApiKey ? ' (no key)' : ''}
28
+ </option>
29
+ ))}
30
+ </optgroup>
31
+ ))}
32
+ </select>
33
+ {chosen && !chosen.hasApiKey && (
34
+ <p className="text-xs text-amber-600 mt-0.5">
35
+ ⚠ No API key — add to <code>.env</code>
36
+ </p>
37
+ )}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ function FieldDiff({ label, before, after }) {
43
+ const beforeStr = Array.isArray(before) ? before.join('\n') : (before ?? '');
44
+ const afterStr = Array.isArray(after) ? after.join('\n') : (after ?? '');
45
+ if (!beforeStr && !afterStr) return null;
46
+ if (beforeStr === afterStr) return null;
47
+ return (
48
+ <div className="mb-3">
49
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">{label}</p>
50
+ {beforeStr && (
51
+ <div className="mb-1 px-2.5 py-2 bg-red-50 border border-red-100 rounded text-xs text-red-700 leading-relaxed line-through">
52
+ {beforeStr}
53
+ </div>
54
+ )}
55
+ {afterStr && (
56
+ <div className="px-2.5 py-2 bg-green-50 border border-green-100 rounded text-xs text-green-700 leading-relaxed">
57
+ {afterStr}
58
+ </div>
59
+ )}
60
+ </div>
61
+ );
62
+ }
63
+
64
+ function StoryUpdateCard({ impact, checked, onToggle }) {
65
+ const [expanded, setExpanded] = useState(false);
66
+ return (
67
+ <div className={`border rounded-lg overflow-hidden transition-colors ${
68
+ checked ? 'border-violet-300 bg-violet-50' : 'border-slate-200 bg-white'
69
+ }`}>
70
+ <div className="flex items-center gap-2.5 px-3 py-2.5">
71
+ <input
72
+ type="checkbox"
73
+ checked={checked}
74
+ onChange={onToggle}
75
+ className="flex-shrink-0 accent-violet-600"
76
+ />
77
+ <div className="flex-1 min-w-0">
78
+ <p className="text-xs font-medium text-slate-800 truncate">
79
+ {impact.proposedStory?.name ?? impact.storyId}
80
+ </p>
81
+ <p className="text-xs text-slate-400 truncate">{impact.storyId}</p>
82
+ </div>
83
+ {impact.changesNeeded && (
84
+ <button
85
+ type="button"
86
+ onClick={() => setExpanded((v) => !v)}
87
+ className="flex-shrink-0 text-slate-400 hover:text-slate-600"
88
+ >
89
+ {expanded
90
+ ? <ChevronDown className="w-3.5 h-3.5" />
91
+ : <ChevronRight className="w-3.5 h-3.5" />}
92
+ </button>
93
+ )}
94
+ </div>
95
+ {expanded && impact.changesNeeded && (
96
+ <div className="px-3 pb-3 pt-1 border-t border-slate-100">
97
+ <p className="text-xs text-slate-600 leading-relaxed">{impact.changesNeeded}</p>
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ function NewStoryCard({ story, checked, onToggle }) {
105
+ const [expanded, setExpanded] = useState(false);
106
+ const acs = story?.acceptance ?? story?.acceptanceCriteria ?? [];
107
+ return (
108
+ <div className={`border rounded-lg overflow-hidden transition-colors ${
109
+ checked ? 'border-emerald-300 bg-emerald-50' : 'border-slate-200 bg-white'
110
+ }`}>
111
+ <div className="flex items-center gap-2.5 px-3 py-2.5">
112
+ <input
113
+ type="checkbox"
114
+ checked={checked}
115
+ onChange={onToggle}
116
+ className="flex-shrink-0 accent-emerald-600"
117
+ />
118
+ <div className="flex-1 min-w-0">
119
+ <div className="flex items-center gap-1.5 mb-0.5">
120
+ <span className="flex-shrink-0 text-xs font-bold px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700">
121
+ NEW
122
+ </span>
123
+ <p className="text-xs font-medium text-slate-800 truncate">{story?.name}</p>
124
+ </div>
125
+ {story?.description && (
126
+ <p className="text-xs text-slate-500 truncate">{story.description}</p>
127
+ )}
128
+ </div>
129
+ <button
130
+ type="button"
131
+ onClick={() => setExpanded((v) => !v)}
132
+ className="flex-shrink-0 text-slate-400 hover:text-slate-600"
133
+ >
134
+ {expanded
135
+ ? <ChevronDown className="w-3.5 h-3.5" />
136
+ : <ChevronRight className="w-3.5 h-3.5" />}
137
+ </button>
138
+ </div>
139
+ {expanded && (
140
+ <div className="px-3 pb-3 pt-1 border-t border-slate-100 space-y-2">
141
+ {story?.description && (
142
+ <div>
143
+ <p className="text-xs font-medium text-slate-500 mb-0.5">Description</p>
144
+ <p className="text-xs text-slate-700 leading-relaxed">{story.description}</p>
145
+ </div>
146
+ )}
147
+ {acs.length > 0 && (
148
+ <div>
149
+ <p className="text-xs font-medium text-slate-500 mb-0.5">Acceptance Criteria</p>
150
+ <ul className="space-y-0.5">
151
+ {acs.map((ac, i) => (
152
+ <li key={i} className="flex items-start gap-1 text-xs text-slate-700">
153
+ <span className="text-slate-400 mt-0.5 flex-shrink-0">•</span>
154
+ <span>{ac}</span>
155
+ </li>
156
+ ))}
157
+ </ul>
158
+ </div>
159
+ )}
160
+ </div>
161
+ )}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ const SEVERITY_COLOR = {
167
+ critical: 'text-red-600',
168
+ major: 'text-orange-600',
169
+ minor: 'text-amber-600',
170
+ };
171
+ const SEVERITY_BG = {
172
+ critical: 'bg-red-50 border-red-100',
173
+ major: 'bg-orange-50 border-orange-100',
174
+ minor: 'bg-amber-50 border-amber-100',
175
+ };
176
+
177
+ function scoreBadgeClass(score) {
178
+ if (score >= 95) return 'bg-green-100 text-green-700';
179
+ if (score >= 80) return 'bg-amber-100 text-amber-700';
180
+ return 'bg-red-100 text-red-700';
181
+ }
182
+
183
+ /**
184
+ * RefineWorkItemPopup
185
+ * Three-phase popup:
186
+ * configure → running (while LLM runs) → results (accept / discard)
187
+ *
188
+ * Props:
189
+ * item - full work item from CardDetailModal (includes metadata.validationResult)
190
+ * refineProgress - { itemId, jobId, message } from WS, or null
191
+ * refineResult - { itemId, jobId, result } from WS, or null
192
+ * refineError - { itemId, jobId, error } from WS, or null
193
+ * onClose() - close popup without accepting
194
+ * onAccepted() - called after successful apply (triggers detail reload)
195
+ */
196
+ export function RefineWorkItemPopup({
197
+ item,
198
+ refineProgress,
199
+ refineResult,
200
+ refineError,
201
+ onClose,
202
+ onAccepted,
203
+ }) {
204
+ // ── Configure state ─────────────────────────────────────────────────────────
205
+ const vr = item?.metadata?.validationResult;
206
+ const allIssues = [
207
+ ...(vr?.criticalIssues || []).map((i) => ({ ...i, severity: 'critical' })),
208
+ ...(vr?.majorIssues || []).map((i) => ({ ...i, severity: 'major' })),
209
+ ...(vr?.minorIssues || []).map((i) => ({ ...i, severity: 'minor' })),
210
+ ];
211
+
212
+ // Pre-select all critical issues
213
+ const [selectedIssueIndices, setSelectedIssueIndices] = useState(
214
+ () => new Set(
215
+ allIssues
216
+ .map((issue, i) => (issue.severity === 'critical' ? i : null))
217
+ .filter((i) => i !== null)
218
+ )
219
+ );
220
+ const [refinementRequest, setRefinementRequest] = useState('');
221
+ const [models, setModels] = useState([]);
222
+ const [selectedModelId, setSelectedModelId] = useState('');
223
+ const [selectedValidatorModelId, setSelectedValidatorModelId] = useState('');
224
+ const [configError, setConfigError] = useState('');
225
+
226
+ // Local "refine started, waiting for first WS progress" state
227
+ const [refining, setRefining] = useState(false);
228
+
229
+ // ── Running state ───────────────────────────────────────────────────────────
230
+ const [progressLog, setProgressLog] = useState([]);
231
+ const progressEndRef = useRef(null);
232
+
233
+ // ── Results state ───────────────────────────────────────────────────────────
234
+ const [storyCheckboxes, setStoryCheckboxes] = useState(null); // { [storyImpactIndex]: boolean }
235
+ const [accepting, setAccepting] = useState(false);
236
+ const [acceptError, setAcceptError] = useState('');
237
+
238
+ // ── Phase derivation ────────────────────────────────────────────────────────
239
+ const phase = refineResult
240
+ ? 'results'
241
+ : refining || refineProgress
242
+ ? 'running'
243
+ : 'configure';
244
+
245
+ // ── Effects ─────────────────────────────────────────────────────────────────
246
+
247
+ // Load models on mount
248
+ useEffect(() => {
249
+ Promise.all([getModels(), getSettings()])
250
+ .then(([data, settings]) => {
251
+ setModels(data);
252
+ const ready = data.filter((m) => settings.apiKeys?.[normalizeProvider(m.provider)]?.isSet);
253
+ const isPro = (id) => /pro|opus|sonnet/i.test(id);
254
+ const isLite = (id) => /flash|lite|haiku|mini/i.test(id);
255
+ const generator = ready.find((m) => isPro(m.modelId)) || ready[0];
256
+ const genId = generator ? generator.modelId : (data.length > 0 ? data[0].modelId : '');
257
+ setSelectedModelId(genId);
258
+ const validator = ready.find((m) => isLite(m.modelId) && m.modelId !== genId)
259
+ || ready.find((m) => m.modelId !== genId)
260
+ || generator;
261
+ setSelectedValidatorModelId(validator ? validator.modelId : genId);
262
+ })
263
+ .catch(() => setConfigError('Failed to load models.'));
264
+ }, []);
265
+
266
+ // Accumulate progress messages
267
+ useEffect(() => {
268
+ if (refineProgress?.message) {
269
+ setProgressLog((prev) => [...prev, refineProgress.message]);
270
+ }
271
+ }, [refineProgress]);
272
+
273
+ // Auto-scroll progress log
274
+ useEffect(() => {
275
+ progressEndRef.current?.scrollIntoView({ behavior: 'smooth' });
276
+ }, [progressLog]);
277
+
278
+ // When result arrives, reset refining and initialise story checkboxes
279
+ useEffect(() => {
280
+ if (!refineResult) return;
281
+ setRefining(false);
282
+ const impacts = refineResult.result?.storyImpacts || [];
283
+ const init = {};
284
+ impacts.forEach((impact, i) => {
285
+ // Pre-check: all impacted updates and all new stories
286
+ init[i] = impact.type === 'new' || impact.impacted === true;
287
+ });
288
+ setStoryCheckboxes(init);
289
+ }, [refineResult]);
290
+
291
+ // When error arrives, go back to configure with the error message shown
292
+ useEffect(() => {
293
+ if (!refineError) return;
294
+ setRefining(false);
295
+ setConfigError(
296
+ typeof refineError.error === 'string'
297
+ ? refineError.error
298
+ : 'Refinement failed — please try again.'
299
+ );
300
+ }, [refineError]);
301
+
302
+ // ── Handlers ─────────────────────────────────────────────────────────────────
303
+
304
+ function toggleIssue(i) {
305
+ setSelectedIssueIndices((prev) => {
306
+ const next = new Set(prev);
307
+ if (next.has(i)) next.delete(i);
308
+ else next.add(i);
309
+ return next;
310
+ });
311
+ }
312
+
313
+ async function handleRefine() {
314
+ const selectedModel = models.find((m) => m.modelId === selectedModelId);
315
+ const selectedValidatorModel = models.find(
316
+ (m) => m.modelId === selectedValidatorModelId
317
+ );
318
+ if (!selectedModel || !selectedValidatorModel) return;
319
+
320
+ const selectedIssues = allIssues.filter((_, i) => selectedIssueIndices.has(i));
321
+
322
+ setRefining(true);
323
+ setConfigError('');
324
+ setProgressLog([]);
325
+
326
+ try {
327
+ await refineWorkItem(item.id, {
328
+ refinementRequest,
329
+ selectedIssues,
330
+ modelId: selectedModelId,
331
+ provider: selectedModel.provider,
332
+ validatorModelId: selectedValidatorModelId,
333
+ validatorProvider: selectedValidatorModel.provider,
334
+ });
335
+ // Job started — WebSocket broadcasts refine:progress / refine:complete / refine:error
336
+ } catch (err) {
337
+ setRefining(false);
338
+ setConfigError(err.message || 'Failed to start refinement.');
339
+ }
340
+ }
341
+
342
+ async function handleAccept() {
343
+ if (!refineResult) return;
344
+ const { proposedItem, storyImpacts = [] } = refineResult.result;
345
+
346
+ const acceptedStoryChanges = storyImpacts
347
+ .filter((_, i) => storyCheckboxes?.[i])
348
+ .map((impact) => ({
349
+ type: impact.type,
350
+ storyId: impact.storyId ?? null,
351
+ proposedStory: impact.proposedStory,
352
+ }));
353
+
354
+ setAccepting(true);
355
+ setAcceptError('');
356
+ try {
357
+ await applyWorkItemChanges(item.id, proposedItem, acceptedStoryChanges);
358
+ onAccepted?.();
359
+ } catch (err) {
360
+ setAcceptError(err.message || 'Failed to apply changes.');
361
+ setAccepting(false);
362
+ }
363
+ }
364
+
365
+ // ── Derived values ──────────────────────────────────────────────────────────
366
+
367
+ const selectedModel = models.find((m) => m.modelId === selectedModelId);
368
+ const selectedValidatorModel = models.find(
369
+ (m) => m.modelId === selectedValidatorModelId
370
+ );
371
+ const canRefine =
372
+ !refining &&
373
+ !!selectedModelId &&
374
+ !!selectedValidatorModelId &&
375
+ !!selectedModel &&
376
+ !!selectedValidatorModel;
377
+ const selectedIssueCount = selectedIssueIndices.size;
378
+
379
+ // Results phase derived values
380
+ const resultData = refineResult?.result;
381
+ const storyImpacts = resultData?.storyImpacts || [];
382
+ const updateImpacts = storyImpacts
383
+ .map((impact, i) => ({ impact, i }))
384
+ .filter(({ impact }) => impact.type === 'update' && impact.impacted);
385
+ const newImpacts = storyImpacts
386
+ .map((impact, i) => ({ impact, i }))
387
+ .filter(({ impact }) => impact.type === 'new');
388
+
389
+ const checkedStoryCount = Object.values(storyCheckboxes || {}).filter(Boolean).length;
390
+ const isEpic = item.type === 'epic';
391
+
392
+ // ── Render ──────────────────────────────────────────────────────────────────
393
+
394
+ return (
395
+ <div
396
+ className="fixed inset-0 z-[80] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
397
+ onClick={(e) => {
398
+ if (e.target === e.currentTarget && phase !== 'running') onClose();
399
+ }}
400
+ >
401
+ <div
402
+ className={`bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden w-full transition-all duration-200 ${
403
+ phase === 'results' ? 'max-w-2xl' : 'max-w-xl'
404
+ }`}
405
+ style={{ maxHeight: '88vh' }}
406
+ >
407
+ {/* ── Header ─────────────────────────────────────────────────────────── */}
408
+ <div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
409
+ <div>
410
+ <h3 className="text-sm font-semibold text-slate-900 flex items-center gap-1.5">
411
+ <Wand2 className="w-3.5 h-3.5 text-violet-600" />
412
+ Refine with AI
413
+ {phase !== 'configure' && (
414
+ <span
415
+ className={`ml-1 text-xs font-medium px-1.5 py-0.5 rounded ${
416
+ phase === 'running'
417
+ ? 'bg-blue-100 text-blue-700'
418
+ : 'bg-violet-100 text-violet-700'
419
+ }`}
420
+ >
421
+ {phase === 'running' ? 'Running…' : 'Results'}
422
+ </span>
423
+ )}
424
+ </h3>
425
+ <p className="text-xs text-slate-400 mt-0.5 truncate max-w-xs">{item.name}</p>
426
+ </div>
427
+ {phase !== 'running' && (
428
+ <button
429
+ type="button"
430
+ onClick={onClose}
431
+ className="text-slate-400 hover:text-slate-600 transition-colors ml-4 flex-shrink-0"
432
+ aria-label="Close"
433
+ >
434
+ <X className="w-4 h-4" />
435
+ </button>
436
+ )}
437
+ </div>
438
+
439
+ {/* ── CONFIGURE PHASE ────────────────────────────────────────────────── */}
440
+ {phase === 'configure' && (
441
+ <div className="flex-1 overflow-y-auto px-5 py-4 space-y-4 min-h-0">
442
+ {/* Issues checklist */}
443
+ {allIssues.length > 0 ? (
444
+ <div>
445
+ <div className="flex items-center justify-between mb-2">
446
+ <p className="text-xs font-semibold text-slate-600 uppercase tracking-wide">
447
+ Validation Issues ({allIssues.length})
448
+ </p>
449
+ <div className="flex items-center gap-2 text-xs">
450
+ <button
451
+ type="button"
452
+ onClick={() =>
453
+ setSelectedIssueIndices(
454
+ new Set(allIssues.map((_, i) => i))
455
+ )
456
+ }
457
+ className="text-violet-600 hover:text-violet-800 transition-colors"
458
+ >
459
+ All
460
+ </button>
461
+ <span className="text-slate-300">|</span>
462
+ <button
463
+ type="button"
464
+ onClick={() => setSelectedIssueIndices(new Set())}
465
+ className="text-slate-400 hover:text-slate-600 transition-colors"
466
+ >
467
+ None
468
+ </button>
469
+ </div>
470
+ </div>
471
+ <ul className="space-y-1.5 max-h-52 overflow-y-auto pr-0.5">
472
+ {allIssues.map((issue, i) => (
473
+ <li
474
+ key={i}
475
+ onClick={() => toggleIssue(i)}
476
+ className={`rounded-lg border px-3 py-2 cursor-pointer flex items-start gap-2.5 transition-shadow ${
477
+ selectedIssueIndices.has(i) ? 'ring-2 ring-violet-400' : ''
478
+ } ${SEVERITY_BG[issue.severity] ?? 'bg-slate-50 border-slate-100'}`}
479
+ >
480
+ <input
481
+ type="checkbox"
482
+ checked={selectedIssueIndices.has(i)}
483
+ onChange={() => toggleIssue(i)}
484
+ onClick={(e) => e.stopPropagation()}
485
+ className="mt-0.5 flex-shrink-0 accent-violet-600"
486
+ />
487
+ <div className="flex-1 min-w-0">
488
+ <span
489
+ className={`text-xs font-semibold uppercase ${
490
+ SEVERITY_COLOR[issue.severity] ?? 'text-slate-500'
491
+ }`}
492
+ >
493
+ {issue.severity}
494
+ </span>
495
+ <p className="text-xs text-slate-800 mt-0.5 leading-snug">
496
+ {issue.description}
497
+ </p>
498
+ {issue.suggestion && (
499
+ <p className="text-xs text-slate-500 mt-0.5 leading-snug">
500
+ <span className="font-medium">Suggestion:</span>{' '}
501
+ {issue.suggestion}
502
+ </p>
503
+ )}
504
+ </div>
505
+ </li>
506
+ ))}
507
+ </ul>
508
+ </div>
509
+ ) : (
510
+ <div className="flex items-center gap-2 px-3 py-2.5 bg-green-50 border border-green-100 rounded-lg">
511
+ <Check className="w-3.5 h-3.5 text-green-600 flex-shrink-0" />
512
+ <p className="text-xs text-green-700">
513
+ No validation issues found — you can still refine with a custom request.
514
+ </p>
515
+ </div>
516
+ )}
517
+
518
+ {/* Free-text request */}
519
+ <div>
520
+ <label className="block text-xs font-medium text-slate-500 mb-1">
521
+ Refinement request{' '}
522
+ <span className="text-slate-400">(optional)</span>
523
+ </label>
524
+ <textarea
525
+ value={refinementRequest}
526
+ onChange={(e) => setRefinementRequest(e.target.value)}
527
+ rows={3}
528
+ placeholder={
529
+ isEpic
530
+ ? 'E.g. Sharpen the scope, add examples, expand the features list…'
531
+ : 'E.g. Make acceptance criteria more testable, add edge cases…'
532
+ }
533
+ className="w-full rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 resize-none"
534
+ />
535
+ </div>
536
+
537
+ {/* Model selectors */}
538
+ <div className="grid grid-cols-2 gap-3">
539
+ <ModelSelect
540
+ label="Generator Model"
541
+ value={selectedModelId}
542
+ onChange={setSelectedModelId}
543
+ models={models}
544
+ disabled={false}
545
+ />
546
+ <ModelSelect
547
+ label="Validator Model"
548
+ value={selectedValidatorModelId}
549
+ onChange={setSelectedValidatorModelId}
550
+ models={models}
551
+ disabled={false}
552
+ />
553
+ </div>
554
+
555
+ {configError && (
556
+ <div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-100 rounded-lg">
557
+ <AlertTriangle className="w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" />
558
+ <p className="text-xs text-red-700">{configError}</p>
559
+ </div>
560
+ )}
561
+ </div>
562
+ )}
563
+
564
+ {/* ── RUNNING PHASE ──────────────────────────────────────────────────── */}
565
+ {phase === 'running' && (
566
+ <div className="flex-1 flex flex-col overflow-hidden px-5 py-4 min-h-0">
567
+ <div className="flex flex-col items-center gap-3 mb-4 flex-shrink-0">
568
+ <span
569
+ className="w-8 h-8 border-2 border-violet-200 border-t-violet-600 rounded-full animate-spin"
570
+ />
571
+ <p className="text-sm font-medium text-slate-700">
572
+ Refining {item.type}…
573
+ </p>
574
+ </div>
575
+ <div className="flex-1 overflow-y-auto bg-slate-50 rounded-lg px-3 py-2.5 space-y-1 min-h-0">
576
+ {progressLog.length === 0 ? (
577
+ <p className="text-xs text-slate-400 italic">Starting…</p>
578
+ ) : (
579
+ progressLog.map((msg, i) => (
580
+ <p key={i} className="text-xs text-slate-600 leading-snug">
581
+ {msg}
582
+ </p>
583
+ ))
584
+ )}
585
+ <div ref={progressEndRef} />
586
+ </div>
587
+ </div>
588
+ )}
589
+
590
+ {/* ── RESULTS PHASE ──────────────────────────────────────────────────── */}
591
+ {phase === 'results' && resultData && (
592
+ <div className="flex-1 overflow-y-auto px-5 py-4 space-y-5 min-h-0">
593
+ {/* Score comparison */}
594
+ {(() => {
595
+ const oldScore = item?.metadata?.validationResult?.averageScore;
596
+ const newScore =
597
+ resultData.validationResult?.averageScore ??
598
+ resultData.proposedItem?.metadata?.validationResult?.averageScore;
599
+ if (newScore == null) return null;
600
+ return (
601
+ <div className="flex items-center gap-3">
602
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
603
+ Validation Score
604
+ </p>
605
+ <div className="flex items-center gap-2">
606
+ {oldScore != null && (
607
+ <>
608
+ <span
609
+ className={`text-xs font-bold px-2 py-0.5 rounded-full ${scoreBadgeClass(oldScore)}`}
610
+ >
611
+ {oldScore}/100
612
+ </span>
613
+ <span className="text-slate-400 text-xs">→</span>
614
+ </>
615
+ )}
616
+ <span
617
+ className={`text-xs font-bold px-2 py-0.5 rounded-full ${scoreBadgeClass(newScore)}`}
618
+ >
619
+ {newScore}/100
620
+ </span>
621
+ </div>
622
+ </div>
623
+ );
624
+ })()}
625
+
626
+ {/* Field diffs */}
627
+ <div>
628
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
629
+ Proposed Changes
630
+ </p>
631
+ <FieldDiff
632
+ label="Description"
633
+ before={resultData.originalItem?.description}
634
+ after={resultData.proposedItem?.description}
635
+ />
636
+ {isEpic && (
637
+ <FieldDiff
638
+ label="Features"
639
+ before={resultData.originalItem?.features}
640
+ after={resultData.proposedItem?.features}
641
+ />
642
+ )}
643
+ {!isEpic && (
644
+ <FieldDiff
645
+ label="Acceptance Criteria"
646
+ before={resultData.originalItem?.acceptance ?? resultData.originalItem?.acceptanceCriteria}
647
+ after={resultData.proposedItem?.acceptance ?? resultData.proposedItem?.acceptanceCriteria}
648
+ />
649
+ )}
650
+ {resultData.originalItem?.description ===
651
+ resultData.proposedItem?.description &&
652
+ !resultData.originalItem?.features &&
653
+ !resultData.originalItem?.acceptanceCriteria && (
654
+ <p className="text-xs text-slate-400 italic">
655
+ No textual changes detected — check dependencies or metadata.
656
+ </p>
657
+ )}
658
+ </div>
659
+
660
+ {/* Existing stories to update */}
661
+ {updateImpacts.length > 0 && (
662
+ <div>
663
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
664
+ Existing Stories to Update ({updateImpacts.length})
665
+ </p>
666
+ <div className="space-y-2">
667
+ {updateImpacts.map(({ impact, i }) => (
668
+ <StoryUpdateCard
669
+ key={i}
670
+ impact={impact}
671
+ checked={storyCheckboxes?.[i] ?? true}
672
+ onToggle={() =>
673
+ setStoryCheckboxes((prev) => ({
674
+ ...prev,
675
+ [i]: !prev?.[i],
676
+ }))
677
+ }
678
+ />
679
+ ))}
680
+ </div>
681
+ </div>
682
+ )}
683
+
684
+ {/* New stories to add */}
685
+ {newImpacts.length > 0 && (
686
+ <div>
687
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
688
+ New Stories to Add ({newImpacts.length})
689
+ </p>
690
+ <div className="space-y-2">
691
+ {newImpacts.map(({ impact, i }) => (
692
+ <NewStoryCard
693
+ key={i}
694
+ story={impact.proposedStory}
695
+ checked={storyCheckboxes?.[i] ?? true}
696
+ onToggle={() =>
697
+ setStoryCheckboxes((prev) => ({
698
+ ...prev,
699
+ [i]: !prev?.[i],
700
+ }))
701
+ }
702
+ />
703
+ ))}
704
+ </div>
705
+ </div>
706
+ )}
707
+
708
+ {acceptError && (
709
+ <div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-100 rounded-lg">
710
+ <AlertTriangle className="w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" />
711
+ <p className="text-xs text-red-700">{acceptError}</p>
712
+ </div>
713
+ )}
714
+ </div>
715
+ )}
716
+
717
+ {/* ── Footer ─────────────────────────────────────────────────────────── */}
718
+ <div className="px-5 py-3 border-t border-slate-100 flex-shrink-0 flex items-center justify-between gap-2">
719
+ {phase === 'configure' && (
720
+ <>
721
+ <button
722
+ type="button"
723
+ onClick={onClose}
724
+ className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
725
+ >
726
+ Cancel
727
+ </button>
728
+ <button
729
+ type="button"
730
+ onClick={handleRefine}
731
+ disabled={!canRefine}
732
+ className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium text-white bg-violet-600 rounded-lg hover:bg-violet-700 transition-colors disabled:opacity-40"
733
+ >
734
+ <Wand2 className="w-3.5 h-3.5" />
735
+ {selectedIssueCount > 0
736
+ ? `Refine (${selectedIssueCount} issue${selectedIssueCount !== 1 ? 's' : ''})`
737
+ : 'Refine'}
738
+ </button>
739
+ </>
740
+ )}
741
+
742
+ {phase === 'running' && (
743
+ <p className="w-full text-center text-xs text-slate-400 italic">
744
+ Waiting for LLM response — please keep this window open.
745
+ </p>
746
+ )}
747
+
748
+ {phase === 'results' && (
749
+ <>
750
+ <button
751
+ type="button"
752
+ onClick={onClose}
753
+ disabled={accepting}
754
+ className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-40"
755
+ >
756
+ Discard
757
+ </button>
758
+ <button
759
+ type="button"
760
+ onClick={handleAccept}
761
+ disabled={accepting}
762
+ className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium text-white bg-violet-600 rounded-lg hover:bg-violet-700 transition-colors disabled:opacity-40"
763
+ >
764
+ {accepting ? (
765
+ <>
766
+ <span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
767
+ Applying…
768
+ </>
769
+ ) : (
770
+ <>
771
+ <Check className="w-3.5 h-3.5" />
772
+ {checkedStoryCount > 0
773
+ ? `Accept + ${checkedStoryCount} story change${checkedStoryCount !== 1 ? 's' : ''}`
774
+ : 'Accept Changes'}
775
+ </>
776
+ )}
777
+ </button>
778
+ </>
779
+ )}
780
+ </div>
781
+ </div>
782
+ </div>
783
+ );
784
+ }