@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
@@ -244,8 +244,15 @@ export async function runCeremony(requirements) {
244
244
  });
245
245
  }
246
246
 
247
- export async function runSprintPlanning() {
248
- return apiFetch('/ceremony/sprint-planning/run', { method: 'POST' });
247
+ export async function runSprintPlanning(resumeFrom = null) {
248
+ return apiFetch('/ceremony/sprint-planning/run', {
249
+ method: 'POST',
250
+ ...(resumeFrom ? { body: JSON.stringify({ resumeFrom }) } : {}),
251
+ });
252
+ }
253
+
254
+ export async function getSprintPlanningResumable() {
255
+ return apiFetch('/ceremony/sprint-planning/resumable');
249
256
  }
250
257
 
251
258
  export const confirmSprintPlanningSelection = (selectedEpicIds, selectedStoryIds) =>
@@ -256,12 +263,31 @@ export const confirmSprintPlanningSelection = (selectedEpicIds, selectedStoryIds
256
263
 
257
264
  export const pauseCeremony = () => apiFetch('/ceremony/pause', { method: 'POST' });
258
265
  export const resumeCeremony = () => apiFetch('/ceremony/resume', { method: 'POST' });
259
- export const cancelCeremony = () => apiFetch('/ceremony/cancel', { method: 'POST' });
266
+ export const cancelCeremony = (keepItems = false) => apiFetch('/ceremony/cancel', { method: 'POST', body: JSON.stringify({ keepItems }) });
260
267
  export const resetCeremony = () => apiFetch('/ceremony/reset', { method: 'POST' });
261
268
  export const continuePastCostLimit = () => apiFetch('/ceremony/cost-limit-continue', { method: 'POST' });
269
+ export const continueAfterQuota = (newProvider = null, newModel = null) =>
270
+ apiFetch('/ceremony/quota-continue', { method: 'POST', body: JSON.stringify({ newProvider, newModel }) });
262
271
 
263
272
  export async function getModels() {
264
- return apiFetch('/ceremony/models');
273
+ const [models, local] = await Promise.all([
274
+ apiFetch('/ceremony/models'),
275
+ apiFetch('/settings/local-models').catch(() => ({ servers: [] })),
276
+ ]);
277
+ // Merge discovered local models into the list
278
+ const localModels = (local.servers || []).flatMap((srv) =>
279
+ srv.models.map((lm) => ({
280
+ modelId: lm.id,
281
+ displayName: `${lm.id} (${srv.label})`,
282
+ provider: 'local',
283
+ hasApiKey: true,
284
+ }))
285
+ );
286
+ return [...models, ...localModels];
287
+ }
288
+
289
+ export async function getLocalModels() {
290
+ return apiFetch('/settings/local-models');
265
291
  }
266
292
 
267
293
  export async function generateMission(description, modelId, provider, validatorModelId, validatorProvider) {
@@ -292,6 +318,13 @@ export async function refineArchitecture(currentArch, refinementRequest, modelId
292
318
  });
293
319
  }
294
320
 
321
+ export async function refineField(fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider) {
322
+ return apiFetch('/ceremony/refine-field', {
323
+ method: 'POST',
324
+ body: JSON.stringify({ fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider }),
325
+ });
326
+ }
327
+
295
328
  // ── Sponsor-call draft (resume support) ──────────────────────────────────────
296
329
 
297
330
  export async function getSponsorCallDraft() {
@@ -336,6 +369,27 @@ export async function resetAgent(name) {
336
369
  return apiFetch(`/settings/agents/${encodeURIComponent(name)}`, { method: 'DELETE' });
337
370
  }
338
371
 
372
+ // ── Micro-Check Definitions API ──────────────────────────────────────────────
373
+
374
+ export async function getCheckList() {
375
+ return apiFetch('/settings/checks');
376
+ }
377
+
378
+ export async function getCheckContent(scope, perspective) {
379
+ return apiFetch(`/settings/checks/${encodeURIComponent(scope)}/${encodeURIComponent(perspective)}`);
380
+ }
381
+
382
+ export async function saveCheckContent(scope, perspective, content) {
383
+ return apiFetch(`/settings/checks/${encodeURIComponent(scope)}/${encodeURIComponent(perspective)}`, {
384
+ method: 'PUT',
385
+ body: JSON.stringify({ content }),
386
+ });
387
+ }
388
+
389
+ export async function resetCheck(scope, perspective) {
390
+ return apiFetch(`/settings/checks/${encodeURIComponent(scope)}/${encodeURIComponent(perspective)}`, { method: 'DELETE' });
391
+ }
392
+
339
393
  // ── Processes API ─────────────────────────────────────────────────────────────
340
394
 
341
395
  export async function getProcesses() {
@@ -378,6 +432,28 @@ export async function applyWorkItemChanges(id, proposedItem, storyChanges = [])
378
432
  });
379
433
  }
380
434
 
435
+ // ── OpenAI OAuth API ──────────────────────────────────────────────────────────
436
+
437
+ export async function connectOpenAIOAuth() {
438
+ return apiFetch('/settings/openai-oauth/login', { method: 'POST' });
439
+ }
440
+
441
+ export async function disconnectOpenAIOAuth() {
442
+ return apiFetch('/settings/openai-oauth/logout', { method: 'POST' });
443
+ }
444
+
445
+ export async function getOpenAIOAuthStatus() {
446
+ return apiFetch('/settings/openai-oauth/status');
447
+ }
448
+
449
+ export async function testOpenAIOAuth() {
450
+ return apiFetch('/settings/openai-oauth/test', { method: 'POST' });
451
+ }
452
+
453
+ export async function setOpenAIOAuthFallback(enabled) {
454
+ return apiFetch('/settings/openai-oauth/fallback', { method: 'POST', body: JSON.stringify({ enabled }) });
455
+ }
456
+
381
457
  // ── Costs API ─────────────────────────────────────────────────────────────────
382
458
 
383
459
  /**
@@ -399,3 +475,41 @@ export async function getCostHistory(range = 30) {
399
475
  }
400
476
  return apiFetch(`/costs/history?from=${range.from}&to=${range.to}`);
401
477
  }
478
+
479
+ // ---- Task Runner: Seed + Run ----
480
+
481
+ export async function updateWorkItemStatus(id, status) {
482
+ return apiFetch(`/work-items/${id}/status`, {
483
+ method: 'PUT',
484
+ body: JSON.stringify({ status }),
485
+ });
486
+ }
487
+
488
+ export async function getDependencyStatus(id) {
489
+ return apiFetch(`/work-items/${id}/dependency-status`);
490
+ }
491
+
492
+ export async function startSeedCeremony(storyId) {
493
+ return apiFetch('/ceremony/seed/run', {
494
+ method: 'POST',
495
+ body: JSON.stringify({ storyId }),
496
+ });
497
+ }
498
+
499
+ export async function startRunTask(taskId) {
500
+ return apiFetch('/ceremony/run-task/run', {
501
+ method: 'POST',
502
+ body: JSON.stringify({ taskId }),
503
+ });
504
+ }
505
+
506
+ export async function getTaskRunnerStatus() {
507
+ return apiFetch('/ceremony/task-runner/status');
508
+ }
509
+
510
+ export async function cancelTaskRun(processId) {
511
+ return apiFetch('/ceremony/task-runner/cancel', {
512
+ method: 'POST',
513
+ body: JSON.stringify({ processId }),
514
+ });
515
+ }
@@ -118,6 +118,16 @@ export function groupItemsByColumn(workItems) {
118
118
  }
119
119
  });
120
120
 
121
+ // Sort items within each column by coding order (if available), then by ID
122
+ for (const column of COLUMN_ORDER) {
123
+ grouped[column].sort((a, b) => {
124
+ const orderA = a.metadata?.codingOrder ?? Infinity;
125
+ const orderB = b.metadata?.codingOrder ?? Infinity;
126
+ if (orderA !== orderB) return orderA - orderB;
127
+ return (a.id || '').localeCompare(b.id || '');
128
+ });
129
+ }
130
+
121
131
  return grouped;
122
132
  }
123
133
 
@@ -12,6 +12,7 @@ export const useKanbanStore = create((set, get) => ({
12
12
  loading: false,
13
13
  error: null,
14
14
  lastUpdated: null,
15
+ ceremonyActive: false, // true while sprint-planning is writing items — board is read-only
15
16
 
16
17
  // Actions
17
18
 
@@ -112,4 +113,11 @@ export const useKanbanStore = create((set, get) => ({
112
113
  clearError: () => {
113
114
  set({ error: null });
114
115
  },
116
+
117
+ /**
118
+ * Set ceremony active state (read-only mode for work items)
119
+ */
120
+ setCeremonyActive: (active) => {
121
+ set({ ceremonyActive: active });
122
+ },
115
123
  }));
@@ -13,10 +13,12 @@ import { createWorkItemsRouter } from './routes/work-items.js';
13
13
  import { createCeremonyRouter } from './routes/ceremony.js';
14
14
  import { createProcessesRouter } from './routes/processes.js';
15
15
  import { createSettingsRouter } from './routes/settings.js';
16
+ import { createOpenAIOAuthRouter } from './routes/openai-oauth.js';
16
17
  import { createCostsRouter } from './routes/costs.js';
17
18
  import { setupWebSocket } from './routes/websocket.js';
18
19
  import { renderMarkdown } from './utils/markdown.js';
19
20
  import { CeremonyService } from './services/CeremonyService.js';
21
+ import { TaskRunnerService } from './services/TaskRunnerService.js';
20
22
  import { ProcessRegistry } from './services/ProcessRegistry.js';
21
23
  import { WorkItemRefineService } from './services/WorkItemRefineService.js';
22
24
 
@@ -51,6 +53,9 @@ export class KanbanServer {
51
53
  this.ceremonyService = new CeremonyService(projectRoot);
52
54
  this.processRegistry = new ProcessRegistry();
53
55
 
56
+ // Task runner service for seed + run-task operations
57
+ this.taskRunnerService = new TaskRunnerService(projectRoot);
58
+
54
59
  // Work item refine service (websocket injected after start())
55
60
  this.refineService = new WorkItemRefineService(projectRoot);
56
61
 
@@ -186,6 +191,13 @@ export class KanbanServer {
186
191
  const settingsRouter = createSettingsRouter(this.projectRoot);
187
192
  this.app.use('/api/settings', settingsRouter);
188
193
 
194
+ // OpenAI OAuth router
195
+ const openaiOAuthRouter = createOpenAIOAuthRouter(
196
+ this.projectRoot,
197
+ () => this.websocket,
198
+ );
199
+ this.app.use('/api/settings/openai-oauth', openaiOAuthRouter);
200
+
189
201
  // Board title setting (read/write from avc.json)
190
202
  const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
191
203
  const DEFAULT_TITLE = 'AVC Kanban Board';
@@ -227,11 +239,11 @@ export class KanbanServer {
227
239
  });
228
240
 
229
241
  // Work items routes
230
- const workItemsRouter = createWorkItemsRouter(this, this.refineService);
242
+ const workItemsRouter = createWorkItemsRouter(this, this.refineService, this.ceremonyService);
231
243
  this.app.use('/api/work-items', workItemsRouter);
232
244
 
233
245
  // Ceremony routes
234
- const ceremonyRouter = createCeremonyRouter(this.ceremonyService, this.processRegistry);
246
+ const ceremonyRouter = createCeremonyRouter(this.ceremonyService, this.processRegistry, this.taskRunnerService);
235
247
  this.app.use('/api/ceremony', ceremonyRouter);
236
248
 
237
249
  // Process monitor routes
@@ -420,6 +432,11 @@ export class KanbanServer {
420
432
  // Create HTTP server
421
433
  this.server = http.createServer(this.app);
422
434
 
435
+ // Allow long-running LLM requests (local models can be very slow)
436
+ this.server.timeout = 30 * 60 * 1000; // 30 min socket timeout
437
+ this.server.headersTimeout = 30 * 60 * 1000; // 30 min headers timeout
438
+ this.server.requestTimeout = 30 * 60 * 1000; // 30 min request timeout
439
+
423
440
  // Setup WebSocket (pass processRegistry for init message to new clients)
424
441
  this.websocket = setupWebSocket(this.server, this, this.processRegistry, this.ceremonyService);
425
442
 
@@ -427,6 +444,10 @@ export class KanbanServer {
427
444
  this.ceremonyService.setWebSocket(this.websocket);
428
445
  this.ceremonyService.setReloadCallback(() => this.reloadWorkItems());
429
446
 
447
+ // Wire task runner service to WebSocket for broadcasting
448
+ this.taskRunnerService.setWebSocket(this.websocket);
449
+ this.taskRunnerService.setReloadCallback(() => this.reloadWorkItems());
450
+
430
451
  // Wire refine service to WebSocket
431
452
  this.refineService.websocket = this.websocket;
432
453
 
@@ -7,7 +7,7 @@ import path from 'path';
7
7
  * Handles all /api/ceremony/* endpoints for the sponsor-call wizard.
8
8
  * @param {CeremonyService} ceremonyService
9
9
  */
10
- export function createCeremonyRouter(ceremonyService, processRegistry) {
10
+ export function createCeremonyRouter(ceremonyService, processRegistry, taskRunnerService = null) {
11
11
  const router = express.Router();
12
12
 
13
13
  // GET /api/ceremony/status — current ceremony state
@@ -194,6 +194,24 @@ export function createCeremonyRouter(ceremonyService, processRegistry) {
194
194
  }
195
195
  });
196
196
 
197
+ // POST /api/ceremony/refine-field
198
+ // Body: { fieldKey, fieldLabel, currentValue, refinementRequest, context: { mission, scope }, modelId, provider }
199
+ router.post('/refine-field', async (req, res) => {
200
+ const { fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider } = req.body;
201
+ if (!fieldKey || !currentValue?.trim() || !refinementRequest?.trim() || !context?.mission || !modelId || !provider) {
202
+ return res.status(400).json({ error: 'fieldKey, currentValue, refinementRequest, context.mission, modelId and provider are required' });
203
+ }
204
+ const start = Date.now();
205
+ try {
206
+ const result = await ceremonyService.refineField(fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider);
207
+ console.log(`[ceremony] refine-field completed in ${Date.now() - start}ms`, { fieldKey });
208
+ res.json(result);
209
+ } catch (err) {
210
+ console.error(`[ceremony] refine-field failed in ${Date.now() - start}ms:`, err.message);
211
+ res.status(500).json({ error: err.message });
212
+ }
213
+ });
214
+
197
215
  // POST /api/ceremony/run
198
216
  // Body: { requirements } — all 7 template variables
199
217
  router.post('/run', async (req, res) => {
@@ -215,11 +233,64 @@ export function createCeremonyRouter(ceremonyService, processRegistry) {
215
233
  }
216
234
  });
217
235
 
236
+ // GET /api/ceremony/sprint-planning/resumable — check if previous run can be resumed
237
+ router.get('/sprint-planning/resumable', async (req, res) => {
238
+ try {
239
+ const { CeremonyHistory } = await import('../../../cli/ceremony-history.js');
240
+ const historyPath = path.join(ceremonyService.projectRoot, '.avc');
241
+ const history = new CeremonyHistory(historyPath);
242
+ history.init();
243
+
244
+ const resumableStages = ['files-written', 'docs-generated', 'enrichment-complete'];
245
+ const ceremonies = history.data?.ceremonies?.['sprint-planning'];
246
+ const executions = ceremonies?.executions || [];
247
+ const last = executions[executions.length - 1];
248
+
249
+ if (!last) return res.json({ resumable: false });
250
+
251
+ const isAborted = last.status === 'aborted' || last.outcome === 'abrupt-termination' ||
252
+ (last.status === 'in-progress');
253
+ const hasResumableCheckpoint = resumableStages.includes(last.stage);
254
+
255
+ if (!isAborted || !hasResumableCheckpoint) {
256
+ return res.json({ resumable: false, reason: isAborted ? 'checkpoint-too-early' : 'not-aborted' });
257
+ }
258
+
259
+ // Validate files exist on disk
260
+ const projectDir = path.join(ceremonyService.projectRoot, '.avc', 'project');
261
+ const epicDirs = fs.existsSync(projectDir)
262
+ ? fs.readdirSync(projectDir).filter(d => d.startsWith('context-') && fs.existsSync(path.join(projectDir, d, 'work.json')))
263
+ : [];
264
+
265
+ if (epicDirs.length === 0) {
266
+ return res.json({ resumable: false, reason: 'no-files-on-disk' });
267
+ }
268
+
269
+ const stageLabels = {
270
+ 'files-written': 'File Writing (doc generation pending)',
271
+ 'docs-generated': 'Doc Generation (enrichment pending)',
272
+ 'enrichment-complete': 'Enrichment (summary pending)',
273
+ };
274
+
275
+ res.json({
276
+ resumable: true,
277
+ checkpoint: last.stage,
278
+ checkpointLabel: stageLabels[last.stage] || last.stage,
279
+ executionId: last.id,
280
+ timestamp: last.lastCheckpoint || last.startTime,
281
+ epics: epicDirs.length,
282
+ });
283
+ } catch (err) {
284
+ res.json({ resumable: false, reason: err.message });
285
+ }
286
+ });
287
+
218
288
  // POST /api/ceremony/sprint-planning/run
219
289
  router.post('/sprint-planning/run', async (req, res) => {
220
290
  try {
221
- const processId = await ceremonyService.runSprintPlanningInProcess(processRegistry);
222
- console.log('[ceremony] sprint-planning/run started in process', processId);
291
+ const { resumeFrom } = req.body || {};
292
+ const processId = await ceremonyService.runSprintPlanningInProcess(processRegistry, resumeFrom || null);
293
+ console.log('[ceremony] sprint-planning/run started in process', processId, resumeFrom ? `(resume from ${resumeFrom})` : '');
223
294
  res.json({ started: true, processId });
224
295
  } catch (err) {
225
296
  console.error('[ceremony] sprint-planning/run error:', err.message);
@@ -252,7 +323,8 @@ export function createCeremonyRouter(ceremonyService, processRegistry) {
252
323
 
253
324
  // POST /api/ceremony/cancel
254
325
  router.post('/cancel', (req, res) => {
255
- ceremonyService.cancel();
326
+ const keepItems = req.body?.keepItems === true;
327
+ ceremonyService.cancel({ keepItems });
256
328
  res.json({ ok: true });
257
329
  });
258
330
 
@@ -262,6 +334,15 @@ export function createCeremonyRouter(ceremonyService, processRegistry) {
262
334
  res.json({ ok: true });
263
335
  });
264
336
 
337
+ // POST /api/ceremony/quota-continue
338
+ // Body: { newProvider?: string, newModel?: string }
339
+ // Omit newProvider to retry with same model (e.g. user added credits).
340
+ router.post('/quota-continue', (req, res) => {
341
+ const { newProvider = null, newModel = null } = req.body || {};
342
+ ceremonyService.continueAfterQuota(newProvider, newModel);
343
+ res.json({ ok: true });
344
+ });
345
+
265
346
  // POST /api/ceremony/reset — force-stop any running ceremony and reset state immediately
266
347
  router.post('/reset', (req, res) => {
267
348
  ceremonyService.forceReset();
@@ -301,5 +382,73 @@ export function createCeremonyRouter(ceremonyService, processRegistry) {
301
382
  res.json({ ok: true });
302
383
  });
303
384
 
385
+ // ---- Task Runner: Seed + Run ----
386
+
387
+ // POST /api/ceremony/seed/run — seed a story into tasks/subtasks
388
+ router.post('/seed/run', async (req, res) => {
389
+ if (!taskRunnerService) {
390
+ return res.status(503).json({ error: 'TaskRunnerService not available' });
391
+ }
392
+
393
+ const { storyId } = req.body;
394
+ if (!storyId || !/^context-\d{4}-\d{4}[a-z]?$/.test(storyId)) {
395
+ return res.status(400).json({ error: 'Valid storyId is required (format: context-XXXX-XXXX)' });
396
+ }
397
+
398
+ if (taskRunnerService.isRunning(storyId)) {
399
+ return res.status(409).json({ error: `Seed already running for ${storyId}` });
400
+ }
401
+
402
+ try {
403
+ const processId = taskRunnerService.runSeed(storyId);
404
+ res.json({ started: true, processId, storyId });
405
+ } catch (err) {
406
+ res.status(500).json({ error: err.message });
407
+ }
408
+ });
409
+
410
+ // POST /api/ceremony/run-task/run — run a task in a worktree
411
+ router.post('/run-task/run', async (req, res) => {
412
+ if (!taskRunnerService) {
413
+ return res.status(503).json({ error: 'TaskRunnerService not available' });
414
+ }
415
+
416
+ const { taskId } = req.body;
417
+ if (!taskId || !/^context-\d{4}-\d{4}[a-z]?-\d{4}$/.test(taskId)) {
418
+ return res.status(400).json({ error: 'Valid taskId is required (format: context-XXXX-XXXX-XXXX)' });
419
+ }
420
+
421
+ if (taskRunnerService.isRunning(taskId)) {
422
+ return res.status(409).json({ error: `Run already active for ${taskId}` });
423
+ }
424
+
425
+ try {
426
+ const processId = taskRunnerService.runTask(taskId);
427
+ res.json({ started: true, processId, taskId });
428
+ } catch (err) {
429
+ res.status(500).json({ error: err.message });
430
+ }
431
+ });
432
+
433
+ // GET /api/ceremony/task-runner/status — list all active runs
434
+ router.get('/task-runner/status', (req, res) => {
435
+ if (!taskRunnerService) {
436
+ return res.status(503).json({ error: 'TaskRunnerService not available' });
437
+ }
438
+ res.json({ runs: taskRunnerService.listRuns() });
439
+ });
440
+
441
+ // POST /api/ceremony/task-runner/cancel — cancel a run
442
+ router.post('/task-runner/cancel', (req, res) => {
443
+ if (!taskRunnerService) {
444
+ return res.status(503).json({ error: 'TaskRunnerService not available' });
445
+ }
446
+ const { processId } = req.body;
447
+ if (!processId) return res.status(400).json({ error: 'processId is required' });
448
+
449
+ const cancelled = taskRunnerService.cancel(processId);
450
+ res.json({ cancelled });
451
+ });
452
+
304
453
  return router;
305
454
  }
@@ -96,7 +96,9 @@ export function createCostsRouter(projectRoot) {
96
96
  .map(([date, data]) => ({
97
97
  date,
98
98
  cost: data.cost?.total ?? 0,
99
+ saved: data.cost?.saved ?? 0,
99
100
  tokens: data.total ?? 0,
101
+ cached: data.cached ?? 0,
100
102
  executions: data.executions ?? 0,
101
103
  }));
102
104
 
@@ -104,7 +106,7 @@ export function createCostsRouter(projectRoot) {
104
106
  const SKIP_KEYS = new Set(['version', 'lastUpdated', 'totals']);
105
107
  const parentNodes = {};
106
108
  for (const p of PARENT_CEREMONIES) {
107
- parentNodes[p] = { name: p, calls: 0, tokens: 0, cost: 0, stages: [] };
109
+ parentNodes[p] = { name: p, calls: 0, tokens: 0, cost: 0, cached: 0, saved: 0, stages: [] };
108
110
  }
109
111
  const orphans = []; // keys that don't map to a known parent
110
112
 
@@ -112,7 +114,7 @@ export function createCostsRouter(projectRoot) {
112
114
  if (SKIP_KEYS.has(key)) continue;
113
115
  if (!value || typeof value !== 'object') continue;
114
116
 
115
- let totalInput = 0, totalOutput = 0, totalCost = 0, totalExec = 0;
117
+ let totalInput = 0, totalOutput = 0, totalCost = 0, totalExec = 0, totalCached = 0, totalSaved = 0;
116
118
  const dailyForKey = value.daily ?? {};
117
119
  for (const [date, data] of Object.entries(dailyForKey)) {
118
120
  const d = new Date(date);
@@ -121,12 +123,14 @@ export function createCostsRouter(projectRoot) {
121
123
  totalOutput += data.output ?? 0;
122
124
  totalCost += data.cost?.total ?? 0;
123
125
  totalExec += data.executions ?? 0;
126
+ totalCached += data.cached ?? 0;
127
+ totalSaved += data.cost?.saved ?? 0;
124
128
  }
125
129
  }
126
130
 
127
131
  if (totalExec === 0 && totalInput === 0 && totalOutput === 0) continue;
128
132
 
129
- const entry = { name: key, calls: totalExec, tokens: totalInput + totalOutput, cost: totalCost };
133
+ const entry = { name: key, calls: totalExec, tokens: totalInput + totalOutput, cost: totalCost, cached: totalCached, saved: totalSaved };
130
134
  const parent = getParentCeremony(key);
131
135
 
132
136
  if (parent && parentNodes[parent]) {
@@ -137,6 +141,8 @@ export function createCostsRouter(projectRoot) {
137
141
  parentNodes[parent].calls += totalExec;
138
142
  parentNodes[parent].tokens += totalInput + totalOutput;
139
143
  parentNodes[parent].cost += totalCost;
144
+ parentNodes[parent].cached += totalCached;
145
+ parentNodes[parent].saved += totalSaved;
140
146
  } else {
141
147
  orphans.push({ ...entry, stages: [] });
142
148
  }