@agile-vibe-coding/avc 0.1.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/README.md +2 -0
  2. package/cli/agent-loader.js +21 -0
  3. package/cli/agents/agent-selector.md +129 -0
  4. package/cli/agents/architecture-recommender.md +418 -0
  5. package/cli/agents/database-deep-dive.md +470 -0
  6. package/cli/agents/database-recommender.md +634 -0
  7. package/cli/agents/doc-distributor.md +176 -0
  8. package/cli/agents/documentation-updater.md +203 -0
  9. package/cli/agents/epic-story-decomposer.md +280 -0
  10. package/cli/agents/feature-context-generator.md +91 -0
  11. package/cli/agents/gap-checker-epic.md +52 -0
  12. package/cli/agents/impact-checker-story.md +51 -0
  13. package/cli/agents/migration-guide-generator.md +305 -0
  14. package/cli/agents/mission-scope-generator.md +79 -0
  15. package/cli/agents/mission-scope-validator.md +112 -0
  16. package/cli/agents/project-context-extractor.md +107 -0
  17. package/cli/agents/project-documentation-creator.json +226 -0
  18. package/cli/agents/project-documentation-creator.md +595 -0
  19. package/cli/agents/question-prefiller.md +269 -0
  20. package/cli/agents/refiner-epic.md +39 -0
  21. package/cli/agents/refiner-story.md +42 -0
  22. package/cli/agents/solver-epic-api.json +15 -0
  23. package/cli/agents/solver-epic-api.md +39 -0
  24. package/cli/agents/solver-epic-backend.json +15 -0
  25. package/cli/agents/solver-epic-backend.md +39 -0
  26. package/cli/agents/solver-epic-cloud.json +15 -0
  27. package/cli/agents/solver-epic-cloud.md +39 -0
  28. package/cli/agents/solver-epic-data.json +15 -0
  29. package/cli/agents/solver-epic-data.md +39 -0
  30. package/cli/agents/solver-epic-database.json +15 -0
  31. package/cli/agents/solver-epic-database.md +39 -0
  32. package/cli/agents/solver-epic-developer.json +15 -0
  33. package/cli/agents/solver-epic-developer.md +39 -0
  34. package/cli/agents/solver-epic-devops.json +15 -0
  35. package/cli/agents/solver-epic-devops.md +39 -0
  36. package/cli/agents/solver-epic-frontend.json +15 -0
  37. package/cli/agents/solver-epic-frontend.md +39 -0
  38. package/cli/agents/solver-epic-mobile.json +15 -0
  39. package/cli/agents/solver-epic-mobile.md +39 -0
  40. package/cli/agents/solver-epic-qa.json +15 -0
  41. package/cli/agents/solver-epic-qa.md +39 -0
  42. package/cli/agents/solver-epic-security.json +15 -0
  43. package/cli/agents/solver-epic-security.md +39 -0
  44. package/cli/agents/solver-epic-solution-architect.json +15 -0
  45. package/cli/agents/solver-epic-solution-architect.md +39 -0
  46. package/cli/agents/solver-epic-test-architect.json +15 -0
  47. package/cli/agents/solver-epic-test-architect.md +39 -0
  48. package/cli/agents/solver-epic-ui.json +15 -0
  49. package/cli/agents/solver-epic-ui.md +39 -0
  50. package/cli/agents/solver-epic-ux.json +15 -0
  51. package/cli/agents/solver-epic-ux.md +39 -0
  52. package/cli/agents/solver-story-api.json +15 -0
  53. package/cli/agents/solver-story-api.md +39 -0
  54. package/cli/agents/solver-story-backend.json +15 -0
  55. package/cli/agents/solver-story-backend.md +39 -0
  56. package/cli/agents/solver-story-cloud.json +15 -0
  57. package/cli/agents/solver-story-cloud.md +39 -0
  58. package/cli/agents/solver-story-data.json +15 -0
  59. package/cli/agents/solver-story-data.md +39 -0
  60. package/cli/agents/solver-story-database.json +15 -0
  61. package/cli/agents/solver-story-database.md +39 -0
  62. package/cli/agents/solver-story-developer.json +15 -0
  63. package/cli/agents/solver-story-developer.md +39 -0
  64. package/cli/agents/solver-story-devops.json +15 -0
  65. package/cli/agents/solver-story-devops.md +39 -0
  66. package/cli/agents/solver-story-frontend.json +15 -0
  67. package/cli/agents/solver-story-frontend.md +39 -0
  68. package/cli/agents/solver-story-mobile.json +15 -0
  69. package/cli/agents/solver-story-mobile.md +39 -0
  70. package/cli/agents/solver-story-qa.json +15 -0
  71. package/cli/agents/solver-story-qa.md +39 -0
  72. package/cli/agents/solver-story-security.json +15 -0
  73. package/cli/agents/solver-story-security.md +39 -0
  74. package/cli/agents/solver-story-solution-architect.json +15 -0
  75. package/cli/agents/solver-story-solution-architect.md +39 -0
  76. package/cli/agents/solver-story-test-architect.json +15 -0
  77. package/cli/agents/solver-story-test-architect.md +39 -0
  78. package/cli/agents/solver-story-ui.json +15 -0
  79. package/cli/agents/solver-story-ui.md +39 -0
  80. package/cli/agents/solver-story-ux.json +15 -0
  81. package/cli/agents/solver-story-ux.md +39 -0
  82. package/cli/agents/story-doc-enricher.md +133 -0
  83. package/cli/agents/suggestion-business-analyst.md +88 -0
  84. package/cli/agents/suggestion-deployment-architect.md +263 -0
  85. package/cli/agents/suggestion-product-manager.md +129 -0
  86. package/cli/agents/suggestion-security-specialist.md +156 -0
  87. package/cli/agents/suggestion-technical-architect.md +269 -0
  88. package/cli/agents/suggestion-ux-researcher.md +93 -0
  89. package/cli/agents/task-subtask-decomposer.md +188 -0
  90. package/cli/agents/validator-documentation.json +152 -0
  91. package/cli/agents/validator-documentation.md +453 -0
  92. package/cli/agents/validator-epic-api.json +93 -0
  93. package/cli/agents/validator-epic-api.md +137 -0
  94. package/cli/agents/validator-epic-backend.json +93 -0
  95. package/cli/agents/validator-epic-backend.md +130 -0
  96. package/cli/agents/validator-epic-cloud.json +93 -0
  97. package/cli/agents/validator-epic-cloud.md +137 -0
  98. package/cli/agents/validator-epic-data.json +93 -0
  99. package/cli/agents/validator-epic-data.md +130 -0
  100. package/cli/agents/validator-epic-database.json +93 -0
  101. package/cli/agents/validator-epic-database.md +137 -0
  102. package/cli/agents/validator-epic-developer.json +74 -0
  103. package/cli/agents/validator-epic-developer.md +153 -0
  104. package/cli/agents/validator-epic-devops.json +74 -0
  105. package/cli/agents/validator-epic-devops.md +153 -0
  106. package/cli/agents/validator-epic-frontend.json +74 -0
  107. package/cli/agents/validator-epic-frontend.md +153 -0
  108. package/cli/agents/validator-epic-mobile.json +93 -0
  109. package/cli/agents/validator-epic-mobile.md +130 -0
  110. package/cli/agents/validator-epic-qa.json +93 -0
  111. package/cli/agents/validator-epic-qa.md +130 -0
  112. package/cli/agents/validator-epic-security.json +74 -0
  113. package/cli/agents/validator-epic-security.md +154 -0
  114. package/cli/agents/validator-epic-solution-architect.json +74 -0
  115. package/cli/agents/validator-epic-solution-architect.md +156 -0
  116. package/cli/agents/validator-epic-test-architect.json +93 -0
  117. package/cli/agents/validator-epic-test-architect.md +130 -0
  118. package/cli/agents/validator-epic-ui.json +93 -0
  119. package/cli/agents/validator-epic-ui.md +130 -0
  120. package/cli/agents/validator-epic-ux.json +93 -0
  121. package/cli/agents/validator-epic-ux.md +130 -0
  122. package/cli/agents/validator-selector.md +211 -0
  123. package/cli/agents/validator-story-api.json +104 -0
  124. package/cli/agents/validator-story-api.md +152 -0
  125. package/cli/agents/validator-story-backend.json +104 -0
  126. package/cli/agents/validator-story-backend.md +152 -0
  127. package/cli/agents/validator-story-cloud.json +104 -0
  128. package/cli/agents/validator-story-cloud.md +152 -0
  129. package/cli/agents/validator-story-data.json +104 -0
  130. package/cli/agents/validator-story-data.md +152 -0
  131. package/cli/agents/validator-story-database.json +104 -0
  132. package/cli/agents/validator-story-database.md +152 -0
  133. package/cli/agents/validator-story-developer.json +104 -0
  134. package/cli/agents/validator-story-developer.md +152 -0
  135. package/cli/agents/validator-story-devops.json +104 -0
  136. package/cli/agents/validator-story-devops.md +152 -0
  137. package/cli/agents/validator-story-frontend.json +104 -0
  138. package/cli/agents/validator-story-frontend.md +152 -0
  139. package/cli/agents/validator-story-mobile.json +104 -0
  140. package/cli/agents/validator-story-mobile.md +152 -0
  141. package/cli/agents/validator-story-qa.json +104 -0
  142. package/cli/agents/validator-story-qa.md +152 -0
  143. package/cli/agents/validator-story-security.json +104 -0
  144. package/cli/agents/validator-story-security.md +152 -0
  145. package/cli/agents/validator-story-solution-architect.json +104 -0
  146. package/cli/agents/validator-story-solution-architect.md +152 -0
  147. package/cli/agents/validator-story-test-architect.json +104 -0
  148. package/cli/agents/validator-story-test-architect.md +152 -0
  149. package/cli/agents/validator-story-ui.json +104 -0
  150. package/cli/agents/validator-story-ui.md +152 -0
  151. package/cli/agents/validator-story-ux.json +104 -0
  152. package/cli/agents/validator-story-ux.md +152 -0
  153. package/cli/ansi-colors.js +21 -0
  154. package/cli/build-docs.js +298 -0
  155. package/cli/ceremony-history.js +369 -0
  156. package/cli/command-logger.js +245 -0
  157. package/cli/components/static-output.js +63 -0
  158. package/cli/console-output-manager.js +94 -0
  159. package/cli/docs-sync.js +306 -0
  160. package/cli/epic-story-validator.js +1174 -0
  161. package/cli/evaluation-prompts.js +1008 -0
  162. package/cli/execution-context.js +195 -0
  163. package/cli/generate-summary-table.js +340 -0
  164. package/cli/index.js +3 -25
  165. package/cli/init-model-config.js +697 -0
  166. package/cli/init.js +1765 -100
  167. package/cli/kanban-server-manager.js +228 -0
  168. package/cli/llm-claude.js +109 -0
  169. package/cli/llm-gemini.js +115 -0
  170. package/cli/llm-mock.js +233 -0
  171. package/cli/llm-openai.js +233 -0
  172. package/cli/llm-provider.js +300 -0
  173. package/cli/llm-token-limits.js +102 -0
  174. package/cli/llm-verifier.js +454 -0
  175. package/cli/logger.js +32 -5
  176. package/cli/message-constants.js +58 -0
  177. package/cli/message-manager.js +334 -0
  178. package/cli/message-types.js +96 -0
  179. package/cli/messaging-api.js +297 -0
  180. package/cli/model-pricing.js +169 -0
  181. package/cli/model-query-engine.js +468 -0
  182. package/cli/model-recommendation-analyzer.js +495 -0
  183. package/cli/model-selector.js +269 -0
  184. package/cli/output-buffer.js +107 -0
  185. package/cli/process-manager.js +332 -0
  186. package/cli/repl-ink.js +5840 -504
  187. package/cli/repl-old.js +4 -4
  188. package/cli/seed-processor.js +792 -0
  189. package/cli/sprint-planning-processor.js +1813 -0
  190. package/cli/template-processor.js +2306 -108
  191. package/cli/templates/project.md +25 -8
  192. package/cli/templates/vitepress-config.mts.template +34 -0
  193. package/cli/token-tracker.js +520 -0
  194. package/cli/tools/generate-story-validators.js +317 -0
  195. package/cli/tools/generate-validators.js +669 -0
  196. package/cli/update-checker.js +19 -17
  197. package/cli/update-notifier.js +4 -4
  198. package/cli/validation-router.js +605 -0
  199. package/cli/verification-tracker.js +563 -0
  200. package/kanban/README.md +386 -0
  201. package/kanban/client/README.md +205 -0
  202. package/kanban/client/components.json +20 -0
  203. package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
  204. package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
  205. package/kanban/client/dist/index.html +16 -0
  206. package/kanban/client/dist/vite.svg +1 -0
  207. package/kanban/client/index.html +15 -0
  208. package/kanban/client/package-lock.json +9442 -0
  209. package/kanban/client/package.json +44 -0
  210. package/kanban/client/postcss.config.js +6 -0
  211. package/kanban/client/public/vite.svg +1 -0
  212. package/kanban/client/src/App.jsx +622 -0
  213. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  214. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
  215. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
  216. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
  217. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  218. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
  219. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
  220. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  221. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
  222. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  223. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  224. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  225. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
  226. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
  227. package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
  228. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  229. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  230. package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
  231. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  232. package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
  233. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  234. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
  235. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  236. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  237. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  238. package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
  239. package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
  240. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
  241. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
  242. package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
  243. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  244. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  245. package/kanban/client/src/components/stats/CostModal.jsx +353 -0
  246. package/kanban/client/src/components/ui/badge.jsx +27 -0
  247. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  248. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  249. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  250. package/kanban/client/src/hooks/useGrouping.js +118 -0
  251. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  252. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  253. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  254. package/kanban/client/src/lib/api.js +401 -0
  255. package/kanban/client/src/lib/status-grouping.js +144 -0
  256. package/kanban/client/src/lib/utils.js +11 -0
  257. package/kanban/client/src/main.jsx +10 -0
  258. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  259. package/kanban/client/src/store/ceremonyStore.js +172 -0
  260. package/kanban/client/src/store/filterStore.js +201 -0
  261. package/kanban/client/src/store/kanbanStore.js +115 -0
  262. package/kanban/client/src/store/processStore.js +65 -0
  263. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  264. package/kanban/client/src/styles/globals.css +59 -0
  265. package/kanban/client/tailwind.config.js +77 -0
  266. package/kanban/client/vite.config.js +28 -0
  267. package/kanban/client/vitest.config.js +28 -0
  268. package/kanban/dev-start.sh +47 -0
  269. package/kanban/package.json +12 -0
  270. package/kanban/server/index.js +516 -0
  271. package/kanban/server/routes/ceremony.js +305 -0
  272. package/kanban/server/routes/costs.js +157 -0
  273. package/kanban/server/routes/processes.js +50 -0
  274. package/kanban/server/routes/settings.js +303 -0
  275. package/kanban/server/routes/websocket.js +276 -0
  276. package/kanban/server/routes/work-items.js +347 -0
  277. package/kanban/server/services/CeremonyService.js +1190 -0
  278. package/kanban/server/services/FileSystemScanner.js +95 -0
  279. package/kanban/server/services/FileWatcher.js +144 -0
  280. package/kanban/server/services/HierarchyBuilder.js +196 -0
  281. package/kanban/server/services/ProcessRegistry.js +122 -0
  282. package/kanban/server/services/WorkItemReader.js +123 -0
  283. package/kanban/server/services/WorkItemRefineService.js +510 -0
  284. package/kanban/server/start.js +49 -0
  285. package/kanban/server/utils/kanban-logger.js +132 -0
  286. package/kanban/server/utils/markdown.js +91 -0
  287. package/kanban/server/utils/status-grouping.js +107 -0
  288. package/kanban/server/workers/sponsor-call-worker.js +84 -0
  289. package/kanban/server/workers/sprint-planning-worker.js +130 -0
  290. package/package.json +34 -7
@@ -0,0 +1,303 @@
1
+ import express from 'express';
2
+ import fs from 'fs/promises';
3
+ import { readdirSync, readFileSync, existsSync } from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __routeDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const LIB_AGENTS_PATH = path.join(__routeDir, '../../../cli/agents');
9
+ const customAgentsDir = (root) => path.join(root, '.avc', 'customized-agents');
10
+
11
+ /**
12
+ * Default model catalogue — mirrors the defaults in src/cli/init.js.
13
+ * Used as a fallback when a project's avc.json pre-dates model pricing support.
14
+ */
15
+ const PRICING_SOURCES = {
16
+ claude: 'https://www.anthropic.com/pricing',
17
+ gemini: 'https://ai.google.dev/pricing',
18
+ openai: 'https://openai.com/api/pricing',
19
+ };
20
+
21
+ const DEFAULT_MODELS = {
22
+ // Anthropic Claude models (prices per 1M tokens in USD)
23
+ 'claude-opus-4-6': { provider: 'claude', displayName: 'Claude Opus 4.6', pricing: { input: 5.00, output: 25.00, unit: 'million', source: PRICING_SOURCES.claude, lastUpdated: '2026-02-24' } },
24
+ 'claude-sonnet-4-6': { provider: 'claude', displayName: 'Claude Sonnet 4.6', pricing: { input: 3.00, output: 15.00, unit: 'million', source: PRICING_SOURCES.claude, lastUpdated: '2026-02-24' } },
25
+ 'claude-haiku-4-5-20251001': { provider: 'claude', displayName: 'Claude Haiku 4.5', pricing: { input: 1.00, output: 5.00, unit: 'million', source: PRICING_SOURCES.claude, lastUpdated: '2026-02-24' } },
26
+ // Google Gemini models (prices per 1M tokens in USD)
27
+ 'gemini-3.1-pro-preview': { provider: 'gemini', displayName: 'Gemini 3.1 Pro Preview', pricing: { input: 2.00, output: 12.00, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-03-05' } },
28
+ 'gemini-3-flash-preview': { provider: 'gemini', displayName: 'Gemini 3 Flash Preview', pricing: { input: 0.50, output: 3.00, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-03-05' } },
29
+ 'gemini-2.5-pro': { provider: 'gemini', displayName: 'Gemini 2.5 Pro', pricing: { input: 1.25, output: 10.00, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-02-24' } },
30
+ 'gemini-2.5-flash': { provider: 'gemini', displayName: 'Gemini 2.5 Flash', pricing: { input: 0.30, output: 2.50, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-02-24' } },
31
+ 'gemini-2.5-flash-lite': { provider: 'gemini', displayName: 'Gemini 2.5 Flash-Lite', pricing: { input: 0.10, output: 0.40, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-02-24' } },
32
+ // OpenAI models (prices per 1M tokens in USD)
33
+ 'gpt-5.2': { provider: 'openai', displayName: 'GPT-5.2', pricing: { input: 1.75, output: 14.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
34
+ 'gpt-5.1': { provider: 'openai', displayName: 'GPT-5.1', pricing: { input: 1.25, output: 10.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
35
+ 'gpt-5-mini': { provider: 'openai', displayName: 'GPT-5 mini', pricing: { input: 0.25, output: 2.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
36
+ 'o4-mini': { provider: 'openai', displayName: 'o4-mini', pricing: { input: 1.10, output: 4.40, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
37
+ 'o3': { provider: 'openai', displayName: 'o3', pricing: { input: 2.00, output: 8.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
38
+ 'o3-mini': { provider: 'openai', displayName: 'o3-mini', pricing: { input: 0.50, output: 2.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
39
+ 'gpt-5.2-codex': { provider: 'openai', displayName: 'GPT-5.2-Codex', pricing: { input: 1.75, output: 14.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
40
+ };
41
+
42
+ /**
43
+ * Settings Router
44
+ * Handles GET /api/settings and PUT sub-routes for project configuration.
45
+ * @param {string} projectRoot - Absolute path to project root
46
+ */
47
+ export function createSettingsRouter(projectRoot) {
48
+ const router = express.Router();
49
+ const avcJsonPath = path.join(projectRoot, '.avc', 'avc.json');
50
+ const envPath = path.join(projectRoot, '.env');
51
+
52
+ const readAvcConfig = async () => {
53
+ try {
54
+ return JSON.parse(await fs.readFile(avcJsonPath, 'utf8'));
55
+ } catch {
56
+ return {};
57
+ }
58
+ };
59
+
60
+ const writeAvcConfig = async (config) => {
61
+ await fs.writeFile(avcJsonPath, JSON.stringify(config, null, 2), 'utf8');
62
+ };
63
+
64
+ // Parse .env into key→value map
65
+ const readEnv = async () => {
66
+ try {
67
+ const lines = (await fs.readFile(envPath, 'utf8')).split('\n');
68
+ const map = {};
69
+ for (const line of lines) {
70
+ const m = line.match(/^([A-Z_]+)\s*=\s*(.*)$/);
71
+ if (m) map[m[1]] = m[2].replace(/^["']|["']$/g, '');
72
+ }
73
+ return map;
74
+ } catch {
75
+ return {};
76
+ }
77
+ };
78
+
79
+ // Update or insert a single key in .env, preserving all other lines
80
+ const upsertEnvKey = async (key, value) => {
81
+ let content = '';
82
+ try { content = await fs.readFile(envPath, 'utf8'); } catch {}
83
+ const lines = content.split('\n');
84
+ const idx = lines.findIndex(l => l.match(new RegExp(`^${key}\\s*=`)));
85
+ const newLine = value ? `${key}=${value}` : '';
86
+ if (idx >= 0) {
87
+ if (newLine) {
88
+ lines[idx] = newLine;
89
+ } else {
90
+ lines.splice(idx, 1);
91
+ }
92
+ } else if (newLine) {
93
+ lines.push(newLine);
94
+ }
95
+ await fs.writeFile(envPath, lines.join('\n'), 'utf8');
96
+ };
97
+
98
+ // GET /api/settings — snapshot of all configurable settings
99
+ router.get('/', async (req, res) => {
100
+ try {
101
+ const [config, env] = await Promise.all([readAvcConfig(), readEnv()]);
102
+ res.json({
103
+ apiKeys: {
104
+ anthropic: {
105
+ isSet: !!env.ANTHROPIC_API_KEY,
106
+ preview: env.ANTHROPIC_API_KEY ? env.ANTHROPIC_API_KEY.slice(0, 10) + '…' : '',
107
+ },
108
+ gemini: {
109
+ isSet: !!env.GEMINI_API_KEY,
110
+ preview: env.GEMINI_API_KEY ? env.GEMINI_API_KEY.slice(0, 10) + '…' : '',
111
+ },
112
+ openai: {
113
+ isSet: !!env.OPENAI_API_KEY,
114
+ preview: env.OPENAI_API_KEY ? env.OPENAI_API_KEY.slice(0, 10) + '…' : '',
115
+ },
116
+ },
117
+ ceremonies: config?.settings?.ceremonies || [],
118
+ models: (config?.settings?.models && Object.keys(config.settings.models).length > 0)
119
+ ? config.settings.models
120
+ : DEFAULT_MODELS,
121
+ missionGenerator: config?.settings?.missionGenerator || { validation: { maxIterations: 3, acceptanceThreshold: 95 } },
122
+ kanbanPort: config?.settings?.kanban?.port || 4174,
123
+ docsPort: config?.settings?.documentation?.port || 4173,
124
+ boardTitle: config?.settings?.kanban?.title || 'AVC Kanban Board',
125
+ costThresholds: config?.settings?.costThresholds || { 'sponsor-call': null, 'sprint-planning': null, 'seed': null },
126
+ });
127
+ } catch (err) {
128
+ res.status(500).json({ error: err.message });
129
+ }
130
+ });
131
+
132
+ // PUT /api/settings/api-keys — only sends keys that are being updated
133
+ router.put('/api-keys', async (req, res) => {
134
+ try {
135
+ const { anthropic, gemini, openai } = req.body;
136
+ if (anthropic !== undefined) await upsertEnvKey('ANTHROPIC_API_KEY', anthropic);
137
+ if (gemini !== undefined) await upsertEnvKey('GEMINI_API_KEY', gemini);
138
+ if (openai !== undefined) await upsertEnvKey('OPENAI_API_KEY', openai);
139
+ res.json({ status: 'ok' });
140
+ } catch (err) {
141
+ res.status(500).json({ error: err.message });
142
+ }
143
+ });
144
+
145
+ // PUT /api/settings/ceremonies — also accepts missionGenerator alongside ceremonies
146
+ router.put('/ceremonies', async (req, res) => {
147
+ try {
148
+ const { ceremonies, missionGenerator } = req.body;
149
+ if (!Array.isArray(ceremonies)) {
150
+ return res.status(400).json({ error: 'ceremonies must be an array' });
151
+ }
152
+ const config = await readAvcConfig();
153
+ if (!config.settings) config.settings = {};
154
+ config.settings.ceremonies = ceremonies;
155
+ // Persist missionGenerator validation params if provided
156
+ if (missionGenerator?.validation && typeof missionGenerator.validation === 'object') {
157
+ if (!config.settings.missionGenerator) config.settings.missionGenerator = {};
158
+ config.settings.missionGenerator.validation = {
159
+ maxIterations: Number(missionGenerator.validation.maxIterations) || 3,
160
+ acceptanceThreshold: Number(missionGenerator.validation.acceptanceThreshold) || 95,
161
+ };
162
+ }
163
+ await writeAvcConfig(config);
164
+ res.json({ status: 'ok' });
165
+ } catch (err) {
166
+ res.status(500).json({ error: err.message });
167
+ }
168
+ });
169
+
170
+ // PUT /api/settings/models — update pricing for all models
171
+ router.put('/models', async (req, res) => {
172
+ try {
173
+ const { models } = req.body;
174
+ if (!models || typeof models !== 'object' || Array.isArray(models)) {
175
+ return res.status(400).json({ error: 'models must be an object' });
176
+ }
177
+ const config = await readAvcConfig();
178
+ if (!config.settings) config.settings = {};
179
+ // Seed from defaults if models have never been persisted (migration for old projects)
180
+ if (!config.settings.models || Object.keys(config.settings.models).length === 0) {
181
+ config.settings.models = JSON.parse(JSON.stringify(DEFAULT_MODELS));
182
+ }
183
+ for (const [modelId, data] of Object.entries(models)) {
184
+ if (!config.settings.models[modelId]) continue; // only update existing models
185
+ if (data.pricing && typeof data.pricing === 'object') {
186
+ const today = new Date().toISOString().split('T')[0];
187
+ config.settings.models[modelId].pricing = {
188
+ input: Number(data.pricing.input) || 0,
189
+ output: Number(data.pricing.output) || 0,
190
+ unit: data.pricing.unit === 'thousand' ? 'thousand' : 'million',
191
+ source: typeof data.pricing.source === 'string' ? data.pricing.source.trim() : '',
192
+ lastUpdated: today,
193
+ };
194
+ }
195
+ }
196
+ await writeAvcConfig(config);
197
+ res.json({ status: 'ok' });
198
+ } catch (err) {
199
+ res.status(500).json({ error: err.message });
200
+ }
201
+ });
202
+
203
+ // PUT /api/settings/general — board title and/or ports
204
+ router.put('/general', async (req, res) => {
205
+ try {
206
+ const { boardTitle, kanbanPort, docsPort } = req.body;
207
+ const config = await readAvcConfig();
208
+ if (!config.settings) config.settings = {};
209
+ if (!config.settings.kanban) config.settings.kanban = {};
210
+ if (!config.settings.documentation) config.settings.documentation = {};
211
+ if (boardTitle !== undefined) config.settings.kanban.title = boardTitle.trim();
212
+ if (kanbanPort !== undefined) config.settings.kanban.port = Number(kanbanPort);
213
+ if (docsPort !== undefined) config.settings.documentation.port = Number(docsPort);
214
+ await writeAvcConfig(config);
215
+ res.json({ status: 'ok' });
216
+ } catch (err) {
217
+ res.status(500).json({ error: err.message });
218
+ }
219
+ });
220
+
221
+ // PUT /api/settings/cost-thresholds — update per-ceremony cost limits
222
+ router.put('/cost-thresholds', async (req, res) => {
223
+ try {
224
+ const { thresholds } = req.body;
225
+ const config = await readAvcConfig();
226
+ if (!config.settings) config.settings = {};
227
+ config.settings.costThresholds = thresholds;
228
+ await writeAvcConfig(config);
229
+ res.json({ ok: true });
230
+ } catch (err) {
231
+ res.status(500).json({ error: err.message });
232
+ }
233
+ });
234
+
235
+ // GET /api/settings/agents — list all agent names with customization status
236
+ router.get('/agents', (req, res) => {
237
+ try {
238
+ const names = readdirSync(LIB_AGENTS_PATH).filter(f => f.endsWith('.md')).sort();
239
+ const customDir = customAgentsDir(projectRoot);
240
+ const agents = names.map(name => ({
241
+ name,
242
+ isCustomized: existsSync(path.join(customDir, name)),
243
+ }));
244
+ res.json({ agents });
245
+ } catch (err) {
246
+ res.status(500).json({ error: err.message });
247
+ }
248
+ });
249
+
250
+ // GET /api/settings/agents/:name — get agent content (customized or default)
251
+ router.get('/agents/:name', (req, res) => {
252
+ const { name } = req.params;
253
+ if (!name.endsWith('.md') || name.includes('/') || name.includes('\\')) {
254
+ return res.status(400).json({ error: 'Invalid name' });
255
+ }
256
+ try {
257
+ const customPath = path.join(customAgentsDir(projectRoot), name);
258
+ const libPath = path.join(LIB_AGENTS_PATH, name);
259
+ if (!existsSync(libPath)) return res.status(404).json({ error: 'Agent not found' });
260
+ const isCustomized = existsSync(customPath);
261
+ const content = readFileSync(isCustomized ? customPath : libPath, 'utf8');
262
+ const defaultContent = readFileSync(libPath, 'utf8');
263
+ res.json({ name, content, isCustomized, defaultContent });
264
+ } catch (err) {
265
+ res.status(500).json({ error: err.message });
266
+ }
267
+ });
268
+
269
+ // PUT /api/settings/agents/:name — save customized agent
270
+ router.put('/agents/:name', async (req, res) => {
271
+ const { name } = req.params;
272
+ const { content } = req.body;
273
+ if (!name.endsWith('.md') || name.includes('/') || name.includes('\\')) {
274
+ return res.status(400).json({ error: 'Invalid name' });
275
+ }
276
+ if (typeof content !== 'string') return res.status(400).json({ error: 'content must be a string' });
277
+ try {
278
+ const dir = customAgentsDir(projectRoot);
279
+ await fs.mkdir(dir, { recursive: true });
280
+ await fs.writeFile(path.join(dir, name), content, 'utf8');
281
+ res.json({ status: 'ok' });
282
+ } catch (err) {
283
+ res.status(500).json({ error: err.message });
284
+ }
285
+ });
286
+
287
+ // DELETE /api/settings/agents/:name — reset to library default
288
+ router.delete('/agents/:name', async (req, res) => {
289
+ const { name } = req.params;
290
+ if (!name.endsWith('.md') || name.includes('/') || name.includes('\\')) {
291
+ return res.status(400).json({ error: 'Invalid name' });
292
+ }
293
+ try {
294
+ const customPath = path.join(customAgentsDir(projectRoot), name);
295
+ try { await fs.unlink(customPath); } catch {}
296
+ res.json({ status: 'ok' });
297
+ } catch (err) {
298
+ res.status(500).json({ error: err.message });
299
+ }
300
+ });
301
+
302
+ return router;
303
+ }
@@ -0,0 +1,276 @@
1
+ import { WebSocketServer } from 'ws';
2
+
3
+ /**
4
+ * Setup WebSocket server for real-time updates
5
+ * @param {http.Server} server - HTTP server instance
6
+ * @param {object} dataStore - Data store with work items
7
+ * @returns {WebSocketServer}
8
+ */
9
+ export function setupWebSocket(server, dataStore, processRegistry = null, ceremonyService = null) {
10
+ const wss = new WebSocketServer({ server, path: '/ws' });
11
+
12
+ const clients = new Set();
13
+
14
+ wss.on('connection', (ws) => {
15
+ console.log('WebSocket client connected');
16
+ clients.add(ws);
17
+
18
+ // Send initial data
19
+ ws.send(
20
+ JSON.stringify({
21
+ type: 'init',
22
+ data: {
23
+ message: 'Connected to AVC Kanban Board',
24
+ timestamp: Date.now(),
25
+ },
26
+ })
27
+ );
28
+
29
+ // Send current process list so the client is immediately in sync
30
+ if (processRegistry) {
31
+ ws.send(JSON.stringify({ type: 'process:list', processes: processRegistry.list() }));
32
+ }
33
+
34
+ // Send current ceremony status so the client restores running state on reconnect
35
+ if (ceremonyService) {
36
+ const ceremonyStatus = ceremonyService.getStatus();
37
+ if (
38
+ ceremonyStatus.status === 'running' ||
39
+ ceremonyStatus.status === 'cost-limit-pending' ||
40
+ ceremonyStatus.status === 'awaiting-selection'
41
+ ) {
42
+ ws.send(JSON.stringify({ type: 'ceremony:sync', ceremonyStatus }));
43
+ }
44
+ }
45
+
46
+ // Handle client messages
47
+ ws.on('message', (message) => {
48
+ try {
49
+ const data = JSON.parse(message);
50
+ console.log('WebSocket message received:', data);
51
+
52
+ // Handle different message types
53
+ if (data.type === 'ping') {
54
+ ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
55
+ }
56
+ } catch (error) {
57
+ console.error('Error handling WebSocket message:', error);
58
+ }
59
+ });
60
+
61
+ // Handle client disconnect
62
+ ws.on('close', () => {
63
+ console.log('WebSocket client disconnected');
64
+ clients.delete(ws);
65
+ });
66
+
67
+ // Handle errors
68
+ ws.on('error', (error) => {
69
+ console.error('WebSocket error:', error);
70
+ clients.delete(ws);
71
+ });
72
+ });
73
+
74
+ /**
75
+ * Broadcast message to all connected clients
76
+ * @param {object} message - Message to broadcast
77
+ */
78
+ function broadcast(message) {
79
+ const payload = JSON.stringify(message);
80
+
81
+ clients.forEach((client) => {
82
+ if (client.readyState === 1) {
83
+ // WebSocket.OPEN
84
+ client.send(payload);
85
+ }
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Broadcast work item update
91
+ * @param {string} action - 'added' | 'changed' | 'deleted'
92
+ * @param {string} workItemId - Work item ID
93
+ */
94
+ function broadcastWorkItemUpdate(action, workItemId) {
95
+ const { items } = dataStore.getHierarchy();
96
+ const item = items.get(workItemId);
97
+
98
+ broadcast({
99
+ type: 'work-item-update',
100
+ action,
101
+ data: {
102
+ id: workItemId,
103
+ item: item ? cleanWorkItemForBroadcast(item) : null,
104
+ timestamp: Date.now(),
105
+ },
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Broadcast full refresh (when major changes occur)
111
+ */
112
+ function broadcastRefresh() {
113
+ broadcast({
114
+ type: 'refresh',
115
+ data: {
116
+ message: 'Work items updated, please refresh',
117
+ timestamp: Date.now(),
118
+ },
119
+ });
120
+ }
121
+
122
+ function broadcastCeremonyProgress(message) {
123
+ broadcast({ type: 'ceremony:progress', message });
124
+ }
125
+
126
+ function broadcastCeremonySubstep(substep, meta = {}) {
127
+ broadcast({ type: 'ceremony:substep', substep, meta });
128
+ }
129
+
130
+ function broadcastCeremonyComplete(result) {
131
+ broadcast({ type: 'ceremony:complete', result });
132
+ }
133
+
134
+ function broadcastCeremonyError(error) {
135
+ broadcast({ type: 'ceremony:error', error });
136
+ }
137
+
138
+ function broadcastSprintPlanningProgress(message) {
139
+ broadcast({ type: 'sprint-planning:progress', message });
140
+ }
141
+
142
+ function broadcastSprintPlanningSubstep(substep, meta) {
143
+ broadcast({ type: 'sprint-planning:substep', substep, meta });
144
+ }
145
+
146
+ function broadcastSprintPlanningComplete(result) {
147
+ broadcast({ type: 'sprint-planning:complete', result });
148
+ }
149
+
150
+ function broadcastSprintPlanningError(error) {
151
+ broadcast({ type: 'sprint-planning:error', error });
152
+ }
153
+
154
+ function broadcastMissionProgress(step, message) {
155
+ broadcast({ type: 'mission:progress', step, message });
156
+ }
157
+
158
+ function broadcastCostUpdate() {
159
+ broadcast({ type: 'cost:update' });
160
+ }
161
+
162
+ function broadcastCostLimit(cost, threshold, runningType) {
163
+ broadcast({ type: 'ceremony:cost-limit', cost, threshold, runningType });
164
+ }
165
+
166
+ function broadcastSprintPlanningDecompositionComplete(hierarchy) {
167
+ broadcast({ type: 'sprint-planning:decomposition-complete', hierarchy });
168
+ }
169
+
170
+ function broadcastSprintPlanningPaused() {
171
+ broadcast({ type: 'sprint-planning:paused' });
172
+ }
173
+
174
+ function broadcastSprintPlanningResumed() {
175
+ broadcast({ type: 'sprint-planning:resumed' });
176
+ }
177
+
178
+ function broadcastSprintPlanningCancelled() {
179
+ broadcast({ type: 'sprint-planning:cancelled' });
180
+ }
181
+
182
+ function broadcastSprintPlanningDetail(detail) {
183
+ broadcast({ type: 'sprint-planning:detail', detail });
184
+ }
185
+
186
+ function broadcastCeremonyDetail(detail) {
187
+ broadcast({ type: 'ceremony:detail', detail });
188
+ }
189
+
190
+ function broadcastCeremonyPaused() {
191
+ broadcast({ type: 'ceremony:paused' });
192
+ }
193
+
194
+ function broadcastCeremonyResumed() {
195
+ broadcast({ type: 'ceremony:resumed' });
196
+ }
197
+
198
+ function broadcastCeremonyCancelled() {
199
+ broadcast({ type: 'ceremony:cancelled' });
200
+ }
201
+
202
+ function broadcastProcessStarted(record) {
203
+ broadcast({
204
+ type: 'process:started',
205
+ processId: record.id,
206
+ processType: record.type,
207
+ label: record.label,
208
+ startedAt: record.startedAt,
209
+ });
210
+ }
211
+
212
+ function broadcastProcessStatus(processId, status, extra = {}) {
213
+ broadcast({ type: 'process:status', processId, status, ...extra });
214
+ }
215
+
216
+ function broadcastRefineProgress(itemId, jobId, message) {
217
+ broadcast({ type: 'refine:progress', itemId, jobId, message });
218
+ }
219
+
220
+ function broadcastRefineComplete(itemId, jobId, result) {
221
+ broadcast({ type: 'refine:complete', itemId, jobId, result });
222
+ }
223
+
224
+ function broadcastRefineError(itemId, jobId, error) {
225
+ broadcast({ type: 'refine:error', itemId, jobId, error });
226
+ }
227
+
228
+ return {
229
+ wss,
230
+ broadcast,
231
+ broadcastWorkItemUpdate,
232
+ broadcastRefresh,
233
+ broadcastCeremonyProgress,
234
+ broadcastCeremonySubstep,
235
+ broadcastCeremonyComplete,
236
+ broadcastCeremonyError,
237
+ broadcastCeremonyPaused,
238
+ broadcastCeremonyResumed,
239
+ broadcastCeremonyCancelled,
240
+ broadcastSprintPlanningProgress,
241
+ broadcastSprintPlanningSubstep,
242
+ broadcastSprintPlanningDetail,
243
+ broadcastSprintPlanningDecompositionComplete,
244
+ broadcastSprintPlanningComplete,
245
+ broadcastSprintPlanningError,
246
+ broadcastSprintPlanningPaused,
247
+ broadcastSprintPlanningResumed,
248
+ broadcastSprintPlanningCancelled,
249
+ broadcastCeremonyDetail,
250
+ broadcastMissionProgress,
251
+ broadcastCostUpdate,
252
+ broadcastCostLimit,
253
+ broadcastProcessStarted,
254
+ broadcastProcessStatus,
255
+ broadcastRefineProgress,
256
+ broadcastRefineComplete,
257
+ broadcastRefineError,
258
+ getClientCount: () => clients.size,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Clean work item for broadcast (remove circular references)
264
+ * @param {object} item - Work item
265
+ * @returns {object} Cleaned work item
266
+ */
267
+ function cleanWorkItemForBroadcast(item) {
268
+ return {
269
+ id: item.id,
270
+ name: item.name,
271
+ type: item._type,
272
+ status: item.status,
273
+ parentId: item._parentId,
274
+ childrenIds: item._children ? item._children.map((c) => c.id) : [],
275
+ };
276
+ }