@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,651 @@
1
+ import { useEffect, useState, useMemo, useRef } from 'react';
2
+ import { Pencil, Check, X, BookOpen, Settings, DollarSign } from 'lucide-react';
3
+ import { getHealth, getBoardTitle, updateBoardTitle, getDocsUrl, getSettings, getModels, getCostSummary, getProjectStatus, getCeremonyStatus, continuePastCostLimit, continueAfterQuota, cancelCeremony } from './lib/api';
4
+ import { useWebSocket } from './hooks/useWebSocket';
5
+ import { useKanbanStore } from './store/kanbanStore';
6
+ import { useFilterStore } from './store/filterStore';
7
+ import { useCeremonyStore } from './store/ceremonyStore';
8
+ import { useSprintPlanningStore } from './store/sprintPlanningStore';
9
+ import { useProcessStore } from './store/processStore';
10
+ import { KanbanBoard } from './components/kanban/KanbanBoard';
11
+ import { ProjectFileEditorPopup } from './components/ProjectFileEditorPopup';
12
+ import { FilterToolbar } from './components/kanban/FilterToolbar';
13
+ import { ProcessMonitorBar } from './components/process/ProcessMonitorBar';
14
+ import { CardDetailModal } from './components/kanban/CardDetailModal';
15
+ import { SponsorCallModal } from './components/ceremony/SponsorCallModal';
16
+ import { SprintPlanningModal } from './components/ceremony/SprintPlanningModal';
17
+ import { EpicStorySelectionModal } from './components/ceremony/EpicStorySelectionModal';
18
+ import { SettingsModal } from './components/settings/SettingsModal';
19
+ import { CostModal } from './components/stats/CostModal';
20
+ import { groupItemsByColumn } from './lib/status-grouping';
21
+
22
+ function App() {
23
+ const [health, setHealth] = useState(null);
24
+ const [selectedItem, setSelectedItem] = useState(null);
25
+ const [modalOpen, setModalOpen] = useState(false);
26
+
27
+ // Board title state
28
+ const [boardTitle, setBoardTitle] = useState('AVC Kanban Board');
29
+ const [docsUrl, setDocsUrl] = useState('http://localhost:4173');
30
+ const [editingTitle, setEditingTitle] = useState(false);
31
+ const [titleInput, setTitleInput] = useState('');
32
+ const titleInputRef = useRef(null);
33
+
34
+ // Settings modal state
35
+ const [settingsOpen, setSettingsOpen] = useState(false);
36
+ const [settingsSnapshot, setSettingsSnapshot] = useState(null);
37
+ const [modelsSnapshot, setModelsSnapshot] = useState([]);
38
+
39
+ // Cost chip + modal state
40
+ const [costSummary, setCostSummary] = useState(null);
41
+ const [costModalOpen, setCostModalOpen] = useState(false);
42
+
43
+ // Cost-limit pause dialog — { cost, threshold, runningType } when ceremony hits limit
44
+ const [costLimitPending, setCostLimitPending] = useState(null);
45
+
46
+ // Quota-limit pause dialog — { provider, model, errMsg, validatorName, runningType } when quota exceeded
47
+ const [quotaLimitPending, setQuotaLimitPending] = useState(null);
48
+
49
+ // Refine work item state — { itemId, jobId, message } / { itemId, jobId, result } / { itemId, jobId, error }
50
+ const [refineProgress, setRefineProgress] = useState(null);
51
+ const [refineResult, setRefineResult] = useState(null);
52
+ const [refineError, setRefineError] = useState(null);
53
+
54
+ // Project file status + editor popup
55
+ const [projectFilesStatus, setProjectFilesStatus] = useState({ docExists: false });
56
+ const [projectFilesLoaded, setProjectFilesLoaded] = useState(false);
57
+ const [editingProjectFile, setEditingProjectFile] = useState(null); // 'doc' | null
58
+
59
+ // Zustand stores
60
+ const { workItems, loadWorkItems, loading, error, setCeremonyActive } = useKanbanStore();
61
+ const { typeFilters, searchQuery } = useFilterStore();
62
+ const {
63
+ isOpen: ceremonyOpen,
64
+ openWizard,
65
+ resetWizard,
66
+ ceremonyStatus,
67
+ setCeremonyStatus,
68
+ setCeremonyResult,
69
+ setCeremonyError,
70
+ appendProgress,
71
+ appendMissionProgress,
72
+ setProgressLog: setCeremonyProgressLog,
73
+ setWizardStep,
74
+ setPaused: setCeremonyPaused,
75
+ setProcessId: setCeremonyProcessId,
76
+ } = useCeremonyStore();
77
+
78
+ const {
79
+ isOpen: sprintPlanningOpen,
80
+ openModal: openSprintPlanning,
81
+ closeModal: closeSprintPlanning,
82
+ reopenModal: reopenSprintPlanning,
83
+ setStep: setSprintPlanningStep,
84
+ setStatus: setSprintPlanningStatus,
85
+ appendProgress: appendSprintPlanningProgress,
86
+ setProgressLog: setSprintPlanningProgressLog,
87
+ setResult: setSprintPlanningResult,
88
+ setError: setSprintPlanningError,
89
+ status: sprintPlanningStatus,
90
+ setPaused: setSprintPlanningPaused,
91
+ setProcessId: setSprintPlanningProcessId,
92
+ setDecomposedHierarchy: setSprintPlanningDecomposedHierarchy,
93
+ } = useSprintPlanningStore();
94
+
95
+ const { handleProcessMessage } = useProcessStore();
96
+
97
+ // Get filtered items for navigation
98
+ const filteredItems = useMemo(() => {
99
+ let filtered = workItems.filter((item) => typeFilters[item.type]);
100
+
101
+ if (searchQuery.trim()) {
102
+ const query = searchQuery.toLowerCase();
103
+ filtered = filtered.filter(
104
+ (item) =>
105
+ item.name.toLowerCase().includes(query) ||
106
+ item.id.toLowerCase().includes(query) ||
107
+ (item.description && item.description.toLowerCase().includes(query)) ||
108
+ (item.epicName && item.epicName.toLowerCase().includes(query))
109
+ );
110
+ }
111
+
112
+ return filtered;
113
+ }, [workItems, typeFilters, searchQuery]);
114
+
115
+ // WebSocket connection for real-time updates + ceremony events
116
+ const { wsStatus } = useWebSocket({
117
+ onMessage: (message) => {
118
+ if (message.type === 'refresh' || message.type === 'work-item-update') {
119
+ loadWorkItems();
120
+ } else if (message.type === 'ceremony:progress') {
121
+ appendProgress({ type: 'progress', message: message.message });
122
+ } else if (message.type === 'ceremony:substep') {
123
+ appendProgress({ type: 'substep', substep: message.substep, meta: message.meta });
124
+ } else if (message.type === 'ceremony:detail') {
125
+ appendProgress({ type: 'detail', detail: message.detail });
126
+ } else if (message.type === 'ceremony:complete') {
127
+ setCostLimitPending(null);
128
+ setQuotaLimitPending(null);
129
+ setCeremonyStatus('complete');
130
+ setCeremonyResult(message.result);
131
+ setWizardStep(7);
132
+ loadWorkItems();
133
+ getProjectStatus().then(setProjectFilesStatus).catch(() => { });
134
+ } else if (message.type === 'ceremony:error') {
135
+ setCeremonyStatus('error');
136
+ setCeremonyError(message.error);
137
+ } else if (message.type === 'mission:progress') {
138
+ appendMissionProgress({ step: message.step, message: message.message });
139
+ } else if (message.type === 'ceremony:cost-limit') {
140
+ setCostLimitPending({ cost: message.cost, threshold: message.threshold, runningType: message.runningType });
141
+ } else if (message.type === 'ceremony:quota-limit') {
142
+ setQuotaLimitPending({ provider: message.provider, model: message.model, errMsg: message.errMsg, validatorName: message.validatorName, runningType: message.runningType });
143
+ } else if (message.type === 'cost:update') {
144
+ getCostSummary().then(setCostSummary).catch(() => { });
145
+ } else if (message.type === 'sprint-planning:progress') {
146
+ appendSprintPlanningProgress({ type: 'progress', message: message.message });
147
+ } else if (message.type === 'sprint-planning:substep') {
148
+ appendSprintPlanningProgress({ type: 'substep', substep: message.substep, meta: message.meta });
149
+ } else if (message.type === 'sprint-planning:detail') {
150
+ appendSprintPlanningProgress({ type: 'detail', detail: message.detail });
151
+ } else if (message.type === 'sprint-planning:decomposition-complete') {
152
+ setSprintPlanningDecomposedHierarchy(message.hierarchy);
153
+ setSprintPlanningStatus('awaiting-selection');
154
+ setSprintPlanningStep(3);
155
+ } else if (message.type === 'sprint-planning:complete') {
156
+ setCostLimitPending(null);
157
+ setQuotaLimitPending(null);
158
+ setSprintPlanningStatus('complete');
159
+ setSprintPlanningResult(message.result);
160
+ setSprintPlanningDecomposedHierarchy(null);
161
+ setSprintPlanningStep(4);
162
+ setCeremonyActive(false);
163
+ loadWorkItems();
164
+ } else if (message.type === 'sprint-planning:error') {
165
+ setSprintPlanningStatus('error');
166
+ setSprintPlanningError(message.error);
167
+ setCeremonyActive(false);
168
+ } else if (message.type === 'sprint-planning:paused') {
169
+ setSprintPlanningPaused(true);
170
+ } else if (message.type === 'sprint-planning:resumed') {
171
+ setSprintPlanningPaused(false);
172
+ } else if (message.type === 'sprint-planning:cancelled') {
173
+ setCostLimitPending(null);
174
+ setQuotaLimitPending(null);
175
+ setSprintPlanningStatus('idle');
176
+ setSprintPlanningStep(1);
177
+ setSprintPlanningPaused(false);
178
+ setSprintPlanningDecomposedHierarchy(null);
179
+ setCeremonyActive(false);
180
+ } else if (message.type === 'ceremony:paused') {
181
+ setCeremonyPaused(true);
182
+ } else if (message.type === 'ceremony:resumed') {
183
+ setCeremonyPaused(false);
184
+ } else if (message.type === 'ceremony:cancelled') {
185
+ setCostLimitPending(null);
186
+ setQuotaLimitPending(null);
187
+ setCeremonyStatus('idle');
188
+ setWizardStep(1);
189
+ setCeremonyPaused(false);
190
+ } else if (message.type?.startsWith('seed:') || message.type?.startsWith('run-task:')) {
191
+ // Dispatch seed/run events to components via CustomEvent
192
+ window.dispatchEvent(new CustomEvent('avc-ws-message', { detail: message }));
193
+ // Refresh board on completion or error
194
+ if (message.type.endsWith(':complete') || message.type.endsWith(':error')) {
195
+ loadWorkItems();
196
+ }
197
+ } else if (message.type === 'refresh') {
198
+ loadWorkItems();
199
+ } else if (message.type === 'refine:progress') {
200
+ setRefineProgress({ itemId: message.itemId, jobId: message.jobId, message: message.message });
201
+ } else if (message.type === 'refine:complete') {
202
+ setRefineProgress(null);
203
+ setRefineResult({ itemId: message.itemId, jobId: message.jobId, result: message.result });
204
+ } else if (message.type === 'refine:error') {
205
+ setRefineProgress(null);
206
+ setRefineError({ itemId: message.itemId, jobId: message.jobId, error: message.error });
207
+ } else if (message.type === 'process:started') {
208
+ handleProcessMessage(message);
209
+ if (message.processType === 'sprint-planning') {
210
+ setSprintPlanningProcessId(message.processId);
211
+ setCeremonyActive(true);
212
+ } else if (message.processType === 'sponsor-call') {
213
+ setCeremonyProcessId(message.processId);
214
+ }
215
+ } else if (message.type === 'process:list' || message.type === 'process:status') {
216
+ handleProcessMessage(message);
217
+ } else if (message.type === 'ceremony:sync') {
218
+ // Server sends this on WebSocket connect when a ceremony is already running.
219
+ // Restores client state without requiring an HTTP round-trip.
220
+ const cs = message.ceremonyStatus;
221
+ if (cs?.status === 'running' || cs?.status === 'cost-limit-pending' || cs?.status === 'quota-limit-pending' || cs?.status === 'awaiting-selection') {
222
+ if (cs.runningType === 'sprint-planning') {
223
+ setCeremonyActive(true);
224
+ if (cs.status === 'awaiting-selection') {
225
+ setSprintPlanningStatus('awaiting-selection');
226
+ setSprintPlanningDecomposedHierarchy(cs.decomposedHierarchy || null);
227
+ setSprintPlanningStep(3);
228
+ } else {
229
+ setSprintPlanningStatus('running');
230
+ }
231
+ if (cs.processId) setSprintPlanningProcessId(cs.processId);
232
+ if (cs.progress?.length) setSprintPlanningProgressLog(cs.progress);
233
+ } else if (cs.runningType === 'sponsor-call') {
234
+ setCeremonyStatus('running');
235
+ if (cs.processId) setCeremonyProcessId(cs.processId);
236
+ if (cs.progress?.length) setCeremonyProgressLog(cs.progress);
237
+ }
238
+ if (cs.status === 'cost-limit-pending' && cs.costLimitInfo) {
239
+ setCostLimitPending({ ...cs.costLimitInfo, runningType: cs.runningType });
240
+ }
241
+ if (cs.status === 'quota-limit-pending' && cs.quotaLimitInfo) {
242
+ setQuotaLimitPending({ ...cs.quotaLimitInfo, runningType: cs.runningType });
243
+ }
244
+ }
245
+ }
246
+ },
247
+ });
248
+
249
+ // Initial data load
250
+ useEffect(() => {
251
+ const init = async () => {
252
+ try {
253
+ const [healthData, title, docsUrlData, filesStatus, ceremonyState] = await Promise.all([
254
+ getHealth(),
255
+ getBoardTitle(),
256
+ getDocsUrl(),
257
+ getProjectStatus(),
258
+ getCeremonyStatus().catch(() => null),
259
+ ]);
260
+ setHealth(healthData);
261
+ setBoardTitle(title);
262
+ setDocsUrl(docsUrlData);
263
+ setProjectFilesStatus(filesStatus);
264
+
265
+ // Restore running ceremony state BEFORE revealing projectFilesLoaded so the
266
+ // board never shows "Start" buttons for an already-running ceremony.
267
+ if (ceremonyState?.status === 'running' || ceremonyState?.status === 'cost-limit-pending' || ceremonyState?.status === 'quota-limit-pending' || ceremonyState?.status === 'awaiting-selection') {
268
+ if (ceremonyState.runningType === 'sprint-planning') {
269
+ if (ceremonyState.status === 'awaiting-selection') {
270
+ setSprintPlanningStatus('awaiting-selection');
271
+ setSprintPlanningDecomposedHierarchy(ceremonyState.decomposedHierarchy || null);
272
+ setSprintPlanningStep(3);
273
+ } else {
274
+ setSprintPlanningStatus('running');
275
+ }
276
+ if (ceremonyState.processId) setSprintPlanningProcessId(ceremonyState.processId);
277
+ if (ceremonyState.progress?.length) setSprintPlanningProgressLog(ceremonyState.progress);
278
+ } else if (ceremonyState.runningType === 'sponsor-call') {
279
+ setCeremonyStatus('running');
280
+ if (ceremonyState.processId) setCeremonyProcessId(ceremonyState.processId);
281
+ if (ceremonyState.progress?.length) setCeremonyProgressLog(ceremonyState.progress);
282
+ }
283
+ if (ceremonyState.status === 'cost-limit-pending' && ceremonyState.costLimitInfo) {
284
+ setCostLimitPending({ ...ceremonyState.costLimitInfo, runningType: ceremonyState.runningType });
285
+ }
286
+ if (ceremonyState.status === 'quota-limit-pending' && ceremonyState.quotaLimitInfo) {
287
+ setQuotaLimitPending({ ...ceremonyState.quotaLimitInfo, runningType: ceremonyState.runningType });
288
+ }
289
+ }
290
+
291
+ setProjectFilesLoaded(true);
292
+
293
+ await loadWorkItems();
294
+ } catch (err) {
295
+ console.error('Initialization error:', err);
296
+ }
297
+ };
298
+
299
+ init();
300
+ }, [loadWorkItems]);
301
+
302
+ // Focus input when editing starts
303
+ useEffect(() => {
304
+ if (editingTitle) {
305
+ titleInputRef.current?.focus();
306
+ titleInputRef.current?.select();
307
+ }
308
+ }, [editingTitle]);
309
+
310
+ // Poll cost summary every 60 seconds
311
+ useEffect(() => {
312
+ getCostSummary().then(setCostSummary).catch(() => { });
313
+ const id = setInterval(() => getCostSummary().then(setCostSummary).catch(() => { }), 60_000);
314
+ return () => clearInterval(id);
315
+ }, []);
316
+
317
+ // ── Title editing handlers ─────────────────────────────────────────────────
318
+
319
+ const startEditTitle = () => {
320
+ setTitleInput(boardTitle);
321
+ setEditingTitle(true);
322
+ };
323
+
324
+ const cancelEditTitle = () => {
325
+ setEditingTitle(false);
326
+ setTitleInput('');
327
+ };
328
+
329
+ const saveTitle = async () => {
330
+ const trimmed = titleInput.trim();
331
+ if (!trimmed || trimmed === boardTitle) {
332
+ cancelEditTitle();
333
+ return;
334
+ }
335
+ try {
336
+ await updateBoardTitle(trimmed);
337
+ setBoardTitle(trimmed);
338
+ } catch (err) {
339
+ console.error('Failed to save title:', err);
340
+ }
341
+ setEditingTitle(false);
342
+ setTitleInput('');
343
+ };
344
+
345
+ const handleTitleKeyDown = (e) => {
346
+ if (e.key === 'Enter') saveTitle();
347
+ if (e.key === 'Escape') cancelEditTitle();
348
+ };
349
+
350
+ // ── Settings modal ─────────────────────────────────────────────────────────
351
+
352
+ const [settingsInitialTab, setSettingsInitialTab] = useState('api-keys');
353
+ const openSettings = async (initialTab = 'api-keys') => {
354
+ try {
355
+ const [data, modelList] = await Promise.all([getSettings(), getModels()]);
356
+ setSettingsSnapshot(data);
357
+ setModelsSnapshot(modelList);
358
+ setSettingsInitialTab(initialTab);
359
+ setSettingsOpen(true);
360
+ } catch (err) {
361
+ console.error('Failed to load settings:', err);
362
+ }
363
+ };
364
+
365
+ const handleSettingsSaved = async () => {
366
+ try {
367
+ const data = await getSettings();
368
+ setSettingsSnapshot(data);
369
+ const title = await getBoardTitle();
370
+ setBoardTitle(title);
371
+ document.dispatchEvent(new CustomEvent('avc:settings-saved'));
372
+ } catch (err) {
373
+ console.error('Failed to refresh settings:', err);
374
+ }
375
+ };
376
+
377
+ // ── Card navigation ────────────────────────────────────────────────────────
378
+
379
+ const handleCardClick = (item) => {
380
+ setSelectedItem(item);
381
+ setModalOpen(true);
382
+ };
383
+
384
+ const handleNavigate = (direction) => {
385
+ if (!selectedItem || filteredItems.length === 0) return;
386
+
387
+ const currentIndex = filteredItems.findIndex((item) => item.id === selectedItem.id);
388
+ if (currentIndex === -1) return;
389
+
390
+ let newIndex;
391
+ if (direction === 'prev') {
392
+ newIndex = currentIndex > 0 ? currentIndex - 1 : filteredItems.length - 1;
393
+ } else {
394
+ newIndex = currentIndex < filteredItems.length - 1 ? currentIndex + 1 : 0;
395
+ }
396
+
397
+ setSelectedItem(filteredItems[newIndex]);
398
+ };
399
+
400
+ const handleModalClose = () => {
401
+ setModalOpen(false);
402
+ setTimeout(() => setSelectedItem(null), 200);
403
+ };
404
+
405
+ // ── Loading / error states ─────────────────────────────────────────────────
406
+
407
+ if (loading && !health) {
408
+ return (
409
+ <div className="flex items-center justify-center min-h-screen bg-slate-50">
410
+ <div className="text-center">
411
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
412
+ <p className="text-slate-600">Loading {boardTitle}...</p>
413
+ </div>
414
+ </div>
415
+ );
416
+ }
417
+
418
+ if (error && !health) {
419
+ return (
420
+ <div className="flex items-center justify-center min-h-screen bg-slate-50">
421
+ <div className="text-center max-w-md p-6 bg-white rounded-lg shadow-md">
422
+ <div className="text-red-600 text-5xl mb-4">⚠️</div>
423
+ <h2 className="text-xl font-semibold text-slate-900 mb-2">Connection Error</h2>
424
+ <p className="text-slate-600 mb-4">{error}</p>
425
+ <button
426
+ onClick={() => window.location.reload()}
427
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
428
+ >
429
+ Retry
430
+ </button>
431
+ </div>
432
+ </div>
433
+ );
434
+ }
435
+
436
+ return (
437
+ <div className="min-h-screen bg-slate-50 flex flex-col">
438
+ {/* Header */}
439
+ <header className="bg-white border-b border-slate-200 shadow-sm flex-shrink-0">
440
+ <div className="max-w-full px-4 sm:px-6 lg:px-8 py-4">
441
+ <div className="flex items-center justify-between">
442
+ <div>
443
+ {/* Editable board title */}
444
+ {editingTitle ? (
445
+ <div className="flex items-center gap-2">
446
+ <input
447
+ ref={titleInputRef}
448
+ value={titleInput}
449
+ onChange={(e) => setTitleInput(e.target.value)}
450
+ onKeyDown={handleTitleKeyDown}
451
+ className="text-2xl font-bold text-slate-900 bg-transparent border-b-2 border-blue-500 outline-none min-w-0 w-72"
452
+ />
453
+ <button
454
+ onClick={saveTitle}
455
+ className="text-green-600 hover:text-green-700 transition-colors"
456
+ title="Save"
457
+ >
458
+ <Check className="w-5 h-5" />
459
+ </button>
460
+ <button
461
+ onClick={cancelEditTitle}
462
+ className="text-slate-400 hover:text-slate-600 transition-colors"
463
+ title="Cancel"
464
+ >
465
+ <X className="w-5 h-5" />
466
+ </button>
467
+ </div>
468
+ ) : (
469
+ <button
470
+ onClick={startEditTitle}
471
+ className="group flex items-center gap-2 text-left"
472
+ title="Click to edit board title"
473
+ >
474
+ <h1 className="text-2xl font-bold text-slate-900 group-hover:text-slate-700 transition-colors">
475
+ {boardTitle}
476
+ </h1>
477
+ <Pencil className="w-4 h-4 text-slate-300 group-hover:text-slate-500 transition-colors flex-shrink-0" />
478
+ </button>
479
+ )}
480
+ <p className="text-sm text-slate-600 mt-1">
481
+ {health?.projectRoot || 'Loading...'}
482
+ </p>
483
+ </div>
484
+ <div className="flex items-center gap-4">
485
+ {/* Real-time updates status */}
486
+ <div className="flex items-center gap-2">
487
+ <div
488
+ className={`w-2 h-2 rounded-full ${wsStatus === 'connected'
489
+ ? 'bg-green-500 animate-pulse'
490
+ : wsStatus === 'connecting'
491
+ ? 'bg-amber-400 animate-pulse'
492
+ : 'bg-slate-400'
493
+ }`}
494
+ ></div>
495
+ <span className="text-sm text-slate-500">
496
+ {wsStatus === 'connected'
497
+ ? 'Live updates'
498
+ : wsStatus === 'connecting'
499
+ ? 'Connecting...'
500
+ : 'No live updates'}
501
+ </span>
502
+ </div>
503
+
504
+ {/* Cost chip */}
505
+ {costSummary != null && (
506
+ <button
507
+ onClick={() => setCostModalOpen(true)}
508
+ className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-700 border border-slate-200 rounded px-2 py-0.5 hover:border-slate-300 transition-colors"
509
+ title="LLM cost this month — click for details"
510
+ >
511
+ <DollarSign className="w-3 h-3" />
512
+ {costSummary.totalCost < 0.01 && costSummary.totalCost > 0
513
+ ? '< 0.01'
514
+ : costSummary.totalCost.toFixed(2)}
515
+ <span className="text-slate-400">/mo</span>
516
+ </button>
517
+ )}
518
+
519
+ {/* Settings button */}
520
+ <button
521
+ onClick={() => openSettings()}
522
+ className="flex items-center gap-1.5 text-sm text-slate-500 hover:text-slate-800 transition-colors"
523
+ title="Project settings"
524
+ >
525
+ <Settings className="w-4 h-4" />
526
+ Settings
527
+ </button>
528
+
529
+ {/* Documentation link */}
530
+ <a
531
+ href={docsUrl}
532
+ target="_blank"
533
+ rel="noopener noreferrer"
534
+ className="flex items-center gap-1.5 text-sm text-slate-500 hover:text-slate-800 transition-colors"
535
+ title="Open project documentation"
536
+ >
537
+ <BookOpen className="w-4 h-4" />
538
+ Project Documentation
539
+ </a>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ </header>
544
+
545
+ {/* Filter Toolbar */}
546
+ <FilterToolbar />
547
+
548
+ {/* Process Monitor Bar */}
549
+ <ProcessMonitorBar />
550
+
551
+ {/* Main Content */}
552
+ <main className="flex-1 overflow-hidden">
553
+ <div className="h-full px-4 sm:px-6 lg:px-8 py-6">
554
+ <KanbanBoard
555
+ onCardClick={handleCardClick}
556
+ projectFilesReady={projectFilesStatus.docExists}
557
+ onStartProject={
558
+ projectFilesLoaded &&
559
+ !projectFilesStatus.docExists && ceremonyStatus !== 'running'
560
+ ? () => { resetWizard(); openWizard(); }
561
+ : undefined
562
+ }
563
+ onEditProjectDoc={() => setEditingProjectFile('doc')}
564
+ onStartSprintPlanning={
565
+ projectFilesLoaded &&
566
+ projectFilesStatus.docExists &&
567
+ sprintPlanningStatus !== 'running' &&
568
+ sprintPlanningStatus !== 'awaiting-selection' &&
569
+ ceremonyStatus !== 'running'
570
+ ? openSprintPlanning
571
+ : undefined
572
+ }
573
+ onOpenSprintPlanningSelection={
574
+ sprintPlanningStatus === 'awaiting-selection' ? reopenSprintPlanning : undefined
575
+ }
576
+ sponsorCallRunning={ceremonyStatus === 'running'}
577
+ />
578
+ </div>
579
+ </main>
580
+
581
+ {/* Footer */}
582
+ <footer className="bg-white border-t border-slate-200 py-4 text-center text-sm text-slate-500 flex-shrink-0">
583
+ Agile Vibe Coding
584
+ </footer>
585
+
586
+ {/* Detail Modal */}
587
+ <CardDetailModal
588
+ workItem={selectedItem}
589
+ open={modalOpen}
590
+ onOpenChange={handleModalClose}
591
+ onNavigate={handleNavigate}
592
+ onItemClick={handleCardClick}
593
+ allItems={workItems}
594
+ refineProgress={refineProgress}
595
+ refineResult={refineResult}
596
+ refineError={refineError}
597
+ onClearRefine={() => { setRefineResult(null); setRefineError(null); setRefineProgress(null); }}
598
+ />
599
+
600
+ {/* Sponsor Call Ceremony Modal */}
601
+ {ceremonyOpen && (
602
+ <SponsorCallModal
603
+ onOpenSettings={openSettings}
604
+ costLimitPending={costLimitPending?.runningType === 'sponsor-call' ? costLimitPending : null}
605
+ onContinuePastCostLimit={async () => { try { await continuePastCostLimit(); setCostLimitPending(null); } catch (_) {} }}
606
+ onCancelFromCostLimit={async () => { try { await cancelCeremony(); setCostLimitPending(null); } catch (_) {} }}
607
+ />
608
+ )}
609
+
610
+ {/* Sprint Planning Ceremony Modal */}
611
+ {sprintPlanningOpen && (
612
+ <SprintPlanningModal
613
+ onClose={closeSprintPlanning}
614
+ costLimitPending={costLimitPending?.runningType === 'sprint-planning' ? costLimitPending : null}
615
+ onContinuePastCostLimit={async () => { try { await continuePastCostLimit(); setCostLimitPending(null); } catch (_) {} }}
616
+ onCancelFromCostLimit={async () => { try { await cancelCeremony(); setCostLimitPending(null); } catch (_) {} }}
617
+ quotaLimitPending={quotaLimitPending?.runningType === 'sprint-planning' ? quotaLimitPending : null}
618
+ onContinueAfterQuota={async (newProvider, newModel) => { try { await continueAfterQuota(newProvider, newModel); setQuotaLimitPending(null); } catch (_) {} }}
619
+ onCancelFromQuota={async () => { try { await cancelCeremony(); setQuotaLimitPending(null); } catch (_) {} }}
620
+ />
621
+ )}
622
+
623
+ {/* Epic/Story Selection Popup — shown independently when decomposition completes */}
624
+ <EpicStorySelectionModal />
625
+
626
+ {/* Settings Modal */}
627
+ {settingsOpen && settingsSnapshot && (
628
+ <SettingsModal
629
+ settings={settingsSnapshot}
630
+ models={modelsSnapshot}
631
+ onClose={() => setSettingsOpen(false)}
632
+ onSaved={handleSettingsSaved}
633
+ initialTab={settingsInitialTab}
634
+ />
635
+ )}
636
+
637
+ {/* Cost Modal */}
638
+ {costModalOpen && <CostModal onClose={() => setCostModalOpen(false)} />}
639
+
640
+ {/* Project File Editor Popup */}
641
+ {editingProjectFile && (
642
+ <ProjectFileEditorPopup
643
+ fileType={editingProjectFile}
644
+ onClose={() => setEditingProjectFile(null)}
645
+ />
646
+ )}
647
+ </div>
648
+ );
649
+ }
650
+
651
+ export default App;