@agile-vibe-coding/avc 0.2.3 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/README.md +475 -3
  2. package/cli/agents/agent-selector.md +23 -0
  3. package/cli/agents/code-implementer.md +117 -0
  4. package/cli/agents/code-validator.md +80 -0
  5. package/cli/agents/context-reviewer-epic.md +101 -0
  6. package/cli/agents/context-reviewer-story.md +92 -0
  7. package/cli/agents/context-writer-epic.md +145 -0
  8. package/cli/agents/context-writer-story.md +111 -0
  9. package/cli/agents/doc-writer-epic.md +42 -0
  10. package/cli/agents/doc-writer-story.md +43 -0
  11. package/cli/agents/duplicate-detector.md +110 -0
  12. package/cli/agents/epic-story-decomposer.md +318 -39
  13. package/cli/agents/mission-scope-generator.md +68 -4
  14. package/cli/agents/mission-scope-validator.md +40 -6
  15. package/cli/agents/project-context-extractor.md +21 -6
  16. package/cli/agents/scaffolding-generator.md +99 -0
  17. package/cli/agents/seed-validator.md +71 -0
  18. package/cli/agents/story-scope-reviewer.md +147 -0
  19. package/cli/agents/story-splitter.md +83 -0
  20. package/cli/agents/validator-documentation.json +31 -0
  21. package/cli/agents/validator-documentation.md +3 -1
  22. package/cli/api-reference-tool.js +368 -0
  23. package/cli/checks/catalog.json +76 -0
  24. package/cli/checks/code/quality.json +26 -0
  25. package/cli/checks/code/testing.json +14 -0
  26. package/cli/checks/code/traceability.json +26 -0
  27. package/cli/checks/cross-refs/epic.json +171 -0
  28. package/cli/checks/cross-refs/story.json +149 -0
  29. package/cli/checks/epic/api.json +114 -0
  30. package/cli/checks/epic/backend.json +126 -0
  31. package/cli/checks/epic/cloud.json +126 -0
  32. package/cli/checks/epic/data.json +102 -0
  33. package/cli/checks/epic/database.json +114 -0
  34. package/cli/checks/epic/developer.json +182 -0
  35. package/cli/checks/epic/devops.json +174 -0
  36. package/cli/checks/epic/frontend.json +162 -0
  37. package/cli/checks/epic/mobile.json +102 -0
  38. package/cli/checks/epic/qa.json +90 -0
  39. package/cli/checks/epic/security.json +184 -0
  40. package/cli/checks/epic/solution-architect.json +192 -0
  41. package/cli/checks/epic/test-architect.json +90 -0
  42. package/cli/checks/epic/ui.json +102 -0
  43. package/cli/checks/epic/ux.json +90 -0
  44. package/cli/checks/fixes/epic-fix-template.md +10 -0
  45. package/cli/checks/fixes/story-fix-template.md +10 -0
  46. package/cli/checks/story/api.json +186 -0
  47. package/cli/checks/story/backend.json +102 -0
  48. package/cli/checks/story/cloud.json +102 -0
  49. package/cli/checks/story/data.json +210 -0
  50. package/cli/checks/story/database.json +102 -0
  51. package/cli/checks/story/developer.json +168 -0
  52. package/cli/checks/story/devops.json +102 -0
  53. package/cli/checks/story/frontend.json +174 -0
  54. package/cli/checks/story/mobile.json +102 -0
  55. package/cli/checks/story/qa.json +210 -0
  56. package/cli/checks/story/security.json +198 -0
  57. package/cli/checks/story/solution-architect.json +230 -0
  58. package/cli/checks/story/test-architect.json +210 -0
  59. package/cli/checks/story/ui.json +102 -0
  60. package/cli/checks/story/ux.json +102 -0
  61. package/cli/coding-order.js +401 -0
  62. package/cli/dependency-checker.js +72 -0
  63. package/cli/epic-story-validator.js +284 -799
  64. package/cli/index.js +0 -0
  65. package/cli/init-model-config.js +17 -10
  66. package/cli/init.js +514 -92
  67. package/cli/kanban-server-manager.js +1 -2
  68. package/cli/llm-claude.js +98 -31
  69. package/cli/llm-gemini.js +29 -5
  70. package/cli/llm-local.js +493 -0
  71. package/cli/llm-openai.js +262 -41
  72. package/cli/llm-provider.js +147 -8
  73. package/cli/llm-token-limits.js +113 -4
  74. package/cli/llm-verifier.js +209 -1
  75. package/cli/llm-xiaomi.js +143 -0
  76. package/cli/message-constants.js +3 -12
  77. package/cli/messaging-api.js +6 -12
  78. package/cli/micro-check-fixer.js +335 -0
  79. package/cli/micro-check-runner.js +449 -0
  80. package/cli/micro-check-scorer.js +148 -0
  81. package/cli/micro-check-validator.js +538 -0
  82. package/cli/model-pricing.js +23 -0
  83. package/cli/model-selector.js +3 -2
  84. package/cli/prompt-logger.js +57 -0
  85. package/cli/repl-ink.js +106 -346
  86. package/cli/repl-old.js +1 -2
  87. package/cli/seed-processor.js +194 -24
  88. package/cli/sprint-planning-processor.js +2638 -289
  89. package/cli/template-processor.js +50 -3
  90. package/cli/token-tracker.js +50 -23
  91. package/cli/tools/generate-story-validators.js +1 -1
  92. package/cli/validation-router.js +70 -8
  93. package/cli/worktree-runner.js +654 -0
  94. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  95. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  96. package/kanban/client/dist/index.html +2 -2
  97. package/kanban/client/src/App.jsx +43 -14
  98. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  99. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  100. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  101. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  102. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  103. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  104. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  105. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  106. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  107. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  108. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  109. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  110. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  111. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  112. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  113. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  114. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  115. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  116. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  117. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  118. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  119. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  120. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  121. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  122. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  123. package/kanban/client/src/hooks/useGrouping.js +59 -0
  124. package/kanban/client/src/lib/api.js +118 -4
  125. package/kanban/client/src/lib/status-grouping.js +10 -0
  126. package/kanban/client/src/store/kanbanStore.js +8 -0
  127. package/kanban/server/index.js +23 -2
  128. package/kanban/server/routes/ceremony.js +153 -4
  129. package/kanban/server/routes/costs.js +9 -3
  130. package/kanban/server/routes/openai-oauth.js +366 -0
  131. package/kanban/server/routes/settings.js +447 -14
  132. package/kanban/server/routes/websocket.js +7 -2
  133. package/kanban/server/routes/work-items.js +141 -1
  134. package/kanban/server/services/CeremonyService.js +275 -24
  135. package/kanban/server/services/TaskRunnerService.js +261 -0
  136. package/kanban/server/workers/run-task-worker.js +121 -0
  137. package/kanban/server/workers/seed-worker.js +94 -0
  138. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  139. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  140. package/package.json +2 -3
  141. package/cli/agents/solver-epic-api.json +0 -15
  142. package/cli/agents/solver-epic-api.md +0 -39
  143. package/cli/agents/solver-epic-backend.json +0 -15
  144. package/cli/agents/solver-epic-backend.md +0 -39
  145. package/cli/agents/solver-epic-cloud.json +0 -15
  146. package/cli/agents/solver-epic-cloud.md +0 -39
  147. package/cli/agents/solver-epic-data.json +0 -15
  148. package/cli/agents/solver-epic-data.md +0 -39
  149. package/cli/agents/solver-epic-database.json +0 -15
  150. package/cli/agents/solver-epic-database.md +0 -39
  151. package/cli/agents/solver-epic-developer.json +0 -15
  152. package/cli/agents/solver-epic-developer.md +0 -39
  153. package/cli/agents/solver-epic-devops.json +0 -15
  154. package/cli/agents/solver-epic-devops.md +0 -39
  155. package/cli/agents/solver-epic-frontend.json +0 -15
  156. package/cli/agents/solver-epic-frontend.md +0 -39
  157. package/cli/agents/solver-epic-mobile.json +0 -15
  158. package/cli/agents/solver-epic-mobile.md +0 -39
  159. package/cli/agents/solver-epic-qa.json +0 -15
  160. package/cli/agents/solver-epic-qa.md +0 -39
  161. package/cli/agents/solver-epic-security.json +0 -15
  162. package/cli/agents/solver-epic-security.md +0 -39
  163. package/cli/agents/solver-epic-solution-architect.json +0 -15
  164. package/cli/agents/solver-epic-solution-architect.md +0 -39
  165. package/cli/agents/solver-epic-test-architect.json +0 -15
  166. package/cli/agents/solver-epic-test-architect.md +0 -39
  167. package/cli/agents/solver-epic-ui.json +0 -15
  168. package/cli/agents/solver-epic-ui.md +0 -39
  169. package/cli/agents/solver-epic-ux.json +0 -15
  170. package/cli/agents/solver-epic-ux.md +0 -39
  171. package/cli/agents/solver-story-api.json +0 -15
  172. package/cli/agents/solver-story-api.md +0 -39
  173. package/cli/agents/solver-story-backend.json +0 -15
  174. package/cli/agents/solver-story-backend.md +0 -39
  175. package/cli/agents/solver-story-cloud.json +0 -15
  176. package/cli/agents/solver-story-cloud.md +0 -39
  177. package/cli/agents/solver-story-data.json +0 -15
  178. package/cli/agents/solver-story-data.md +0 -39
  179. package/cli/agents/solver-story-database.json +0 -15
  180. package/cli/agents/solver-story-database.md +0 -39
  181. package/cli/agents/solver-story-developer.json +0 -15
  182. package/cli/agents/solver-story-developer.md +0 -39
  183. package/cli/agents/solver-story-devops.json +0 -15
  184. package/cli/agents/solver-story-devops.md +0 -39
  185. package/cli/agents/solver-story-frontend.json +0 -15
  186. package/cli/agents/solver-story-frontend.md +0 -39
  187. package/cli/agents/solver-story-mobile.json +0 -15
  188. package/cli/agents/solver-story-mobile.md +0 -39
  189. package/cli/agents/solver-story-qa.json +0 -15
  190. package/cli/agents/solver-story-qa.md +0 -39
  191. package/cli/agents/solver-story-security.json +0 -15
  192. package/cli/agents/solver-story-security.md +0 -39
  193. package/cli/agents/solver-story-solution-architect.json +0 -15
  194. package/cli/agents/solver-story-solution-architect.md +0 -39
  195. package/cli/agents/solver-story-test-architect.json +0 -15
  196. package/cli/agents/solver-story-test-architect.md +0 -39
  197. package/cli/agents/solver-story-ui.json +0 -15
  198. package/cli/agents/solver-story-ui.md +0 -39
  199. package/cli/agents/solver-story-ux.json +0 -15
  200. package/cli/agents/solver-story-ux.md +0 -39
  201. package/cli/agents/validator-epic-api.json +0 -93
  202. package/cli/agents/validator-epic-api.md +0 -137
  203. package/cli/agents/validator-epic-backend.json +0 -93
  204. package/cli/agents/validator-epic-backend.md +0 -130
  205. package/cli/agents/validator-epic-cloud.json +0 -93
  206. package/cli/agents/validator-epic-cloud.md +0 -137
  207. package/cli/agents/validator-epic-data.json +0 -93
  208. package/cli/agents/validator-epic-data.md +0 -130
  209. package/cli/agents/validator-epic-database.json +0 -93
  210. package/cli/agents/validator-epic-database.md +0 -137
  211. package/cli/agents/validator-epic-developer.json +0 -74
  212. package/cli/agents/validator-epic-developer.md +0 -153
  213. package/cli/agents/validator-epic-devops.json +0 -74
  214. package/cli/agents/validator-epic-devops.md +0 -153
  215. package/cli/agents/validator-epic-frontend.json +0 -74
  216. package/cli/agents/validator-epic-frontend.md +0 -153
  217. package/cli/agents/validator-epic-mobile.json +0 -93
  218. package/cli/agents/validator-epic-mobile.md +0 -130
  219. package/cli/agents/validator-epic-qa.json +0 -93
  220. package/cli/agents/validator-epic-qa.md +0 -130
  221. package/cli/agents/validator-epic-security.json +0 -74
  222. package/cli/agents/validator-epic-security.md +0 -154
  223. package/cli/agents/validator-epic-solution-architect.json +0 -74
  224. package/cli/agents/validator-epic-solution-architect.md +0 -156
  225. package/cli/agents/validator-epic-test-architect.json +0 -93
  226. package/cli/agents/validator-epic-test-architect.md +0 -130
  227. package/cli/agents/validator-epic-ui.json +0 -93
  228. package/cli/agents/validator-epic-ui.md +0 -130
  229. package/cli/agents/validator-epic-ux.json +0 -93
  230. package/cli/agents/validator-epic-ux.md +0 -130
  231. package/cli/agents/validator-story-api.json +0 -104
  232. package/cli/agents/validator-story-api.md +0 -152
  233. package/cli/agents/validator-story-backend.json +0 -104
  234. package/cli/agents/validator-story-backend.md +0 -152
  235. package/cli/agents/validator-story-cloud.json +0 -104
  236. package/cli/agents/validator-story-cloud.md +0 -152
  237. package/cli/agents/validator-story-data.json +0 -104
  238. package/cli/agents/validator-story-data.md +0 -152
  239. package/cli/agents/validator-story-database.json +0 -104
  240. package/cli/agents/validator-story-database.md +0 -152
  241. package/cli/agents/validator-story-developer.json +0 -104
  242. package/cli/agents/validator-story-developer.md +0 -152
  243. package/cli/agents/validator-story-devops.json +0 -104
  244. package/cli/agents/validator-story-devops.md +0 -152
  245. package/cli/agents/validator-story-frontend.json +0 -104
  246. package/cli/agents/validator-story-frontend.md +0 -152
  247. package/cli/agents/validator-story-mobile.json +0 -104
  248. package/cli/agents/validator-story-mobile.md +0 -152
  249. package/cli/agents/validator-story-qa.json +0 -104
  250. package/cli/agents/validator-story-qa.md +0 -152
  251. package/cli/agents/validator-story-security.json +0 -104
  252. package/cli/agents/validator-story-security.md +0 -152
  253. package/cli/agents/validator-story-solution-architect.json +0 -104
  254. package/cli/agents/validator-story-solution-architect.md +0 -152
  255. package/cli/agents/validator-story-test-architect.json +0 -104
  256. package/cli/agents/validator-story-test-architect.md +0 -152
  257. package/cli/agents/validator-story-ui.json +0 -104
  258. package/cli/agents/validator-story-ui.md +0 -152
  259. package/cli/agents/validator-story-ux.json +0 -104
  260. package/cli/agents/validator-story-ux.md +0 -152
  261. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  262. package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
@@ -6,7 +6,9 @@ import { fileURLToPath } from 'url';
6
6
 
7
7
  const __routeDir = path.dirname(fileURLToPath(import.meta.url));
8
8
  const LIB_AGENTS_PATH = path.join(__routeDir, '../../../cli/agents');
9
+ const LIB_CHECKS_PATH = path.join(__routeDir, '../../../cli/checks');
9
10
  const customAgentsDir = (root) => path.join(root, '.avc', 'customized-agents');
11
+ const customChecksDir = (root) => path.join(root, '.avc', 'customized-agents', 'checks');
10
12
 
11
13
  /**
12
14
  * Default model catalogue — mirrors the defaults in src/cli/init.js.
@@ -30,15 +32,160 @@ const DEFAULT_MODELS = {
30
32
  '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
33
  '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
34
  // 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.4': { provider: 'openai', displayName: 'GPT-5.4', pricing: { input: 2.50, output: 15.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-03-06' } },
36
+ 'gpt-5.4-pro': { provider: 'openai', displayName: 'GPT-5.4 Pro', pricing: { input: 30.00, output: 180.00,unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-03-06' } },
35
37
  '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' } },
38
+ 'gpt-5-nano': { provider: 'openai', displayName: 'GPT-5 nano', pricing: { input: 0.05, output: 0.40, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-03-06' } },
40
39
  };
41
40
 
41
+ /**
42
+ * Provider preset defaults — mirrors providerPresets in src/cli/init.js.
43
+ * Injected server-side into ceremony objects that pre-date this feature.
44
+ */
45
+ const PROVIDER_PRESETS = {
46
+ 'sponsor-call': {
47
+ claude: {
48
+ provider: 'claude', defaultModel: 'claude-sonnet-4-6',
49
+ stages: {
50
+ suggestions: { provider: 'claude', model: 'claude-sonnet-4-6' },
51
+ documentation: { provider: 'claude', model: 'claude-sonnet-4-6' },
52
+ 'architecture-recommendation': { provider: 'claude', model: 'claude-opus-4-6' },
53
+ 'question-prefilling': { provider: 'claude', model: 'claude-haiku-4-5-20251001' },
54
+ },
55
+ validation: {
56
+ provider: 'claude', model: 'claude-haiku-4-5-20251001',
57
+ documentation: { provider: 'claude', model: 'claude-haiku-4-5-20251001' },
58
+ refinement: { provider: 'claude', model: 'claude-sonnet-4-6' },
59
+ },
60
+ },
61
+ gemini: {
62
+ provider: 'gemini', defaultModel: 'gemini-2.5-flash',
63
+ stages: {
64
+ suggestions: { provider: 'gemini', model: 'gemini-2.5-flash' },
65
+ documentation: { provider: 'gemini', model: 'gemini-2.5-flash' },
66
+ 'architecture-recommendation': { provider: 'gemini', model: 'gemini-2.5-pro' },
67
+ 'question-prefilling': { provider: 'gemini', model: 'gemini-2.5-flash-lite' },
68
+ },
69
+ validation: {
70
+ provider: 'gemini', model: 'gemini-2.5-flash-lite',
71
+ documentation: { provider: 'gemini', model: 'gemini-2.5-flash-lite' },
72
+ refinement: { provider: 'gemini', model: 'gemini-2.5-flash' },
73
+ },
74
+ },
75
+ openai: {
76
+ provider: 'openai', defaultModel: 'gpt-5.4',
77
+ stages: {
78
+ suggestions: { provider: 'openai', model: 'gpt-5.4' },
79
+ documentation: { provider: 'openai', model: 'gpt-5.4' },
80
+ 'architecture-recommendation': { provider: 'openai', model: 'gpt-5.4' },
81
+ 'question-prefilling': { provider: 'openai', model: 'gpt-5-mini' },
82
+ },
83
+ validation: {
84
+ provider: 'openai', model: 'gpt-5.4',
85
+ documentation: { provider: 'openai', model: 'gpt-5.4' },
86
+ refinement: { provider: 'openai', model: 'gpt-5.4' },
87
+ },
88
+ },
89
+ xiaomi: {
90
+ provider: 'xiaomi', defaultModel: 'mimo-v2-flash',
91
+ stages: {
92
+ suggestions: { provider: 'xiaomi', model: 'mimo-v2-flash' },
93
+ documentation: { provider: 'xiaomi', model: 'mimo-v2-pro' },
94
+ 'architecture-recommendation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
95
+ 'question-prefilling': { provider: 'xiaomi', model: 'mimo-v2-flash' },
96
+ },
97
+ validation: {
98
+ provider: 'xiaomi', model: 'mimo-v2-flash',
99
+ documentation: { provider: 'xiaomi', model: 'mimo-v2-flash' },
100
+ refinement: { provider: 'xiaomi', model: 'mimo-v2-pro' },
101
+ },
102
+ },
103
+ },
104
+ 'sprint-planning': {
105
+ claude: {
106
+ provider: 'claude', defaultModel: 'claude-sonnet-4-6',
107
+ stages: {
108
+ decomposition: { provider: 'claude', model: 'claude-opus-4-6' },
109
+ validation: { provider: 'claude', model: 'claude-sonnet-4-6', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, batchSize: 8, maxFixAttempts: 3 },
110
+ 'context-generation': { provider: 'claude', model: 'claude-sonnet-4-6' },
111
+ 'doc-generation': { provider: 'claude', model: 'claude-sonnet-4-6' },
112
+ enrichment: { provider: 'claude', model: 'claude-sonnet-4-6' },
113
+ },
114
+ },
115
+ gemini: {
116
+ provider: 'gemini', defaultModel: 'gemini-2.5-flash',
117
+ stages: {
118
+ decomposition: { provider: 'gemini', model: 'gemini-2.5-pro' },
119
+ validation: { provider: 'gemini', model: 'gemini-2.5-flash', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, batchSize: 8, maxFixAttempts: 3 },
120
+ 'context-generation': { provider: 'gemini', model: 'gemini-2.5-flash' },
121
+ 'doc-generation': { provider: 'gemini', model: 'gemini-2.5-flash' },
122
+ enrichment: { provider: 'gemini', model: 'gemini-2.5-flash' },
123
+ },
124
+ },
125
+ openai: {
126
+ provider: 'openai', defaultModel: 'gpt-5.4',
127
+ stages: {
128
+ decomposition: { provider: 'openai', model: 'gpt-5.4' },
129
+ validation: { provider: 'openai', model: 'gpt-5.4', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, batchSize: 8, maxFixAttempts: 3 },
130
+ 'context-generation': { provider: 'openai', model: 'gpt-5.4' },
131
+ 'doc-generation': { provider: 'openai', model: 'gpt-5.4' },
132
+ enrichment: { provider: 'openai', model: 'gpt-5.4' },
133
+ },
134
+ },
135
+ xiaomi: {
136
+ provider: 'xiaomi', defaultModel: 'mimo-v2-pro',
137
+ stages: {
138
+ decomposition: { provider: 'xiaomi', model: 'mimo-v2-pro' },
139
+ validation: { provider: 'xiaomi', model: 'mimo-v2-flash', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, batchSize: 8, maxFixAttempts: 3 },
140
+ 'context-generation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
141
+ 'doc-generation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
142
+ enrichment: { provider: 'xiaomi', model: 'mimo-v2-pro' },
143
+ },
144
+ },
145
+ },
146
+ 'seed': {
147
+ claude: {
148
+ provider: 'claude', defaultModel: 'claude-sonnet-4-6',
149
+ stages: {
150
+ decomposition: { provider: 'claude', model: 'claude-opus-4-6' },
151
+ 'doc-distribution': { provider: 'claude', model: 'claude-sonnet-4-6' },
152
+ },
153
+ },
154
+ gemini: {
155
+ provider: 'gemini', defaultModel: 'gemini-2.5-flash',
156
+ stages: {
157
+ decomposition: { provider: 'gemini', model: 'gemini-2.5-pro' },
158
+ 'doc-distribution': { provider: 'gemini', model: 'gemini-2.5-flash' },
159
+ },
160
+ },
161
+ openai: {
162
+ provider: 'openai', defaultModel: 'gpt-5.4',
163
+ stages: {
164
+ decomposition: { provider: 'openai', model: 'gpt-5.4' },
165
+ 'doc-distribution': { provider: 'openai', model: 'gpt-5.4' },
166
+ },
167
+ },
168
+ xiaomi: {
169
+ provider: 'xiaomi', defaultModel: 'mimo-v2-pro',
170
+ stages: {
171
+ decomposition: { provider: 'xiaomi', model: 'mimo-v2-pro' },
172
+ 'doc-distribution': { provider: 'xiaomi', model: 'mimo-v2-pro' },
173
+ 'context-generation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
174
+ },
175
+ },
176
+ },
177
+ };
178
+
179
+ async function readOAuthStatus(projectRoot, env = {}) {
180
+ try {
181
+ const raw = await fs.readFile(path.join(projectRoot, '.avc', 'openai-oauth.json'), 'utf8');
182
+ const { accountId, expires } = JSON.parse(raw);
183
+ return { connected: true, accountId, expiresAt: expires,
184
+ expiresIn: Math.max(0, Math.round((expires - Date.now()) / 1000)),
185
+ fallback: env.OPENAI_OAUTH_FALLBACK === 'true' };
186
+ } catch { return { connected: false, fallback: false }; }
187
+ }
188
+
42
189
  /**
43
190
  * Settings Router
44
191
  * Handles GET /api/settings and PUT sub-routes for project configuration.
@@ -95,14 +242,176 @@ export function createSettingsRouter(projectRoot) {
95
242
  await fs.writeFile(envPath, lines.join('\n'), 'utf8');
96
243
  };
97
244
 
245
+ // GET /api/settings/local-models — discover running local inference servers
246
+ router.get('/local-models', async (req, res) => {
247
+ try {
248
+ const { discoverLocalServers } = await import('../../../cli/llm-local.js');
249
+ const servers = await discoverLocalServers();
250
+ res.json({ servers });
251
+ } catch (err) {
252
+ res.status(500).json({ error: err.message, servers: [] });
253
+ }
254
+ });
255
+
98
256
  // GET /api/settings — snapshot of all configurable settings
99
257
  router.get('/', async (req, res) => {
100
258
  try {
101
259
  const [config, env] = await Promise.all([readAvcConfig(), readEnv()]);
260
+ const oauthStatus = await readOAuthStatus(projectRoot, env);
261
+ // Migrate existing projects: inject providerPresets if missing or incomplete
262
+ let configChanged = false;
263
+ const ceremonies = config?.settings?.ceremonies || [];
264
+ ceremonies.forEach((ceremony) => {
265
+ const defaultPresets = PROVIDER_PRESETS[ceremony.name];
266
+ if (!ceremony.providerPresets && defaultPresets) {
267
+ ceremony.providerPresets = defaultPresets;
268
+ configChanged = true;
269
+ } else if (ceremony.providerPresets && defaultPresets) {
270
+ for (const [provider, preset] of Object.entries(defaultPresets)) {
271
+ if (!ceremony.providerPresets[provider]) {
272
+ ceremony.providerPresets[provider] = preset;
273
+ configChanged = true;
274
+ }
275
+ }
276
+ }
277
+ // Migrate sprint-planning: doc-distribution → context-generation + doc-generation
278
+ if (ceremony.name === 'sprint-planning' && ceremony.stages) {
279
+ const old = ceremony.stages['doc-distribution'];
280
+ if (old) {
281
+ if (!ceremony.stages['context-generation']) ceremony.stages['context-generation'] = { ...old };
282
+ if (!ceremony.stages['doc-generation']) ceremony.stages['doc-generation'] = { ...old };
283
+ }
284
+ }
285
+ });
286
+
287
+ // Inject missing models for new providers (e.g., xiaomi added after project init)
288
+ const models = config?.settings?.models || {};
289
+ const DEFAULT_MODELS = {
290
+ 'mimo-v2-flash': { provider: 'xiaomi', displayName: 'MiMo V2 Flash', pricing: { input: 0.09, output: 0.29, unit: 'million', source: 'https://platform.xiaomimimo.com', lastUpdated: '2026-03-25' } },
291
+ 'mimo-v2-pro': { provider: 'xiaomi', displayName: 'MiMo V2 Pro', pricing: { input: 1.00, output: 3.00, unit: 'million', source: 'https://platform.xiaomimimo.com', lastUpdated: '2026-03-25' } },
292
+ 'mimo-v2-omni': { provider: 'xiaomi', displayName: 'MiMo V2 Omni', pricing: { input: 0.40, output: 2.00, unit: 'million', source: 'https://platform.xiaomimimo.com', lastUpdated: '2026-03-25' } },
293
+ };
294
+ for (const [modelId, modelDef] of Object.entries(DEFAULT_MODELS)) {
295
+ if (!models[modelId]) {
296
+ models[modelId] = modelDef;
297
+ configChanged = true;
298
+ } else if (!models[modelId].pricing?.source || !models[modelId].pricing?.lastUpdated) {
299
+ // Backfill missing pricing metadata for existing models
300
+ models[modelId].pricing = { ...models[modelId].pricing, ...modelDef.pricing };
301
+ configChanged = true;
302
+ }
303
+ }
304
+ if (config.settings) config.settings.models = models;
305
+
306
+ // Auto-switch ceremony providers when configured provider has no API key
307
+ // but another provider with a valid key is available.
308
+ const keyAvailable = (provider) => {
309
+ if (provider === 'local') return true;
310
+ if (provider === 'claude' || provider === 'anthropic') return !!env.ANTHROPIC_API_KEY;
311
+ if (provider === 'openai') return !!(env.OPENAI_API_KEY || (env.OPENAI_AUTH_MODE === 'oauth'));
312
+ if (provider === 'gemini') return !!env.GEMINI_API_KEY;
313
+ if (provider === 'xiaomi') return !!env.XIAOMI_API_KEY;
314
+ return false;
315
+ };
316
+ const FALLBACK_ORDER = ['claude', 'gemini', 'openai', 'xiaomi'];
317
+ const FALLBACK_MODELS = { claude: 'claude-sonnet-4-6', gemini: 'gemini-2.5-flash', openai: 'gpt-4.1', xiaomi: 'MiMo-V2-Flash' };
318
+ const findAvailable = () => FALLBACK_ORDER.find(p => keyAvailable(p)) || null;
319
+
320
+ for (const ceremony of ceremonies) {
321
+ const mainProvider = ceremony.provider || 'claude';
322
+ if (!keyAvailable(mainProvider)) {
323
+ const fallback = findAvailable();
324
+ if (fallback) {
325
+ console.log(`[settings] auto-switch: ${mainProvider}→${fallback} for ceremony ${ceremony.name}`);
326
+ const preset = ceremony.providerPresets?.[fallback];
327
+ const fallbackModel = preset?.defaultModel || FALLBACK_MODELS[fallback];
328
+
329
+ ceremony.provider = fallback;
330
+ ceremony.defaultModel = fallbackModel;
331
+
332
+ // Apply preset stage config (model + extra props like concurrency)
333
+ if (ceremony.stages && typeof ceremony.stages === 'object') {
334
+ for (const [stageName, stage] of Object.entries(ceremony.stages)) {
335
+ const presetStage = preset?.stages?.[stageName];
336
+ if (presetStage) {
337
+ Object.assign(stage, presetStage);
338
+ } else {
339
+ stage.provider = fallback;
340
+ stage.model = fallbackModel;
341
+ }
342
+ }
343
+ }
344
+
345
+ // Apply preset validation models or fall back to default model
346
+ if (ceremony.validation && typeof ceremony.validation === 'object') {
347
+ const presetVal = preset?.validation;
348
+ ceremony.validation.provider = fallback;
349
+ ceremony.validation.model = presetVal?.model || fallbackModel;
350
+ for (const [k, v] of Object.entries(ceremony.validation)) {
351
+ if (v && typeof v === 'object' && typeof v.provider === 'string') {
352
+ const presetSub = presetVal?.[k];
353
+ v.provider = fallback;
354
+ v.model = presetSub?.model || presetVal?.model || fallbackModel;
355
+ }
356
+ }
357
+ }
358
+ configChanged = true;
359
+ }
360
+ }
361
+ }
362
+
363
+ // Re-apply provider presets to repair stale stage models
364
+ // (handles case where a previous auto-switch set all stages to a single default model)
365
+ for (const ceremony of ceremonies) {
366
+ const provider = ceremony.provider || 'claude';
367
+ const preset = ceremony.providerPresets?.[provider];
368
+ if (!preset) continue;
369
+
370
+ let repaired = false;
371
+ if (preset.defaultModel && ceremony.defaultModel !== preset.defaultModel) {
372
+ ceremony.defaultModel = preset.defaultModel;
373
+ repaired = true;
374
+ }
375
+ if (ceremony.stages && preset.stages) {
376
+ for (const [stageName, stage] of Object.entries(ceremony.stages)) {
377
+ const ps = preset.stages[stageName];
378
+ if (ps && stage.model !== ps.model) {
379
+ Object.assign(stage, ps);
380
+ repaired = true;
381
+ }
382
+ }
383
+ }
384
+ if (ceremony.validation && typeof ceremony.validation === 'object' && preset.validation) {
385
+ if (preset.validation.model && ceremony.validation.model !== preset.validation.model) {
386
+ ceremony.validation.provider = provider;
387
+ ceremony.validation.model = preset.validation.model;
388
+ repaired = true;
389
+ }
390
+ for (const [k, v] of Object.entries(ceremony.validation)) {
391
+ if (v && typeof v === 'object' && typeof v.provider === 'string' && preset.validation[k]) {
392
+ if (v.model !== preset.validation[k].model) {
393
+ v.provider = provider;
394
+ v.model = preset.validation[k].model;
395
+ repaired = true;
396
+ }
397
+ }
398
+ }
399
+ }
400
+ if (repaired) {
401
+ console.log(`[settings] repaired stage models for ${ceremony.name} using ${provider} preset`);
402
+ configChanged = true;
403
+ }
404
+ }
405
+
406
+ // Persist all migrations to avc.json so other endpoints see them
407
+ if (configChanged) {
408
+ try { await writeAvcConfig(config); } catch {}
409
+ }
410
+
102
411
  res.json({
103
412
  apiKeys: {
104
413
  anthropic: {
105
- isSet: !!env.ANTHROPIC_API_KEY,
414
+ isSet: !!env.ANTHROPIC_API_KEY,
106
415
  preview: env.ANTHROPIC_API_KEY ? env.ANTHROPIC_API_KEY.slice(0, 10) + '…' : '',
107
416
  },
108
417
  gemini: {
@@ -110,11 +419,21 @@ export function createSettingsRouter(projectRoot) {
110
419
  preview: env.GEMINI_API_KEY ? env.GEMINI_API_KEY.slice(0, 10) + '…' : '',
111
420
  },
112
421
  openai: {
113
- isSet: !!env.OPENAI_API_KEY,
114
- preview: env.OPENAI_API_KEY ? env.OPENAI_API_KEY.slice(0, 10) + '…' : '',
422
+ isSet: !!(env.OPENAI_API_KEY || oauthStatus.connected),
423
+ preview: env.OPENAI_API_KEY ? env.OPENAI_API_KEY.slice(0, 10) + '…' : '',
424
+ authMode: env.OPENAI_AUTH_MODE || 'api-key',
425
+ oauth: oauthStatus,
426
+ },
427
+ xiaomi: {
428
+ isSet: !!env.XIAOMI_API_KEY,
429
+ preview: env.XIAOMI_API_KEY ? env.XIAOMI_API_KEY.slice(0, 10) + '…' : '',
430
+ },
431
+ local: {
432
+ isSet: true, // No API key needed — availability checked via /local-models
433
+ preview: 'localhost',
115
434
  },
116
435
  },
117
- ceremonies: config?.settings?.ceremonies || [],
436
+ ceremonies,
118
437
  models: (config?.settings?.models && Object.keys(config.settings.models).length > 0)
119
438
  ? config.settings.models
120
439
  : DEFAULT_MODELS,
@@ -132,10 +451,27 @@ export function createSettingsRouter(projectRoot) {
132
451
  // PUT /api/settings/api-keys — only sends keys that are being updated
133
452
  router.put('/api-keys', async (req, res) => {
134
453
  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);
454
+ const { anthropic, gemini, openai, xiaomi } = req.body;
455
+ if (anthropic !== undefined) {
456
+ await upsertEnvKey('ANTHROPIC_API_KEY', anthropic);
457
+ if (anthropic) process.env.ANTHROPIC_API_KEY = anthropic;
458
+ else delete process.env.ANTHROPIC_API_KEY;
459
+ }
460
+ if (gemini !== undefined) {
461
+ await upsertEnvKey('GEMINI_API_KEY', gemini);
462
+ if (gemini) process.env.GEMINI_API_KEY = gemini;
463
+ else delete process.env.GEMINI_API_KEY;
464
+ }
465
+ if (openai !== undefined) {
466
+ await upsertEnvKey('OPENAI_API_KEY', openai);
467
+ if (openai) process.env.OPENAI_API_KEY = openai;
468
+ else delete process.env.OPENAI_API_KEY;
469
+ }
470
+ if (xiaomi !== undefined) {
471
+ await upsertEnvKey('XIAOMI_API_KEY', xiaomi);
472
+ if (xiaomi) process.env.XIAOMI_API_KEY = xiaomi;
473
+ else delete process.env.XIAOMI_API_KEY;
474
+ }
139
475
  res.json({ status: 'ok' });
140
476
  } catch (err) {
141
477
  res.status(500).json({ error: err.message });
@@ -299,5 +635,102 @@ export function createSettingsRouter(projectRoot) {
299
635
  }
300
636
  });
301
637
 
638
+ // ── Micro-Check Definitions API ────────────────────────────────────────────
639
+
640
+ // GET /api/settings/checks — list all check files with customization status
641
+ router.get('/checks', (req, res) => {
642
+ try {
643
+ const checks = [];
644
+ for (const scope of ['epic', 'story', 'code']) {
645
+ const scopeDir = path.join(LIB_CHECKS_PATH, scope);
646
+ if (!existsSync(scopeDir)) continue;
647
+ const files = readdirSync(scopeDir).filter(f => f.endsWith('.json')).sort();
648
+ for (const file of files) {
649
+ const perspective = file.replace(/\.json$/, '');
650
+ const customPath = path.join(customChecksDir(projectRoot), scope, file);
651
+ const content = JSON.parse(readFileSync(path.join(scopeDir, file), 'utf8'));
652
+ checks.push({
653
+ scope,
654
+ perspective,
655
+ checkCount: Array.isArray(content) ? content.length : 0,
656
+ isCustomized: existsSync(customPath),
657
+ });
658
+ }
659
+ }
660
+ // Cross-refs
661
+ const crossDir = path.join(LIB_CHECKS_PATH, 'cross-refs');
662
+ if (existsSync(crossDir)) {
663
+ for (const file of readdirSync(crossDir).filter(f => f.endsWith('.json')).sort()) {
664
+ const scope = file.replace(/\.json$/, '');
665
+ const customPath = path.join(customChecksDir(projectRoot), 'cross-refs', file);
666
+ const content = JSON.parse(readFileSync(path.join(crossDir, file), 'utf8'));
667
+ checks.push({
668
+ scope: 'cross-refs',
669
+ perspective: scope,
670
+ checkCount: Array.isArray(content) ? content.length : 0,
671
+ isCustomized: existsSync(customPath),
672
+ });
673
+ }
674
+ }
675
+ res.json({ checks });
676
+ } catch (err) {
677
+ res.status(500).json({ error: err.message });
678
+ }
679
+ });
680
+
681
+ // GET /api/settings/checks/:scope/:perspective — get check content
682
+ router.get('/checks/:scope/:perspective', (req, res) => {
683
+ const { scope, perspective } = req.params;
684
+ if (!['epic', 'story', 'code', 'cross-refs'].includes(scope) || /[/\\]/.test(perspective)) {
685
+ return res.status(400).json({ error: 'Invalid scope or perspective' });
686
+ }
687
+ try {
688
+ const libPath = path.join(LIB_CHECKS_PATH, scope, `${perspective}.json`);
689
+ if (!existsSync(libPath)) return res.status(404).json({ error: 'Check file not found' });
690
+ const customPath = path.join(customChecksDir(projectRoot), scope, `${perspective}.json`);
691
+ const isCustomized = existsSync(customPath);
692
+ const content = readFileSync(isCustomized ? customPath : libPath, 'utf8');
693
+ const defaultContent = readFileSync(libPath, 'utf8');
694
+ res.json({ scope, perspective, content, isCustomized, defaultContent });
695
+ } catch (err) {
696
+ res.status(500).json({ error: err.message });
697
+ }
698
+ });
699
+
700
+ // PUT /api/settings/checks/:scope/:perspective — save customized check file
701
+ router.put('/checks/:scope/:perspective', async (req, res) => {
702
+ const { scope, perspective } = req.params;
703
+ const { content } = req.body;
704
+ if (!['epic', 'story', 'code', 'cross-refs'].includes(scope) || /[/\\]/.test(perspective)) {
705
+ return res.status(400).json({ error: 'Invalid scope or perspective' });
706
+ }
707
+ if (typeof content !== 'string') return res.status(400).json({ error: 'content must be a string' });
708
+ // Validate JSON
709
+ try { JSON.parse(content); } catch { return res.status(400).json({ error: 'Invalid JSON' }); }
710
+ try {
711
+ const dir = path.join(customChecksDir(projectRoot), scope);
712
+ await fs.mkdir(dir, { recursive: true });
713
+ await fs.writeFile(path.join(dir, `${perspective}.json`), content, 'utf8');
714
+ res.json({ status: 'ok' });
715
+ } catch (err) {
716
+ res.status(500).json({ error: err.message });
717
+ }
718
+ });
719
+
720
+ // DELETE /api/settings/checks/:scope/:perspective — reset to built-in default
721
+ router.delete('/checks/:scope/:perspective', async (req, res) => {
722
+ const { scope, perspective } = req.params;
723
+ if (!['epic', 'story', 'code', 'cross-refs'].includes(scope) || /[/\\]/.test(perspective)) {
724
+ return res.status(400).json({ error: 'Invalid scope or perspective' });
725
+ }
726
+ try {
727
+ const customPath = path.join(customChecksDir(projectRoot), scope, `${perspective}.json`);
728
+ try { await fs.unlink(customPath); } catch {}
729
+ res.json({ status: 'ok' });
730
+ } catch (err) {
731
+ res.status(500).json({ error: err.message });
732
+ }
733
+ });
734
+
302
735
  return router;
303
736
  }
@@ -163,6 +163,10 @@ export function setupWebSocket(server, dataStore, processRegistry = null, ceremo
163
163
  broadcast({ type: 'ceremony:cost-limit', cost, threshold, runningType });
164
164
  }
165
165
 
166
+ function broadcastQuotaLimit(provider, model, errMsg, validatorName, runningType) {
167
+ broadcast({ type: 'ceremony:quota-limit', provider, model, errMsg, validatorName, runningType });
168
+ }
169
+
166
170
  function broadcastSprintPlanningDecompositionComplete(hierarchy) {
167
171
  broadcast({ type: 'sprint-planning:decomposition-complete', hierarchy });
168
172
  }
@@ -175,8 +179,8 @@ export function setupWebSocket(server, dataStore, processRegistry = null, ceremo
175
179
  broadcast({ type: 'sprint-planning:resumed' });
176
180
  }
177
181
 
178
- function broadcastSprintPlanningCancelled() {
179
- broadcast({ type: 'sprint-planning:cancelled' });
182
+ function broadcastSprintPlanningCancelled(itemsKept = false) {
183
+ broadcast({ type: 'sprint-planning:cancelled', itemsKept });
180
184
  }
181
185
 
182
186
  function broadcastSprintPlanningDetail(detail) {
@@ -250,6 +254,7 @@ export function setupWebSocket(server, dataStore, processRegistry = null, ceremo
250
254
  broadcastMissionProgress,
251
255
  broadcastCostUpdate,
252
256
  broadcastCostLimit,
257
+ broadcastQuotaLimit,
253
258
  broadcastProcessStarted,
254
259
  broadcastProcessStatus,
255
260
  broadcastRefineProgress,