@agile-vibe-coding/avc 0.1.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +129 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/database-deep-dive.md +470 -0
  5. package/cli/agents/database-recommender.md +634 -0
  6. package/cli/agents/doc-distributor.md +176 -0
  7. package/cli/agents/documentation-updater.md +203 -0
  8. package/cli/agents/epic-story-decomposer.md +280 -0
  9. package/cli/agents/feature-context-generator.md +91 -0
  10. package/cli/agents/gap-checker-epic.md +52 -0
  11. package/cli/agents/impact-checker-story.md +51 -0
  12. package/cli/agents/migration-guide-generator.md +305 -0
  13. package/cli/agents/mission-scope-generator.md +79 -0
  14. package/cli/agents/mission-scope-validator.md +112 -0
  15. package/cli/agents/project-context-extractor.md +107 -0
  16. package/cli/agents/project-documentation-creator.json +226 -0
  17. package/cli/agents/project-documentation-creator.md +595 -0
  18. package/cli/agents/question-prefiller.md +269 -0
  19. package/cli/agents/refiner-epic.md +39 -0
  20. package/cli/agents/refiner-story.md +42 -0
  21. package/cli/agents/solver-epic-api.json +15 -0
  22. package/cli/agents/solver-epic-api.md +39 -0
  23. package/cli/agents/solver-epic-backend.json +15 -0
  24. package/cli/agents/solver-epic-backend.md +39 -0
  25. package/cli/agents/solver-epic-cloud.json +15 -0
  26. package/cli/agents/solver-epic-cloud.md +39 -0
  27. package/cli/agents/solver-epic-data.json +15 -0
  28. package/cli/agents/solver-epic-data.md +39 -0
  29. package/cli/agents/solver-epic-database.json +15 -0
  30. package/cli/agents/solver-epic-database.md +39 -0
  31. package/cli/agents/solver-epic-developer.json +15 -0
  32. package/cli/agents/solver-epic-developer.md +39 -0
  33. package/cli/agents/solver-epic-devops.json +15 -0
  34. package/cli/agents/solver-epic-devops.md +39 -0
  35. package/cli/agents/solver-epic-frontend.json +15 -0
  36. package/cli/agents/solver-epic-frontend.md +39 -0
  37. package/cli/agents/solver-epic-mobile.json +15 -0
  38. package/cli/agents/solver-epic-mobile.md +39 -0
  39. package/cli/agents/solver-epic-qa.json +15 -0
  40. package/cli/agents/solver-epic-qa.md +39 -0
  41. package/cli/agents/solver-epic-security.json +15 -0
  42. package/cli/agents/solver-epic-security.md +39 -0
  43. package/cli/agents/solver-epic-solution-architect.json +15 -0
  44. package/cli/agents/solver-epic-solution-architect.md +39 -0
  45. package/cli/agents/solver-epic-test-architect.json +15 -0
  46. package/cli/agents/solver-epic-test-architect.md +39 -0
  47. package/cli/agents/solver-epic-ui.json +15 -0
  48. package/cli/agents/solver-epic-ui.md +39 -0
  49. package/cli/agents/solver-epic-ux.json +15 -0
  50. package/cli/agents/solver-epic-ux.md +39 -0
  51. package/cli/agents/solver-story-api.json +15 -0
  52. package/cli/agents/solver-story-api.md +39 -0
  53. package/cli/agents/solver-story-backend.json +15 -0
  54. package/cli/agents/solver-story-backend.md +39 -0
  55. package/cli/agents/solver-story-cloud.json +15 -0
  56. package/cli/agents/solver-story-cloud.md +39 -0
  57. package/cli/agents/solver-story-data.json +15 -0
  58. package/cli/agents/solver-story-data.md +39 -0
  59. package/cli/agents/solver-story-database.json +15 -0
  60. package/cli/agents/solver-story-database.md +39 -0
  61. package/cli/agents/solver-story-developer.json +15 -0
  62. package/cli/agents/solver-story-developer.md +39 -0
  63. package/cli/agents/solver-story-devops.json +15 -0
  64. package/cli/agents/solver-story-devops.md +39 -0
  65. package/cli/agents/solver-story-frontend.json +15 -0
  66. package/cli/agents/solver-story-frontend.md +39 -0
  67. package/cli/agents/solver-story-mobile.json +15 -0
  68. package/cli/agents/solver-story-mobile.md +39 -0
  69. package/cli/agents/solver-story-qa.json +15 -0
  70. package/cli/agents/solver-story-qa.md +39 -0
  71. package/cli/agents/solver-story-security.json +15 -0
  72. package/cli/agents/solver-story-security.md +39 -0
  73. package/cli/agents/solver-story-solution-architect.json +15 -0
  74. package/cli/agents/solver-story-solution-architect.md +39 -0
  75. package/cli/agents/solver-story-test-architect.json +15 -0
  76. package/cli/agents/solver-story-test-architect.md +39 -0
  77. package/cli/agents/solver-story-ui.json +15 -0
  78. package/cli/agents/solver-story-ui.md +39 -0
  79. package/cli/agents/solver-story-ux.json +15 -0
  80. package/cli/agents/solver-story-ux.md +39 -0
  81. package/cli/agents/story-doc-enricher.md +133 -0
  82. package/cli/agents/suggestion-business-analyst.md +88 -0
  83. package/cli/agents/suggestion-deployment-architect.md +263 -0
  84. package/cli/agents/suggestion-product-manager.md +129 -0
  85. package/cli/agents/suggestion-security-specialist.md +156 -0
  86. package/cli/agents/suggestion-technical-architect.md +269 -0
  87. package/cli/agents/suggestion-ux-researcher.md +93 -0
  88. package/cli/agents/task-subtask-decomposer.md +188 -0
  89. package/cli/agents/validator-documentation.json +152 -0
  90. package/cli/agents/validator-documentation.md +453 -0
  91. package/cli/agents/validator-epic-api.json +93 -0
  92. package/cli/agents/validator-epic-api.md +137 -0
  93. package/cli/agents/validator-epic-backend.json +93 -0
  94. package/cli/agents/validator-epic-backend.md +130 -0
  95. package/cli/agents/validator-epic-cloud.json +93 -0
  96. package/cli/agents/validator-epic-cloud.md +137 -0
  97. package/cli/agents/validator-epic-data.json +93 -0
  98. package/cli/agents/validator-epic-data.md +130 -0
  99. package/cli/agents/validator-epic-database.json +93 -0
  100. package/cli/agents/validator-epic-database.md +137 -0
  101. package/cli/agents/validator-epic-developer.json +74 -0
  102. package/cli/agents/validator-epic-developer.md +153 -0
  103. package/cli/agents/validator-epic-devops.json +74 -0
  104. package/cli/agents/validator-epic-devops.md +153 -0
  105. package/cli/agents/validator-epic-frontend.json +74 -0
  106. package/cli/agents/validator-epic-frontend.md +153 -0
  107. package/cli/agents/validator-epic-mobile.json +93 -0
  108. package/cli/agents/validator-epic-mobile.md +130 -0
  109. package/cli/agents/validator-epic-qa.json +93 -0
  110. package/cli/agents/validator-epic-qa.md +130 -0
  111. package/cli/agents/validator-epic-security.json +74 -0
  112. package/cli/agents/validator-epic-security.md +154 -0
  113. package/cli/agents/validator-epic-solution-architect.json +74 -0
  114. package/cli/agents/validator-epic-solution-architect.md +156 -0
  115. package/cli/agents/validator-epic-test-architect.json +93 -0
  116. package/cli/agents/validator-epic-test-architect.md +130 -0
  117. package/cli/agents/validator-epic-ui.json +93 -0
  118. package/cli/agents/validator-epic-ui.md +130 -0
  119. package/cli/agents/validator-epic-ux.json +93 -0
  120. package/cli/agents/validator-epic-ux.md +130 -0
  121. package/cli/agents/validator-selector.md +211 -0
  122. package/cli/agents/validator-story-api.json +104 -0
  123. package/cli/agents/validator-story-api.md +152 -0
  124. package/cli/agents/validator-story-backend.json +104 -0
  125. package/cli/agents/validator-story-backend.md +152 -0
  126. package/cli/agents/validator-story-cloud.json +104 -0
  127. package/cli/agents/validator-story-cloud.md +152 -0
  128. package/cli/agents/validator-story-data.json +104 -0
  129. package/cli/agents/validator-story-data.md +152 -0
  130. package/cli/agents/validator-story-database.json +104 -0
  131. package/cli/agents/validator-story-database.md +152 -0
  132. package/cli/agents/validator-story-developer.json +104 -0
  133. package/cli/agents/validator-story-developer.md +152 -0
  134. package/cli/agents/validator-story-devops.json +104 -0
  135. package/cli/agents/validator-story-devops.md +152 -0
  136. package/cli/agents/validator-story-frontend.json +104 -0
  137. package/cli/agents/validator-story-frontend.md +152 -0
  138. package/cli/agents/validator-story-mobile.json +104 -0
  139. package/cli/agents/validator-story-mobile.md +152 -0
  140. package/cli/agents/validator-story-qa.json +104 -0
  141. package/cli/agents/validator-story-qa.md +152 -0
  142. package/cli/agents/validator-story-security.json +104 -0
  143. package/cli/agents/validator-story-security.md +152 -0
  144. package/cli/agents/validator-story-solution-architect.json +104 -0
  145. package/cli/agents/validator-story-solution-architect.md +152 -0
  146. package/cli/agents/validator-story-test-architect.json +104 -0
  147. package/cli/agents/validator-story-test-architect.md +152 -0
  148. package/cli/agents/validator-story-ui.json +104 -0
  149. package/cli/agents/validator-story-ui.md +152 -0
  150. package/cli/agents/validator-story-ux.json +104 -0
  151. package/cli/agents/validator-story-ux.md +152 -0
  152. package/cli/ansi-colors.js +21 -0
  153. package/cli/build-docs.js +29 -8
  154. package/cli/ceremony-history.js +369 -0
  155. package/cli/command-logger.js +49 -12
  156. package/cli/components/static-output.js +63 -0
  157. package/cli/console-output-manager.js +94 -0
  158. package/cli/docs-sync.js +306 -0
  159. package/cli/epic-story-validator.js +1174 -0
  160. package/cli/evaluation-prompts.js +1008 -0
  161. package/cli/execution-context.js +195 -0
  162. package/cli/generate-summary-table.js +340 -0
  163. package/cli/index.js +0 -0
  164. package/cli/init-model-config.js +697 -0
  165. package/cli/init.js +1311 -274
  166. package/cli/kanban-server-manager.js +228 -0
  167. package/cli/llm-claude.js +83 -1
  168. package/cli/llm-gemini.js +85 -0
  169. package/cli/llm-mock.js +233 -0
  170. package/cli/llm-openai.js +233 -0
  171. package/cli/llm-provider.js +240 -3
  172. package/cli/llm-token-limits.js +102 -0
  173. package/cli/llm-verifier.js +454 -0
  174. package/cli/message-constants.js +58 -0
  175. package/cli/message-manager.js +334 -0
  176. package/cli/message-types.js +96 -0
  177. package/cli/messaging-api.js +297 -0
  178. package/cli/model-pricing.js +169 -0
  179. package/cli/model-query-engine.js +468 -0
  180. package/cli/model-recommendation-analyzer.js +495 -0
  181. package/cli/model-selector.js +269 -0
  182. package/cli/output-buffer.js +107 -0
  183. package/cli/process-manager.js +73 -2
  184. package/cli/repl-ink.js +4988 -1217
  185. package/cli/repl-old.js +4 -4
  186. package/cli/seed-processor.js +792 -0
  187. package/cli/sprint-planning-processor.js +1813 -0
  188. package/cli/template-processor.js +2102 -105
  189. package/cli/templates/project.md +25 -8
  190. package/cli/templates/vitepress-config.mts.template +5 -4
  191. package/cli/token-tracker.js +520 -0
  192. package/cli/tools/generate-story-validators.js +317 -0
  193. package/cli/tools/generate-validators.js +669 -0
  194. package/cli/update-checker.js +19 -17
  195. package/cli/update-notifier.js +4 -4
  196. package/cli/validation-router.js +605 -0
  197. package/cli/verification-tracker.js +563 -0
  198. package/kanban/README.md +386 -0
  199. package/kanban/client/README.md +205 -0
  200. package/kanban/client/components.json +20 -0
  201. package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
  202. package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
  203. package/kanban/client/dist/index.html +16 -0
  204. package/kanban/client/dist/vite.svg +1 -0
  205. package/kanban/client/index.html +15 -0
  206. package/kanban/client/package-lock.json +9442 -0
  207. package/kanban/client/package.json +44 -0
  208. package/kanban/client/postcss.config.js +6 -0
  209. package/kanban/client/public/vite.svg +1 -0
  210. package/kanban/client/src/App.jsx +622 -0
  211. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  212. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
  213. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
  214. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
  215. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  216. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
  217. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
  218. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  219. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
  220. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  221. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  222. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  223. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
  224. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
  225. package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
  226. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  227. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  228. package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
  229. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  230. package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
  231. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  232. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
  233. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  234. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  235. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  236. package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
  237. package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
  238. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
  239. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
  240. package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
  241. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  242. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  243. package/kanban/client/src/components/stats/CostModal.jsx +353 -0
  244. package/kanban/client/src/components/ui/badge.jsx +27 -0
  245. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  246. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  247. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  248. package/kanban/client/src/hooks/useGrouping.js +118 -0
  249. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  250. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  251. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  252. package/kanban/client/src/lib/api.js +401 -0
  253. package/kanban/client/src/lib/status-grouping.js +144 -0
  254. package/kanban/client/src/lib/utils.js +11 -0
  255. package/kanban/client/src/main.jsx +10 -0
  256. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  257. package/kanban/client/src/store/ceremonyStore.js +172 -0
  258. package/kanban/client/src/store/filterStore.js +201 -0
  259. package/kanban/client/src/store/kanbanStore.js +115 -0
  260. package/kanban/client/src/store/processStore.js +65 -0
  261. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  262. package/kanban/client/src/styles/globals.css +59 -0
  263. package/kanban/client/tailwind.config.js +77 -0
  264. package/kanban/client/vite.config.js +28 -0
  265. package/kanban/client/vitest.config.js +28 -0
  266. package/kanban/dev-start.sh +47 -0
  267. package/kanban/package.json +12 -0
  268. package/kanban/server/index.js +516 -0
  269. package/kanban/server/routes/ceremony.js +305 -0
  270. package/kanban/server/routes/costs.js +157 -0
  271. package/kanban/server/routes/processes.js +50 -0
  272. package/kanban/server/routes/settings.js +303 -0
  273. package/kanban/server/routes/websocket.js +276 -0
  274. package/kanban/server/routes/work-items.js +347 -0
  275. package/kanban/server/services/CeremonyService.js +1190 -0
  276. package/kanban/server/services/FileSystemScanner.js +95 -0
  277. package/kanban/server/services/FileWatcher.js +144 -0
  278. package/kanban/server/services/HierarchyBuilder.js +196 -0
  279. package/kanban/server/services/ProcessRegistry.js +122 -0
  280. package/kanban/server/services/WorkItemReader.js +123 -0
  281. package/kanban/server/services/WorkItemRefineService.js +510 -0
  282. package/kanban/server/start.js +49 -0
  283. package/kanban/server/utils/kanban-logger.js +132 -0
  284. package/kanban/server/utils/markdown.js +91 -0
  285. package/kanban/server/utils/status-grouping.js +107 -0
  286. package/kanban/server/workers/sponsor-call-worker.js +84 -0
  287. package/kanban/server/workers/sprint-planning-worker.js +130 -0
  288. package/package.json +18 -5
  289. package/cli/agents/documentation.md +0 -302
@@ -0,0 +1,704 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { X, Info, ArrowDownToLine } from 'lucide-react';
3
+ import { useSprintPlanningStore } from '../../store/sprintPlanningStore';
4
+ import { runSprintPlanning, getSettings, getModels, saveCeremonies, pauseCeremony, resumeCeremony, cancelCeremony, resetCeremony } from '../../lib/api';
5
+ import { CeremonyWorkflowModal } from './CeremonyWorkflowModal';
6
+
7
+ // ── Step progress header ─────────────────────────────────────────────────────
8
+
9
+ const STEPS = [
10
+ { id: 1, label: 'Ready' },
11
+ { id: 2, label: 'Running' },
12
+ { id: 3, label: 'Select Epics/Stories' },
13
+ { id: 4, label: 'Complete' },
14
+ ];
15
+
16
+ function StepProgress({ currentStep }) {
17
+ return (
18
+ <div className="flex items-center gap-1 flex-nowrap">
19
+ {STEPS.map((s, idx) => {
20
+ const isDone = currentStep > s.id;
21
+ const isCurrent = currentStep === s.id;
22
+ return (
23
+ <div key={s.id} className="flex items-center gap-1">
24
+ <div
25
+ className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded-full ${
26
+ isCurrent
27
+ ? 'bg-blue-600 text-white font-medium'
28
+ : isDone
29
+ ? 'bg-green-100 text-green-700'
30
+ : 'bg-slate-100 text-slate-400'
31
+ }`}
32
+ >
33
+ {isDone ? '✓' : s.id} {s.label}
34
+ </div>
35
+ {idx < STEPS.length - 1 && (
36
+ <span className="text-slate-300 text-xs">›</span>
37
+ )}
38
+ </div>
39
+ );
40
+ })}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ // ── Stat card ────────────────────────────────────────────────────────────────
46
+
47
+ function Stat({ label, value }) {
48
+ return (
49
+ <div className="bg-slate-50 rounded-lg border border-slate-200 p-3 text-center">
50
+ <div className="text-lg font-bold text-slate-900">{value}</div>
51
+ <div className="text-xs text-slate-500">{label}</div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ // ── Step 1: Ready ─────────────────────────────────────────────────────────────
57
+
58
+ function ReadyStep({ onStart }) {
59
+ return (
60
+ <div className="space-y-6">
61
+ <div>
62
+ <h2 className="text-xl font-semibold text-slate-900">Ready to Plan</h2>
63
+ <p className="text-sm text-slate-500 mt-1">
64
+ AI will decompose your project documentation into Epics and Stories using multi-agent analysis.
65
+ </p>
66
+ </div>
67
+
68
+ <div className="flex items-center justify-end pt-2">
69
+ <button
70
+ onClick={onStart}
71
+ className="px-5 py-2 text-sm font-medium bg-slate-900 text-white rounded-lg hover:bg-slate-700 transition-colors"
72
+ >
73
+ Start
74
+ </button>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ // ── Step 2: Running ───────────────────────────────────────────────────────────
81
+
82
+ function parseStageNumber(message) {
83
+ const m = message?.match(/Stage\s+(\d+(?:\.\d+)?)\/(\d+)/i);
84
+ if (m) return { current: parseFloat(m[1]), total: parseInt(m[2]) };
85
+ return null;
86
+ }
87
+
88
+ function parseStageTotals(message) {
89
+ const m = message?.match(/\((\d+)\s+epics?,\s*(\d+)\s+stories?\)/i);
90
+ if (m) return { total: parseInt(m[1]) + parseInt(m[2]) };
91
+ const m2 = message?.match(/\((\d+)\s+stories?\)/i);
92
+ if (m2) return { total: parseInt(m2[1]) };
93
+ return null;
94
+ }
95
+
96
+ // Group flat progressLog into a 3-level hierarchy:
97
+ // Level 1 — stage headers (type:'progress')
98
+ // Level 2 — substeps (type:'substep') → { text, details[] }
99
+ // Level 3 — details (type:'detail') → appended to last substep's details[]
100
+ // If no substep exists yet, details go into group.orphanDetails[]
101
+ function buildStageGroups(progressLog) {
102
+ const groups = [];
103
+ for (const entry of progressLog) {
104
+ if (entry.type === 'progress') {
105
+ groups.push({ message: entry.message, substeps: [], orphanDetails: [] });
106
+ } else if (entry.type === 'substep' && groups.length > 0) {
107
+ groups[groups.length - 1].substeps.push({ text: entry.substep, details: [] });
108
+ } else if (entry.type === 'detail' && groups.length > 0) {
109
+ const group = groups[groups.length - 1];
110
+ const substeps = group.substeps;
111
+ if (substeps.length > 0) {
112
+ substeps[substeps.length - 1].details.push(entry.detail);
113
+ } else {
114
+ group.orphanDetails.push(entry.detail);
115
+ }
116
+ }
117
+ }
118
+ return groups;
119
+ }
120
+
121
+ function RunningStep({ transitioning, onPause, onResume, onCancel, onBackground }) {
122
+ const { progressLog, status, error, isPaused, setStatus, setStep, setError } = useSprintPlanningStore();
123
+ const logBottomRef = useRef(null);
124
+
125
+ const handleForceReset = async () => {
126
+ try { await resetCeremony(); } catch (_) {}
127
+ setStatus('idle');
128
+ setStep(1);
129
+ setError(null);
130
+ };
131
+
132
+ useEffect(() => {
133
+ logBottomRef.current?.scrollIntoView({ behavior: 'smooth' });
134
+ }, [progressLog]);
135
+
136
+ const stageGroups = buildStageGroups(progressLog);
137
+
138
+ const STAGE_WEIGHTS = {
139
+ '1/6': [0, 3],
140
+ '2/6': [3, 7],
141
+ '3/6': [7, 12],
142
+ '4/6': [12, 22],
143
+ '4.5/6': [22, 24],
144
+ '5/6': [24, 76],
145
+ '6/7': [76, 90],
146
+ '7/7': [90, 100],
147
+ };
148
+
149
+ const currentGroup = [...stageGroups].reverse().find(g => parseStageNumber(g.message));
150
+
151
+ let progressPct = 5;
152
+ if (currentGroup) {
153
+ const parsed = parseStageNumber(currentGroup.message);
154
+ const key = `${parsed.current}/${parsed.total}`;
155
+ const [stageStart, stageEnd] = STAGE_WEIGHTS[key] ?? [0, 90];
156
+
157
+ const totals = parseStageTotals(currentGroup.message);
158
+ if (totals && totals.total > 0) {
159
+ let countedItems = 0;
160
+ if (key === '5/6') {
161
+ countedItems = currentGroup.substeps.filter(s => s.text?.includes('Validating ')).length;
162
+ } else if (key === '6/7') {
163
+ countedItems = currentGroup.substeps.filter(s => s.text?.includes('Distributing documentation')).length;
164
+ } else if (key === '7/7') {
165
+ countedItems = currentGroup.substeps.filter(s => s.text?.includes('Enriching documentation')).length;
166
+ }
167
+ const fraction = Math.min(countedItems, totals.total) / totals.total;
168
+ progressPct = Math.round(stageStart + fraction * (stageEnd - stageStart));
169
+ } else {
170
+ progressPct = stageStart;
171
+ }
172
+ }
173
+ const latestProgress = stageGroups[stageGroups.length - 1]?.message || 'Starting…';
174
+
175
+ if (status === 'error') {
176
+ const isAlreadyRunning = error?.includes('already running');
177
+ return (
178
+ <div className="space-y-4">
179
+ <h2 className="text-xl font-semibold text-slate-900">Sprint Planning Failed</h2>
180
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4">
181
+ <p className="text-sm font-medium text-red-700 mb-1">Error</p>
182
+ <p className="text-sm text-red-600">{error || 'An unknown error occurred.'}</p>
183
+ </div>
184
+ {isAlreadyRunning ? (
185
+ <div className="space-y-3">
186
+ <p className="text-sm text-slate-500">
187
+ A ceremony is already running on the server. You can force-stop it and reset the state — this will discard any in-progress work.
188
+ </p>
189
+ <button
190
+ onClick={handleForceReset}
191
+ className="px-4 py-2 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700 transition-colors"
192
+ >
193
+ Force Stop &amp; Reset
194
+ </button>
195
+ </div>
196
+ ) : error === 'Not found' ? (
197
+ <div className="space-y-3">
198
+ <p className="text-sm text-slate-500">
199
+ The server may be running an older version. Run{' '}
200
+ <code className="bg-slate-100 px-1 rounded">/kanban</code> in the AVC terminal to restart it, then try again.
201
+ </p>
202
+ <button
203
+ onClick={handleForceReset}
204
+ className="px-4 py-2 text-sm rounded-lg bg-slate-600 text-white hover:bg-slate-700 transition-colors"
205
+ >
206
+ Cancel &amp; Retry
207
+ </button>
208
+ </div>
209
+ ) : (
210
+ <div className="space-y-3">
211
+ <p className="text-sm text-slate-500">
212
+ You can dismiss this error and start a new sprint planning session.
213
+ </p>
214
+ <button
215
+ onClick={handleForceReset}
216
+ className="px-4 py-2 text-sm rounded-lg bg-slate-600 text-white hover:bg-slate-700 transition-colors"
217
+ >
218
+ Cancel &amp; Retry
219
+ </button>
220
+ </div>
221
+ )}
222
+ </div>
223
+ );
224
+ }
225
+
226
+ return (
227
+ <div className="space-y-6">
228
+ <div>
229
+ <h2 className="text-xl font-semibold text-slate-900">Running Sprint Planning</h2>
230
+ <p className="text-sm text-slate-500 mt-1">
231
+ The AI is decomposing your project scope and validating each work item. Duration varies with project size and validation iterations — from a few minutes to 30+ minutes for larger projects.
232
+ </p>
233
+ </div>
234
+
235
+ <div>
236
+ <div className="flex items-center justify-between mb-1">
237
+ <span className="text-xs font-medium text-slate-600">{latestProgress}</span>
238
+ <span className="text-xs text-slate-400">{progressPct}%</span>
239
+ </div>
240
+ <div className="w-full bg-slate-200 rounded-full h-2">
241
+ <div
242
+ className="bg-blue-500 h-2 rounded-full transition-all duration-500"
243
+ style={{ width: `${progressPct}%` }}
244
+ />
245
+ </div>
246
+ </div>
247
+
248
+ <div className="bg-slate-900 rounded-lg p-4 h-64 overflow-y-auto font-mono text-xs">
249
+ {stageGroups.length === 0 ? (
250
+ <p className="text-slate-400 animate-pulse">Initializing…</p>
251
+ ) : (
252
+ <div className="space-y-2">
253
+ {stageGroups.map((group, gi) => {
254
+ const isActive = gi === stageGroups.length - 1 && status === 'running';
255
+ return (
256
+ <div key={gi}>
257
+ {/* Stage header */}
258
+ <div className="flex items-center gap-1.5">
259
+ {isActive ? (
260
+ <span className="inline-block w-3 h-3 border border-blue-400 border-t-blue-200 rounded-full animate-spin flex-shrink-0" />
261
+ ) : (
262
+ <span className="text-green-500 flex-shrink-0">✓</span>
263
+ )}
264
+ <span className={isActive ? 'text-blue-300 font-medium' : 'text-slate-400'}>
265
+ {group.message}
266
+ </span>
267
+ </div>
268
+ {/* Orphan details — arrived before any substep in this group */}
269
+ {group.orphanDetails?.length > 0 && (
270
+ <div className="ml-5 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
271
+ {group.orphanDetails.map((d, di) => (
272
+ <p key={di} className="text-slate-500">{d}</p>
273
+ ))}
274
+ </div>
275
+ )}
276
+ {/* Substeps (Level 2) + Details (Level 3) */}
277
+ {group.substeps.length > 0 && (
278
+ <div className="ml-5 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
279
+ {group.substeps.map((sub, si) => (
280
+ <div key={si}>
281
+ <p className="text-slate-400">{sub.text}</p>
282
+ {sub.details.length > 0 && (
283
+ <div className="ml-3 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
284
+ {sub.details.map((d, di) => (
285
+ <p key={di} className="text-slate-500">{d}</p>
286
+ ))}
287
+ </div>
288
+ )}
289
+ </div>
290
+ ))}
291
+ </div>
292
+ )}
293
+ </div>
294
+ );
295
+ })}
296
+ {transitioning === 'cancelling' && (
297
+ <div className="flex items-center gap-1.5 mt-1">
298
+ <span className="inline-block w-3 h-3 border border-red-400 border-t-red-200 rounded-full animate-spin flex-shrink-0" />
299
+ <span className="text-red-400 font-medium">Cancelling…</span>
300
+ </div>
301
+ )}
302
+ <div ref={logBottomRef} />
303
+ </div>
304
+ )}
305
+ </div>
306
+
307
+ <div className="flex items-center justify-between gap-2 pt-2">
308
+ {/* Left: run in background */}
309
+ {onBackground && !transitioning ? (
310
+ <button
311
+ type="button"
312
+ onClick={onBackground}
313
+ className="flex items-center gap-1.5 text-xs font-medium text-slate-500 hover:text-slate-700 bg-slate-50 hover:bg-slate-100 border border-slate-200 rounded-md px-2.5 py-1.5 transition-colors"
314
+ title="Hide this window — ceremony keeps running in the background"
315
+ >
316
+ <ArrowDownToLine className="w-3.5 h-3.5" />
317
+ Run in Background
318
+ </button>
319
+ ) : <span />}
320
+
321
+ {/* Right: pause / resume / cancel */}
322
+ <div className="flex items-center gap-2">
323
+ {transitioning === 'pausing' ? (
324
+ <span className="flex items-center gap-1.5 text-xs text-slate-400"><span className="inline-block w-3 h-3 border border-slate-400 border-t-slate-200 rounded-full animate-spin" />Pausing…</span>
325
+ ) : transitioning === 'cancelling' ? (
326
+ <span className="flex items-center gap-1.5 text-xs text-red-400"><span className="inline-block w-3 h-3 border border-red-400 border-t-red-200 rounded-full animate-spin" />Cancelling…</span>
327
+ ) : !isPaused ? (
328
+ <button
329
+ onClick={onPause}
330
+ className="px-4 py-2 text-sm rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors"
331
+ >
332
+ ⏸ Pause
333
+ </button>
334
+ ) : (
335
+ <button
336
+ onClick={onResume}
337
+ className="px-4 py-2 text-sm rounded-lg border border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100 transition-colors"
338
+ >
339
+ ▶ Resume
340
+ </button>
341
+ )}
342
+ {!transitioning && (
343
+ <button
344
+ onClick={onCancel}
345
+ className="px-4 py-2 text-sm rounded-lg border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
346
+ >
347
+ ✕ Cancel
348
+ </button>
349
+ )}
350
+ </div>
351
+ </div>
352
+ </div>
353
+ );
354
+ }
355
+
356
+ // ── Step 4: Complete ──────────────────────────────────────────────────────────
357
+
358
+ const EXAMPLE_ISSUES = [
359
+ { stage: 'Project Documentation', ruleId: 'fix-header-formatting', name: 'Fix Header Spacing', severity: 'major' },
360
+ { stage: 'Project Documentation', ruleId: 'add-section-spacing', name: 'Add Section Spacing', severity: 'minor' },
361
+ { stage: 'Project Context', ruleId: 'token-count-too-short', name: 'Expand If Too Short', severity: 'major' },
362
+ { stage: 'Project Context', ruleId: 'no-redundant-info', name: 'Remove Truly Redundant Information', severity: 'minor' },
363
+ { stage: 'Context Validation', ruleId: 'fix-unclosed-code-blocks', name: 'Fix Unclosed Code Blocks', severity: 'major' },
364
+ ];
365
+
366
+ function IssueTag({ severity }) {
367
+ const cls =
368
+ severity === 'critical' ? 'bg-red-100 text-red-700' :
369
+ severity === 'major' ? 'bg-amber-100 text-amber-700' :
370
+ 'bg-slate-100 text-slate-500';
371
+ return (
372
+ <span className={`flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase ${cls}`}>
373
+ {severity}
374
+ </span>
375
+ );
376
+ }
377
+
378
+ function CompleteStep({ onClose }) {
379
+ const { result } = useSprintPlanningStore();
380
+ const r = result || {};
381
+
382
+ const tokenInput = r.tokenUsage?.input || 0;
383
+ const tokenOutput = r.tokenUsage?.output || 0;
384
+
385
+ return (
386
+ <div className="space-y-6">
387
+ <div className="text-center">
388
+ <div className="text-5xl mb-3">✅</div>
389
+ <h2 className="text-xl font-semibold text-slate-900">Sprint Planning Complete</h2>
390
+ <p className="text-sm text-slate-500 mt-1">
391
+ Your project has been decomposed into Epics and Stories. The kanban board will refresh.
392
+ </p>
393
+ </div>
394
+
395
+ <div className="grid grid-cols-2 gap-3 text-center">
396
+ <div className="bg-slate-50 rounded-lg border border-slate-200 p-3">
397
+ <div className="text-2xl font-bold text-slate-900">{r.epicsCreated ?? 0}</div>
398
+ <div className="text-xs text-slate-500">Epics created</div>
399
+ </div>
400
+ <div className="bg-slate-50 rounded-lg border border-slate-200 p-3">
401
+ <div className="text-2xl font-bold text-slate-900">{r.storiesCreated ?? 0}</div>
402
+ <div className="text-xs text-slate-500">Stories created</div>
403
+ </div>
404
+ </div>
405
+
406
+ {(r.totalEpics != null || r.totalStories != null) && (
407
+ <p className="text-xs text-center text-slate-400">
408
+ Total in project: {r.totalEpics ?? 0} Epics · {r.totalStories ?? 0} Stories
409
+ </p>
410
+ )}
411
+
412
+ <div className="grid grid-cols-2 gap-3">
413
+ <Stat label="Input tokens" value={tokenInput.toLocaleString()} />
414
+ <Stat label="Output tokens" value={tokenOutput.toLocaleString()} />
415
+ </div>
416
+
417
+ {r.model && (
418
+ <p className="text-xs text-center text-slate-400">
419
+ Model: <span className="font-mono">{r.model}</span>
420
+ </p>
421
+ )}
422
+
423
+ {(() => {
424
+ const isExample = r.validationIssues === undefined;
425
+ const issues = r.validationIssues ?? EXAMPLE_ISSUES;
426
+ return issues.length > 0 ? (
427
+ <div>
428
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
429
+ Quality fixes applied
430
+ {isExample && <span className="ml-2 normal-case font-normal text-slate-300">(example preview)</span>}
431
+ </p>
432
+ <div className="space-y-1.5">
433
+ {issues.map((issue, i) => (
434
+ <div key={i} className="flex items-center gap-2 text-xs bg-amber-50 border border-amber-100 rounded-md px-3 py-2">
435
+ <IssueTag severity={issue.severity} />
436
+ <span className="text-slate-400 flex-shrink-0">{issue.stage}</span>
437
+ <span className="text-slate-600">{issue.name}</span>
438
+ </div>
439
+ ))}
440
+ </div>
441
+ </div>
442
+ ) : null;
443
+ })()}
444
+
445
+ <div className="flex justify-center pt-2">
446
+ <button
447
+ onClick={onClose}
448
+ className="px-6 py-2 bg-slate-900 text-white text-sm font-medium rounded-lg hover:bg-slate-700 transition-colors"
449
+ >
450
+ Close
451
+ </button>
452
+ </div>
453
+ </div>
454
+ );
455
+ }
456
+
457
+ // ── Main modal ────────────────────────────────────────────────────────────────
458
+
459
+ export function SprintPlanningModal({ onClose, costLimitPending, onContinuePastCostLimit, onCancelFromCostLimit }) {
460
+ const {
461
+ isOpen,
462
+ step,
463
+ status,
464
+ isPaused,
465
+ setStep,
466
+ setStatus,
467
+ setError,
468
+ closeModal,
469
+ setProcessId,
470
+ } = useSprintPlanningStore();
471
+
472
+ const [workflowOpen, setWorkflowOpen] = useState(false);
473
+ const [workflowCeremony, setWorkflowCeremony] = useState(null);
474
+ const [workflowModels, setWorkflowModels] = useState([]);
475
+ const [workflowAllCeremonies, setWorkflowAllCeremonies] = useState([]);
476
+ const [showCancelConfirm, setShowCancelConfirm] = useState(false);
477
+ const [transitioning, setTransitioning] = useState(null); // null | 'pausing' | 'cancelling'
478
+
479
+ if (!isOpen) return null;
480
+
481
+ const isBlocked = status === 'running' || status === 'awaiting-selection';
482
+
483
+ const handleClose = () => {
484
+ if (isBlocked) return;
485
+ closeModal();
486
+ onClose?.();
487
+ };
488
+
489
+ const handleStart = async () => {
490
+ setStatus('running');
491
+ setStep(2);
492
+ try {
493
+ const result = await runSprintPlanning();
494
+ if (result?.processId) setProcessId(result.processId);
495
+ // Completion is handled via WebSocket in App.jsx
496
+ } catch (err) {
497
+ setStatus('error');
498
+ setError(err.message);
499
+ }
500
+ };
501
+
502
+ const openWorkflow = async () => {
503
+ try {
504
+ const [s, m] = await Promise.all([getSettings(), getModels()]);
505
+ const sc = s.ceremonies?.find((c) => c.name === 'sprint-planning') ?? { name: 'sprint-planning' };
506
+ setWorkflowCeremony(sc);
507
+ setWorkflowModels(m);
508
+ setWorkflowAllCeremonies(s.ceremonies || []);
509
+ setWorkflowOpen(true);
510
+ } catch {}
511
+ };
512
+
513
+ const handleWorkflowSave = async (updatedCeremony) => {
514
+ const base = workflowAllCeremonies.length > 0 ? workflowAllCeremonies : [updatedCeremony];
515
+ const next = base.map((c) => c.name === updatedCeremony.name ? updatedCeremony : c);
516
+ await saveCeremonies(next, null);
517
+ setWorkflowCeremony(updatedCeremony);
518
+ setWorkflowAllCeremonies(next);
519
+ };
520
+
521
+ const handlePause = async () => {
522
+ setTransitioning('pausing');
523
+ try { await pauseCeremony(); } catch (_) {}
524
+ };
525
+
526
+ const handleResume = async () => {
527
+ try { await resumeCeremony(); } catch (_) {}
528
+ };
529
+
530
+ const handleConfirmCancel = async () => {
531
+ setShowCancelConfirm(false);
532
+ setTransitioning('cancelling');
533
+ try { await cancelCeremony(); } catch (_) {}
534
+ };
535
+
536
+ // Clear transitioning state when WS events arrive (isPaused / status change)
537
+ useEffect(() => {
538
+ if (transitioning === 'pausing' && isPaused) setTransitioning(null);
539
+ }, [isPaused, transitioning]);
540
+
541
+ useEffect(() => {
542
+ if (transitioning === 'cancelling' && status === 'idle') setTransitioning(null);
543
+ }, [status, transitioning]);
544
+
545
+ const renderStep = () => {
546
+ switch (step) {
547
+ case 1: return <ReadyStep onStart={handleStart} />;
548
+ case 2: return (
549
+ <RunningStep
550
+ transitioning={transitioning}
551
+ onPause={handlePause}
552
+ onResume={handleResume}
553
+ onCancel={() => setShowCancelConfirm(true)}
554
+ onBackground={closeModal}
555
+ />
556
+ );
557
+ case 3: return (
558
+ <div className="flex flex-col items-center justify-center py-16 gap-4 text-center">
559
+ <span className="w-2 h-2 rounded-full bg-amber-400" />
560
+ <p className="text-sm text-slate-500">Reviewing decomposed work…</p>
561
+ </div>
562
+ );
563
+ case 4: return <CompleteStep onClose={handleClose} />;
564
+ default: return null;
565
+ }
566
+ };
567
+
568
+ return (
569
+ <div className="fixed inset-0 z-[70] flex items-center justify-center">
570
+ {/* Backdrop */}
571
+ <div
572
+ className="absolute inset-0 bg-black/40"
573
+ onClick={!isBlocked ? handleClose : undefined}
574
+ />
575
+
576
+ {/* Modal */}
577
+ <div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col">
578
+ {/* Cost-limit pause overlay */}
579
+ {costLimitPending && (
580
+ <div className="absolute inset-0 z-20 flex items-center justify-center bg-white/90 rounded-2xl">
581
+ <div className="bg-white border border-amber-200 rounded-xl shadow-lg p-6 max-w-sm mx-4 text-center space-y-4">
582
+ <div className="text-3xl">⚠️</div>
583
+ <p className="text-base font-semibold text-slate-900">Cost Limit Reached</p>
584
+ <p className="text-sm text-slate-600">
585
+ <span className="font-mono font-medium">${costLimitPending.cost.toFixed(4)}</span> spent
586
+ {costLimitPending.threshold != null && (
587
+ <> (limit: <span className="font-mono">${Number(costLimitPending.threshold).toFixed(2)}</span>)</>
588
+ )}
589
+ </p>
590
+ <p className="text-sm text-slate-500">
591
+ The ceremony is paused. What would you like to do?
592
+ </p>
593
+ <div className="flex gap-3 justify-center pt-1">
594
+ <button
595
+ onClick={onContinuePastCostLimit}
596
+ className="px-4 py-2 text-sm rounded-lg bg-slate-900 text-white hover:bg-slate-700"
597
+ >
598
+ Continue Anyway
599
+ </button>
600
+ <button
601
+ onClick={onCancelFromCostLimit}
602
+ className="px-4 py-2 text-sm rounded-lg border border-red-200 text-red-600 hover:bg-red-50"
603
+ >
604
+ Cancel Ceremony
605
+ </button>
606
+ </div>
607
+ <p className="text-xs text-slate-400">
608
+ Continue disables cost checking for the rest of this run.
609
+ </p>
610
+ </div>
611
+ </div>
612
+ )}
613
+
614
+ {/* Cancel confirmation overlay */}
615
+ {showCancelConfirm && (
616
+ <div className="absolute inset-0 z-10 flex items-center justify-center bg-white/90 rounded-2xl">
617
+ <div className="bg-white border border-slate-200 rounded-xl shadow-lg p-6 max-w-sm mx-4 text-center space-y-4">
618
+ <p className="text-base font-semibold text-slate-900">Cancel sprint planning?</p>
619
+ <p className="text-sm text-slate-500">
620
+ Any epics and stories created in this run will be permanently deleted.
621
+ </p>
622
+ <div className="flex gap-3 justify-center pt-1">
623
+ <button
624
+ onClick={() => setShowCancelConfirm(false)}
625
+ className="px-4 py-2 text-sm rounded-lg border border-slate-200 text-slate-700 hover:bg-slate-50"
626
+ >
627
+ Keep Running
628
+ </button>
629
+ <button
630
+ onClick={handleConfirmCancel}
631
+ className="px-4 py-2 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700"
632
+ >
633
+ Cancel Run
634
+ </button>
635
+ </div>
636
+ </div>
637
+ </div>
638
+ )}
639
+
640
+ {/* Header */}
641
+ <div className="flex items-start justify-between px-6 pt-5 pb-4 border-b border-slate-200 flex-shrink-0">
642
+ <div className="min-w-0 flex-1">
643
+ <h1 className="text-base font-semibold text-slate-900">Sprint Planning Ceremony</h1>
644
+ <div className="mt-2">
645
+ <StepProgress currentStep={step} />
646
+ </div>
647
+ </div>
648
+ <div className="flex items-center gap-3 ml-4 mt-0.5 flex-shrink-0">
649
+ {!isBlocked && (
650
+ <button
651
+ type="button"
652
+ onClick={openWorkflow}
653
+ className="flex items-center gap-1 text-xs text-slate-400 hover:text-blue-500 transition-colors whitespace-nowrap"
654
+ title="View ceremony workflow"
655
+ >
656
+ <Info className="w-3.5 h-3.5" />
657
+ How it works
658
+ </button>
659
+ )}
660
+ {!isBlocked && (
661
+ <button
662
+ onClick={handleClose}
663
+ className="text-slate-400 hover:text-slate-600 transition-colors"
664
+ >
665
+ <X className="w-5 h-5" />
666
+ </button>
667
+ )}
668
+ </div>
669
+ </div>
670
+
671
+ {/* Scrollable content */}
672
+ <div className="flex-1 overflow-y-auto px-6 py-5">
673
+ {renderStep()}
674
+ </div>
675
+
676
+ {/* Status bar */}
677
+ <div className="flex-shrink-0 border-t border-slate-100 px-6 h-8 flex items-center gap-2">
678
+ {status === 'running' && (
679
+ <>
680
+ <span className="w-3 h-3 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin flex-shrink-0" />
681
+ <p className="text-xs text-blue-600 font-medium truncate">Running sprint planning…</p>
682
+ </>
683
+ )}
684
+ {status === 'awaiting-selection' && (
685
+ <>
686
+ <span className="w-2 h-2 rounded-full bg-amber-400 flex-shrink-0" />
687
+ <p className="text-xs text-amber-600 font-medium truncate">Waiting for your selection to continue…</p>
688
+ </>
689
+ )}
690
+ </div>
691
+ </div>
692
+
693
+ {workflowOpen && workflowCeremony && (
694
+ <CeremonyWorkflowModal
695
+ ceremony={workflowCeremony}
696
+ models={workflowModels}
697
+ readOnly={status === 'running'}
698
+ onSave={status !== 'running' ? handleWorkflowSave : undefined}
699
+ onClose={() => setWorkflowOpen(false)}
700
+ />
701
+ )}
702
+ </div>
703
+ );
704
+ }