@agile-vibe-coding/avc 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/cli/agents/agent-selector.md +23 -0
  2. package/cli/agents/code-implementer.md +117 -0
  3. package/cli/agents/code-validator.md +80 -0
  4. package/cli/agents/context-reviewer-epic.md +101 -0
  5. package/cli/agents/context-reviewer-story.md +92 -0
  6. package/cli/agents/context-writer-epic.md +145 -0
  7. package/cli/agents/context-writer-story.md +111 -0
  8. package/cli/agents/doc-writer-epic.md +42 -0
  9. package/cli/agents/doc-writer-story.md +43 -0
  10. package/cli/agents/duplicate-detector.md +110 -0
  11. package/cli/agents/epic-story-decomposer.md +318 -39
  12. package/cli/agents/mission-scope-generator.md +68 -4
  13. package/cli/agents/mission-scope-validator.md +40 -6
  14. package/cli/agents/project-context-extractor.md +21 -6
  15. package/cli/agents/scaffolding-generator.md +99 -0
  16. package/cli/agents/seed-validator.md +71 -0
  17. package/cli/agents/story-scope-reviewer.md +147 -0
  18. package/cli/agents/story-splitter.md +83 -0
  19. package/cli/agents/validator-documentation.json +31 -0
  20. package/cli/agents/validator-documentation.md +3 -1
  21. package/cli/api-reference-tool.js +368 -0
  22. package/cli/checks/catalog.json +76 -0
  23. package/cli/checks/code/quality.json +26 -0
  24. package/cli/checks/code/testing.json +14 -0
  25. package/cli/checks/code/traceability.json +26 -0
  26. package/cli/checks/cross-refs/epic.json +171 -0
  27. package/cli/checks/cross-refs/story.json +149 -0
  28. package/cli/checks/epic/api.json +114 -0
  29. package/cli/checks/epic/backend.json +126 -0
  30. package/cli/checks/epic/cloud.json +126 -0
  31. package/cli/checks/epic/data.json +102 -0
  32. package/cli/checks/epic/database.json +114 -0
  33. package/cli/checks/epic/developer.json +182 -0
  34. package/cli/checks/epic/devops.json +174 -0
  35. package/cli/checks/epic/frontend.json +162 -0
  36. package/cli/checks/epic/mobile.json +102 -0
  37. package/cli/checks/epic/qa.json +90 -0
  38. package/cli/checks/epic/security.json +184 -0
  39. package/cli/checks/epic/solution-architect.json +192 -0
  40. package/cli/checks/epic/test-architect.json +90 -0
  41. package/cli/checks/epic/ui.json +102 -0
  42. package/cli/checks/epic/ux.json +90 -0
  43. package/cli/checks/fixes/epic-fix-template.md +10 -0
  44. package/cli/checks/fixes/story-fix-template.md +10 -0
  45. package/cli/checks/story/api.json +186 -0
  46. package/cli/checks/story/backend.json +102 -0
  47. package/cli/checks/story/cloud.json +102 -0
  48. package/cli/checks/story/data.json +210 -0
  49. package/cli/checks/story/database.json +102 -0
  50. package/cli/checks/story/developer.json +168 -0
  51. package/cli/checks/story/devops.json +102 -0
  52. package/cli/checks/story/frontend.json +174 -0
  53. package/cli/checks/story/mobile.json +102 -0
  54. package/cli/checks/story/qa.json +210 -0
  55. package/cli/checks/story/security.json +198 -0
  56. package/cli/checks/story/solution-architect.json +230 -0
  57. package/cli/checks/story/test-architect.json +210 -0
  58. package/cli/checks/story/ui.json +102 -0
  59. package/cli/checks/story/ux.json +102 -0
  60. package/cli/coding-order.js +401 -0
  61. package/cli/dependency-checker.js +72 -0
  62. package/cli/epic-story-validator.js +284 -799
  63. package/cli/index.js +0 -0
  64. package/cli/init-model-config.js +17 -10
  65. package/cli/init.js +514 -92
  66. package/cli/kanban-server-manager.js +1 -2
  67. package/cli/llm-claude.js +98 -31
  68. package/cli/llm-gemini.js +29 -5
  69. package/cli/llm-local.js +493 -0
  70. package/cli/llm-openai.js +262 -41
  71. package/cli/llm-provider.js +147 -8
  72. package/cli/llm-token-limits.js +113 -4
  73. package/cli/llm-verifier.js +209 -1
  74. package/cli/llm-xiaomi.js +143 -0
  75. package/cli/message-constants.js +3 -12
  76. package/cli/messaging-api.js +6 -12
  77. package/cli/micro-check-fixer.js +335 -0
  78. package/cli/micro-check-runner.js +449 -0
  79. package/cli/micro-check-scorer.js +148 -0
  80. package/cli/micro-check-validator.js +538 -0
  81. package/cli/model-pricing.js +23 -0
  82. package/cli/model-selector.js +3 -2
  83. package/cli/prompt-logger.js +57 -0
  84. package/cli/repl-ink.js +106 -346
  85. package/cli/repl-old.js +1 -2
  86. package/cli/seed-processor.js +194 -24
  87. package/cli/sprint-planning-processor.js +2638 -289
  88. package/cli/template-processor.js +50 -3
  89. package/cli/token-tracker.js +50 -23
  90. package/cli/tools/generate-story-validators.js +1 -1
  91. package/cli/validation-router.js +70 -8
  92. package/cli/worktree-runner.js +654 -0
  93. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  94. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  95. package/kanban/client/dist/index.html +2 -2
  96. package/kanban/client/src/App.jsx +43 -14
  97. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  98. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  99. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  100. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  101. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  102. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  103. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  104. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  105. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  106. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  107. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  108. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  109. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  110. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  111. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  112. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  113. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  114. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  115. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  116. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  117. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  118. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  119. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  120. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  121. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  122. package/kanban/client/src/hooks/useGrouping.js +59 -0
  123. package/kanban/client/src/lib/api.js +118 -4
  124. package/kanban/client/src/lib/status-grouping.js +10 -0
  125. package/kanban/client/src/store/kanbanStore.js +8 -0
  126. package/kanban/server/index.js +23 -2
  127. package/kanban/server/routes/ceremony.js +153 -4
  128. package/kanban/server/routes/costs.js +9 -3
  129. package/kanban/server/routes/openai-oauth.js +366 -0
  130. package/kanban/server/routes/settings.js +447 -14
  131. package/kanban/server/routes/websocket.js +7 -2
  132. package/kanban/server/routes/work-items.js +141 -1
  133. package/kanban/server/services/CeremonyService.js +275 -24
  134. package/kanban/server/services/TaskRunnerService.js +261 -0
  135. package/kanban/server/workers/run-task-worker.js +121 -0
  136. package/kanban/server/workers/seed-worker.js +94 -0
  137. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  138. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  139. package/package.json +2 -3
  140. package/cli/agents/solver-epic-api.json +0 -15
  141. package/cli/agents/solver-epic-api.md +0 -39
  142. package/cli/agents/solver-epic-backend.json +0 -15
  143. package/cli/agents/solver-epic-backend.md +0 -39
  144. package/cli/agents/solver-epic-cloud.json +0 -15
  145. package/cli/agents/solver-epic-cloud.md +0 -39
  146. package/cli/agents/solver-epic-data.json +0 -15
  147. package/cli/agents/solver-epic-data.md +0 -39
  148. package/cli/agents/solver-epic-database.json +0 -15
  149. package/cli/agents/solver-epic-database.md +0 -39
  150. package/cli/agents/solver-epic-developer.json +0 -15
  151. package/cli/agents/solver-epic-developer.md +0 -39
  152. package/cli/agents/solver-epic-devops.json +0 -15
  153. package/cli/agents/solver-epic-devops.md +0 -39
  154. package/cli/agents/solver-epic-frontend.json +0 -15
  155. package/cli/agents/solver-epic-frontend.md +0 -39
  156. package/cli/agents/solver-epic-mobile.json +0 -15
  157. package/cli/agents/solver-epic-mobile.md +0 -39
  158. package/cli/agents/solver-epic-qa.json +0 -15
  159. package/cli/agents/solver-epic-qa.md +0 -39
  160. package/cli/agents/solver-epic-security.json +0 -15
  161. package/cli/agents/solver-epic-security.md +0 -39
  162. package/cli/agents/solver-epic-solution-architect.json +0 -15
  163. package/cli/agents/solver-epic-solution-architect.md +0 -39
  164. package/cli/agents/solver-epic-test-architect.json +0 -15
  165. package/cli/agents/solver-epic-test-architect.md +0 -39
  166. package/cli/agents/solver-epic-ui.json +0 -15
  167. package/cli/agents/solver-epic-ui.md +0 -39
  168. package/cli/agents/solver-epic-ux.json +0 -15
  169. package/cli/agents/solver-epic-ux.md +0 -39
  170. package/cli/agents/solver-story-api.json +0 -15
  171. package/cli/agents/solver-story-api.md +0 -39
  172. package/cli/agents/solver-story-backend.json +0 -15
  173. package/cli/agents/solver-story-backend.md +0 -39
  174. package/cli/agents/solver-story-cloud.json +0 -15
  175. package/cli/agents/solver-story-cloud.md +0 -39
  176. package/cli/agents/solver-story-data.json +0 -15
  177. package/cli/agents/solver-story-data.md +0 -39
  178. package/cli/agents/solver-story-database.json +0 -15
  179. package/cli/agents/solver-story-database.md +0 -39
  180. package/cli/agents/solver-story-developer.json +0 -15
  181. package/cli/agents/solver-story-developer.md +0 -39
  182. package/cli/agents/solver-story-devops.json +0 -15
  183. package/cli/agents/solver-story-devops.md +0 -39
  184. package/cli/agents/solver-story-frontend.json +0 -15
  185. package/cli/agents/solver-story-frontend.md +0 -39
  186. package/cli/agents/solver-story-mobile.json +0 -15
  187. package/cli/agents/solver-story-mobile.md +0 -39
  188. package/cli/agents/solver-story-qa.json +0 -15
  189. package/cli/agents/solver-story-qa.md +0 -39
  190. package/cli/agents/solver-story-security.json +0 -15
  191. package/cli/agents/solver-story-security.md +0 -39
  192. package/cli/agents/solver-story-solution-architect.json +0 -15
  193. package/cli/agents/solver-story-solution-architect.md +0 -39
  194. package/cli/agents/solver-story-test-architect.json +0 -15
  195. package/cli/agents/solver-story-test-architect.md +0 -39
  196. package/cli/agents/solver-story-ui.json +0 -15
  197. package/cli/agents/solver-story-ui.md +0 -39
  198. package/cli/agents/solver-story-ux.json +0 -15
  199. package/cli/agents/solver-story-ux.md +0 -39
  200. package/cli/agents/validator-epic-api.json +0 -93
  201. package/cli/agents/validator-epic-api.md +0 -137
  202. package/cli/agents/validator-epic-backend.json +0 -93
  203. package/cli/agents/validator-epic-backend.md +0 -130
  204. package/cli/agents/validator-epic-cloud.json +0 -93
  205. package/cli/agents/validator-epic-cloud.md +0 -137
  206. package/cli/agents/validator-epic-data.json +0 -93
  207. package/cli/agents/validator-epic-data.md +0 -130
  208. package/cli/agents/validator-epic-database.json +0 -93
  209. package/cli/agents/validator-epic-database.md +0 -137
  210. package/cli/agents/validator-epic-developer.json +0 -74
  211. package/cli/agents/validator-epic-developer.md +0 -153
  212. package/cli/agents/validator-epic-devops.json +0 -74
  213. package/cli/agents/validator-epic-devops.md +0 -153
  214. package/cli/agents/validator-epic-frontend.json +0 -74
  215. package/cli/agents/validator-epic-frontend.md +0 -153
  216. package/cli/agents/validator-epic-mobile.json +0 -93
  217. package/cli/agents/validator-epic-mobile.md +0 -130
  218. package/cli/agents/validator-epic-qa.json +0 -93
  219. package/cli/agents/validator-epic-qa.md +0 -130
  220. package/cli/agents/validator-epic-security.json +0 -74
  221. package/cli/agents/validator-epic-security.md +0 -154
  222. package/cli/agents/validator-epic-solution-architect.json +0 -74
  223. package/cli/agents/validator-epic-solution-architect.md +0 -156
  224. package/cli/agents/validator-epic-test-architect.json +0 -93
  225. package/cli/agents/validator-epic-test-architect.md +0 -130
  226. package/cli/agents/validator-epic-ui.json +0 -93
  227. package/cli/agents/validator-epic-ui.md +0 -130
  228. package/cli/agents/validator-epic-ux.json +0 -93
  229. package/cli/agents/validator-epic-ux.md +0 -130
  230. package/cli/agents/validator-story-api.json +0 -104
  231. package/cli/agents/validator-story-api.md +0 -152
  232. package/cli/agents/validator-story-backend.json +0 -104
  233. package/cli/agents/validator-story-backend.md +0 -152
  234. package/cli/agents/validator-story-cloud.json +0 -104
  235. package/cli/agents/validator-story-cloud.md +0 -152
  236. package/cli/agents/validator-story-data.json +0 -104
  237. package/cli/agents/validator-story-data.md +0 -152
  238. package/cli/agents/validator-story-database.json +0 -104
  239. package/cli/agents/validator-story-database.md +0 -152
  240. package/cli/agents/validator-story-developer.json +0 -104
  241. package/cli/agents/validator-story-developer.md +0 -152
  242. package/cli/agents/validator-story-devops.json +0 -104
  243. package/cli/agents/validator-story-devops.md +0 -152
  244. package/cli/agents/validator-story-frontend.json +0 -104
  245. package/cli/agents/validator-story-frontend.md +0 -152
  246. package/cli/agents/validator-story-mobile.json +0 -104
  247. package/cli/agents/validator-story-mobile.md +0 -152
  248. package/cli/agents/validator-story-qa.json +0 -104
  249. package/cli/agents/validator-story-qa.md +0 -152
  250. package/cli/agents/validator-story-security.json +0 -104
  251. package/cli/agents/validator-story-security.md +0 -152
  252. package/cli/agents/validator-story-solution-architect.json +0 -104
  253. package/cli/agents/validator-story-solution-architect.md +0 -152
  254. package/cli/agents/validator-story-test-architect.json +0 -104
  255. package/cli/agents/validator-story-test-architect.md +0 -152
  256. package/cli/agents/validator-story-ui.json +0 -104
  257. package/cli/agents/validator-story-ui.md +0 -152
  258. package/cli/agents/validator-story-ux.json +0 -104
  259. package/cli/agents/validator-story-ux.md +0 -152
  260. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  261. 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,