@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
@@ -5,14 +5,35 @@ import { fileURLToPath } from 'url';
5
5
  import { KanbanLogger } from '../utils/kanban-logger.js';
6
6
  import { TokenTracker } from '../../../cli/token-tracker.js';
7
7
  import { loadAgent } from '../../../cli/agent-loader.js';
8
+ import { PromptLogger } from '../../../cli/prompt-logger.js';
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
10
 
10
11
  const PROVIDER_KEY_MAP = {
11
12
  claude: 'ANTHROPIC_API_KEY',
12
13
  gemini: 'GEMINI_API_KEY',
13
14
  openai: 'OPENAI_API_KEY',
15
+ xiaomi: 'XIAOMI_API_KEY',
14
16
  };
15
17
 
18
+ /** Returns true when the provider is OpenAI and OAuth mode is active (flat-rate — no per-token billing). */
19
+ function isOAuthProvider(provider) {
20
+ return provider === 'openai' && process.env.OPENAI_AUTH_MODE === 'oauth';
21
+ }
22
+
23
+ // Returns true if a provider has any valid auth credential (API key or token/oauth)
24
+ function hasProviderAuth(provider, projectRoot) {
25
+ if (provider === 'local') return true; // No API key needed for local models
26
+ if (provider === 'claude') {
27
+ return !!process.env.ANTHROPIC_API_KEY;
28
+ }
29
+ if (provider === 'openai') {
30
+ const oauthFile = path.join(projectRoot, '.avc', 'openai-oauth.json');
31
+ return !!(process.env.OPENAI_API_KEY ||
32
+ (process.env.OPENAI_AUTH_MODE === 'oauth' && fs.existsSync(oauthFile)));
33
+ }
34
+ return !!process.env[PROVIDER_KEY_MAP[provider]];
35
+ }
36
+
16
37
  /**
17
38
  * CeremonyService
18
39
  * Orchestrates the sponsor-call ceremony from the web UI.
@@ -22,7 +43,7 @@ const PROVIDER_KEY_MAP = {
22
43
  export class CeremonyService {
23
44
  constructor(projectRoot) {
24
45
  this.projectRoot = projectRoot;
25
- this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, decomposedHierarchy: null };
46
+ this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, quotaLimitInfo: null, decomposedHierarchy: null };
26
47
  this.websocket = null;
27
48
  this._paused = false;
28
49
  this._cancelled = false;
@@ -54,10 +75,32 @@ export class CeremonyService {
54
75
  }
55
76
  }
56
77
 
57
- cancel() {
78
+ cancel({ keepItems = false } = {}) {
58
79
  this._cancelled = true;
80
+ this._keepItemsOnCancel = keepItems;
81
+ // Mark state as cancelling so handleWorkerExit can distinguish cancel-in-progress
82
+ // from a genuine unexpected crash (the worker may exit before sending 'cancelled' IPC).
83
+ this.state.status = 'cancelling';
59
84
  if (this._activeChild) {
60
85
  try { this._activeChild.send({ type: 'cancel' }); } catch (_) {}
86
+ // Safety net: if the worker doesn't exit within 10s (stuck in a long LLM call),
87
+ // send SIGTERM to force it down. handleWorkerExit will clean up.
88
+ const child = this._activeChild;
89
+ this._cancelKillTimer = setTimeout(() => {
90
+ this._cancelKillTimer = null;
91
+ if (child === this._activeChild && this.state.status === 'cancelling') {
92
+ console.log('[ceremony] cancel timeout — sending SIGTERM to worker');
93
+ try { child.kill('SIGTERM'); } catch (_) {}
94
+ // Last resort: SIGKILL after 3 more seconds
95
+ this._cancelKillTimer = setTimeout(() => {
96
+ this._cancelKillTimer = null;
97
+ if (child === this._activeChild) {
98
+ console.log('[ceremony] cancel SIGTERM timeout — sending SIGKILL');
99
+ try { child.kill('SIGKILL'); } catch (_) {}
100
+ }
101
+ }, 3000);
102
+ }
103
+ }, 10000);
61
104
  }
62
105
  const isSprintPlanning = this._runningType === 'sprint-planning';
63
106
  const msg = 'Waiting for current LLM call to finish…';
@@ -69,6 +112,7 @@ export class CeremonyService {
69
112
  forceReset() {
70
113
  this._cancelled = true;
71
114
  this._paused = false;
115
+ if (this._cancelKillTimer) { clearTimeout(this._cancelKillTimer); this._cancelKillTimer = null; }
72
116
  if (this._activeChild) {
73
117
  try { this._activeChild.send({ type: 'cancel' }); } catch (_) {}
74
118
  const child = this._activeChild;
@@ -78,7 +122,7 @@ export class CeremonyService {
78
122
  const wasRunningType = this._runningType;
79
123
  this._runningType = null;
80
124
  this._activeProcessId = null;
81
- this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, decomposedHierarchy: null };
125
+ this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, quotaLimitInfo: null, decomposedHierarchy: null };
82
126
  // Broadcast to whichever ceremony was running (or both if unknown)
83
127
  if (wasRunningType === 'sprint-planning' || !wasRunningType) {
84
128
  this.websocket?.broadcastSprintPlanningCancelled();
@@ -90,13 +134,20 @@ export class CeremonyService {
90
134
 
91
135
  _cleanupCancelledSprintPlanning() {
92
136
  const projectDir = path.join(this.projectRoot, '.avc', 'project');
93
- if (!fs.existsSync(projectDir)) return;
137
+ if (!fs.existsSync(projectDir)) {
138
+ console.log('[ceremony] cleanup: project dir does not exist, nothing to delete');
139
+ return;
140
+ }
94
141
  const current = fs.readdirSync(projectDir);
95
142
  const toDelete = current.filter(d => !this._preRunSnapshot.includes(d));
143
+ console.log(`[ceremony] cleanup: snapshot=${this._preRunSnapshot?.length ?? 'null'} entries, current=${current.length}, toDelete=${toDelete.length}`);
96
144
  for (const d of toDelete) {
97
145
  try {
98
146
  fs.rmSync(path.join(projectDir, d), { recursive: true, force: true });
99
- } catch (_) {}
147
+ console.log(`[ceremony] cleanup: deleted ${d}`);
148
+ } catch (err) {
149
+ console.error(`[ceremony] cleanup: failed to delete ${d}: ${err.message}`);
150
+ }
100
151
  }
101
152
  }
102
153
 
@@ -117,13 +168,14 @@ export class CeremonyService {
117
168
  result: this.state.result,
118
169
  error: this.state.error,
119
170
  costLimitInfo: this.state.costLimitInfo || null,
171
+ quotaLimitInfo: this.state.quotaLimitInfo || null,
120
172
  decomposedHierarchy: this.state.decomposedHierarchy || null,
121
173
  };
122
174
  }
123
175
 
124
176
  async getAvailableModels() {
125
177
  const { default: dotenv } = await import('dotenv');
126
- dotenv.config({ path: path.join(this.projectRoot, '.env') });
178
+ dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
127
179
 
128
180
  const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
129
181
  const avcConfig = JSON.parse(fs.readFileSync(avcJsonPath, 'utf8'));
@@ -133,7 +185,7 @@ export class CeremonyService {
133
185
  modelId,
134
186
  displayName: info.displayName,
135
187
  provider: info.provider,
136
- hasApiKey: !!process.env[PROVIDER_KEY_MAP[info.provider]],
188
+ hasApiKey: hasProviderAuth(info.provider, this.projectRoot),
137
189
  }));
138
190
  }
139
191
 
@@ -147,7 +199,7 @@ export class CeremonyService {
147
199
 
148
200
  try {
149
201
  const { default: dotenv } = await import('dotenv');
150
- dotenv.config({ path: path.join(this.projectRoot, '.env') });
202
+ dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
151
203
  log.debug('dotenv loaded', { envFile: path.join(this.projectRoot, '.env') });
152
204
 
153
205
  // Read validation settings exclusively from avc.json
@@ -177,6 +229,11 @@ export class CeremonyService {
177
229
  validator: `${validatorProvider}/${validatorModelId}`,
178
230
  });
179
231
 
232
+ // Attach prompt logger
233
+ const _missionPromptLogger = new PromptLogger(this.projectRoot, 'sponsor-call');
234
+ generatorLLM.setPromptLogger(_missionPromptLogger, 'mission-generate');
235
+ validatorLLM.setPromptLogger(_missionPromptLogger, 'mission-validate');
236
+
180
237
  // Load agent files
181
238
  log.debug('Loading agent files');
182
239
  const generatorAgent = loadAgent('mission-scope-generator.md', this.projectRoot);
@@ -343,6 +400,7 @@ export class CeremonyService {
343
400
  output: genUsage.outputTokens,
344
401
  provider,
345
402
  model: modelId,
403
+ skipCost: isOAuthProvider(provider),
346
404
  });
347
405
  }
348
406
  const valUsage = validatorLLM.getTokenUsage();
@@ -352,6 +410,7 @@ export class CeremonyService {
352
410
  output: valUsage.outputTokens,
353
411
  provider: validatorProvider,
354
412
  model: validatorModelId,
413
+ skipCost: isOAuthProvider(validatorProvider),
355
414
  });
356
415
  }
357
416
  } catch (trackErr) {
@@ -381,7 +440,7 @@ export class CeremonyService {
381
440
 
382
441
  try {
383
442
  const { default: dotenv } = await import('dotenv');
384
- dotenv.config({ path: path.join(this.projectRoot, '.env') });
443
+ dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
385
444
 
386
445
  const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
387
446
  const avcConfig = JSON.parse(fs.readFileSync(avcJsonPath, 'utf8'));
@@ -400,6 +459,11 @@ export class CeremonyService {
400
459
  const generatorLLM = await LLMProvider.create(provider, modelId);
401
460
  const validatorLLM = await LLMProvider.create(validatorProvider, validatorModelId);
402
461
 
462
+ // Attach prompt logger
463
+ const _refinePromptLogger = new PromptLogger(this.projectRoot, 'sponsor-call');
464
+ generatorLLM.setPromptLogger(_refinePromptLogger, 'mission-refine-generate');
465
+ validatorLLM.setPromptLogger(_refinePromptLogger, 'mission-refine-validate');
466
+
403
467
  const generatorAgent = loadAgent('mission-scope-generator.md', this.projectRoot);
404
468
  const validatorAgent = loadAgent('mission-scope-validator.md', this.projectRoot);
405
469
 
@@ -535,6 +599,7 @@ export class CeremonyService {
535
599
  output: genUsage.outputTokens,
536
600
  provider,
537
601
  model: modelId,
602
+ skipCost: isOAuthProvider(provider),
538
603
  });
539
604
  }
540
605
  const valUsage = validatorLLM.getTokenUsage();
@@ -544,6 +609,7 @@ export class CeremonyService {
544
609
  output: valUsage.outputTokens,
545
610
  provider: validatorProvider,
546
611
  model: validatorModelId,
612
+ skipCost: isOAuthProvider(validatorProvider),
547
613
  });
548
614
  }
549
615
  } catch (trackErr) {
@@ -562,10 +628,11 @@ export class CeremonyService {
562
628
  async generateCustomArchitecture(description, modelId, provider) {
563
629
  const log = new KanbanLogger('arch-custom', this.projectRoot);
564
630
  const { default: dotenv } = await import('dotenv');
565
- dotenv.config({ path: path.join(this.projectRoot, '.env') });
631
+ dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
566
632
 
567
633
  const { LLMProvider } = await import('../../../cli/llm-provider.js');
568
634
  const llm = await LLMProvider.create(provider, modelId);
635
+ llm.setPromptLogger(new PromptLogger(this.projectRoot, 'sponsor-call'), 'arch-generate');
569
636
  const agentInstruction = loadAgent('architecture-recommender.md', this.projectRoot);
570
637
 
571
638
  const prompt =
@@ -588,10 +655,11 @@ export class CeremonyService {
588
655
  async refineCustomArchitecture(currentArch, refinementRequest, modelId, provider) {
589
656
  const log = new KanbanLogger('arch-custom', this.projectRoot);
590
657
  const { default: dotenv } = await import('dotenv');
591
- dotenv.config({ path: path.join(this.projectRoot, '.env') });
658
+ dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
592
659
 
593
660
  const { LLMProvider } = await import('../../../cli/llm-provider.js');
594
661
  const llm = await LLMProvider.create(provider, modelId);
662
+ llm.setPromptLogger(new PromptLogger(this.projectRoot, 'sponsor-call'), 'arch-refine');
595
663
  const agentInstruction = loadAgent('architecture-recommender.md', this.projectRoot);
596
664
 
597
665
  const prompt =
@@ -636,6 +704,7 @@ export class CeremonyService {
636
704
  output: usage.outputTokens,
637
705
  provider: usage.provider,
638
706
  model: usage.model,
707
+ skipCost: isOAuthProvider(usage.provider),
639
708
  });
640
709
  }
641
710
  }
@@ -682,6 +751,7 @@ export class CeremonyService {
682
751
  output: usage.outputTokens,
683
752
  provider: usage.provider,
684
753
  model: usage.model,
754
+ skipCost: isOAuthProvider(usage.provider),
685
755
  });
686
756
  }
687
757
  }
@@ -730,6 +800,7 @@ export class CeremonyService {
730
800
  output: usage.outputTokens,
731
801
  provider: usage.provider,
732
802
  model: usage.model,
803
+ skipCost: isOAuthProvider(usage.provider),
733
804
  });
734
805
  }
735
806
  }
@@ -748,6 +819,71 @@ export class CeremonyService {
748
819
  }
749
820
  }
750
821
 
822
+ /**
823
+ * Refine a single requirement field using an LLM.
824
+ * @param {string} fieldKey - e.g. 'TARGET_USERS'
825
+ * @param {string} fieldLabel - e.g. 'Target Users'
826
+ * @param {string} currentValue - current field content
827
+ * @param {string} refinementRequest - user's instruction for improvement
828
+ * @param {object} context - { mission, scope } for grounding
829
+ * @param {string} modelId
830
+ * @param {string} provider
831
+ * @returns {Promise<{ value: string }>}
832
+ */
833
+ async refineField(fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider) {
834
+ const log = new KanbanLogger('refine-field', this.projectRoot);
835
+ log.info('refineField() called', { fieldKey, modelId, provider });
836
+
837
+ const { default: dotenv } = await import('dotenv');
838
+ dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
839
+
840
+ const { LLMProvider } = await import('../../../cli/llm-provider.js');
841
+ const llm = await LLMProvider.create(provider, modelId);
842
+ llm.setPromptLogger(new PromptLogger(this.projectRoot, 'sponsor-call'), 'refine-field');
843
+
844
+ const prompt =
845
+ `You are helping refine a single field of a software project's requirements.\n\n` +
846
+ `Project Mission: ${context.mission}\n\n` +
847
+ `Project Scope: ${context.scope}\n\n` +
848
+ `Field: "${fieldLabel}" (key: ${fieldKey})\n` +
849
+ `Current value:\n${currentValue}\n\n` +
850
+ `User's refinement request: ${refinementRequest}\n\n` +
851
+ `Return a JSON object with a single key "value" containing the improved text for this field. ` +
852
+ `Keep the same general format and level of detail. Do not include any other keys.`;
853
+
854
+ try {
855
+ const result = await llm.generateJSON(prompt);
856
+ if (!result?.value) {
857
+ throw new Error('Model returned empty result');
858
+ }
859
+
860
+ // Track token usage
861
+ try {
862
+ const tracker = new TokenTracker(path.join(this.projectRoot, '.avc'));
863
+ const usage = llm.getTokenUsage();
864
+ if (usage.totalCalls > 0) {
865
+ tracker.addExecution('refine-field', {
866
+ input: usage.inputTokens,
867
+ output: usage.outputTokens,
868
+ provider: usage.provider,
869
+ model: usage.model,
870
+ skipCost: isOAuthProvider(usage.provider),
871
+ });
872
+ }
873
+ } catch (trackErr) {
874
+ log.warn('Failed to track token usage', { error: trackErr.message });
875
+ }
876
+
877
+ log.info('refineField() completed', { fieldKey, valueLength: result.value.length });
878
+ log.finish(true);
879
+ return { value: result.value };
880
+ } catch (err) {
881
+ log.error('refineField() failed', { fieldKey, message: err.message });
882
+ log.finish(false, err.message);
883
+ throw err;
884
+ }
885
+ }
886
+
751
887
  // ── Fork-based ceremony execution ───────────────────────────────────────────
752
888
 
753
889
  /**
@@ -803,15 +939,25 @@ export class CeremonyService {
803
939
  else this.websocket?.broadcastCeremonyComplete(msg.result);
804
940
  this.websocket?.broadcastRefresh();
805
941
  break;
806
- case 'cancelled':
807
- if (isSP) this._cleanupCancelledSprintPlanning();
942
+ case 'cancelled': {
943
+ if (this._cancelKillTimer) { clearTimeout(this._cancelKillTimer); this._cancelKillTimer = null; }
944
+ const itemsKept = isSP && this._keepItemsOnCancel;
945
+ if (isSP && !this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
946
+ // Reload + refresh regardless of keep/delete so the board reflects disk state
947
+ if (isSP) {
948
+ await this._reloadCallback?.();
949
+ this.websocket?.broadcastRefresh();
950
+ }
808
951
  this.state.status = 'idle';
809
952
  this._activeChild = null;
810
953
  this._activeProcessId = null;
954
+ this._keepItemsOnCancel = false;
955
+ this._runningType = null;
811
956
  registry.setStatus(record.id, 'cancelled');
812
- if (isSP) this.websocket?.broadcastSprintPlanningCancelled();
957
+ if (isSP) this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
813
958
  else this.websocket?.broadcastCeremonyCancelled();
814
959
  break;
960
+ }
815
961
  case 'error':
816
962
  this.state.status = 'error';
817
963
  this.state.error = msg.error;
@@ -831,6 +977,17 @@ export class CeremonyService {
831
977
  this.websocket?.broadcastCostLimit(msg.cost, msg.threshold, this._runningType);
832
978
  break;
833
979
  }
980
+ case 'quota-limit':
981
+ this.state.status = 'quota-limit-pending';
982
+ this.state.quotaLimitInfo = {
983
+ validatorName: msg.validatorName,
984
+ errMsg: msg.errMsg,
985
+ provider: msg.provider,
986
+ model: msg.model,
987
+ };
988
+ registry.setStatus(record.id, 'paused');
989
+ this.websocket?.broadcastQuotaLimit(msg.provider, msg.model, msg.errMsg, msg.validatorName, this._runningType);
990
+ break;
834
991
  case 'decomposition-complete':
835
992
  this.state.status = 'awaiting-selection';
836
993
  this.state.decomposedHierarchy = msg.hierarchy;
@@ -838,6 +995,22 @@ export class CeremonyService {
838
995
  registry.setStatus(record.id, 'paused');
839
996
  this.websocket?.broadcastSprintPlanningDecompositionComplete(msg.hierarchy);
840
997
  break;
998
+ case 'item-written':
999
+ // A single epic or story work.json was written — debounce board refresh
1000
+ // so rapid writes don't cause excessive reloads.
1001
+ if (this._itemWrittenTimer) clearTimeout(this._itemWrittenTimer);
1002
+ this._itemWrittenTimer = setTimeout(async () => {
1003
+ this._itemWrittenTimer = null;
1004
+ await this._reloadCallback?.();
1005
+ this.websocket?.broadcastRefresh();
1006
+ }, 300);
1007
+ break;
1008
+ case 'hierarchy-written':
1009
+ // Stage 6 finished writing all work.json files — final refresh to catch everything.
1010
+ if (this._itemWrittenTimer) { clearTimeout(this._itemWrittenTimer); this._itemWrittenTimer = null; }
1011
+ await this._reloadCallback?.();
1012
+ this.websocket?.broadcastRefresh();
1013
+ break;
841
1014
  }
842
1015
  }
843
1016
 
@@ -867,6 +1040,21 @@ export class CeremonyService {
867
1040
  this.state.costLimitInfo = null;
868
1041
  }
869
1042
 
1043
+ /**
1044
+ * Resume ceremony after quota-limit pause.
1045
+ * newProvider/newModel: if provided, worker switches validation+solver stage config.
1046
+ * If null, worker retries with the same model (e.g., user added credits).
1047
+ */
1048
+ continueAfterQuota(newProvider = null, newModel = null) {
1049
+ if (this._activeChild) {
1050
+ try {
1051
+ this._activeChild.send({ type: 'quota-continue', newProvider, newModel });
1052
+ } catch (_) {}
1053
+ }
1054
+ this.state.status = 'running';
1055
+ this.state.quotaLimitInfo = null;
1056
+ }
1057
+
870
1058
  /**
871
1059
  * Read the cost threshold for a ceremony type from avc.json.
872
1060
  * Returns null if not configured (unlimited).
@@ -874,7 +1062,13 @@ export class CeremonyService {
874
1062
  _getCostThreshold(ceremonyType) {
875
1063
  try {
876
1064
  const config = JSON.parse(fs.readFileSync(path.join(this.projectRoot, '.avc', 'avc.json'), 'utf8'));
877
- return config.settings?.costThresholds?.[ceremonyType] ?? null;
1065
+ const threshold = config.settings?.costThresholds?.[ceremonyType] ?? null;
1066
+ if (threshold == null) return null;
1067
+ // OAuth providers are flat-rate (no per-token billing) — cost limits don't apply
1068
+ const ceremony = config.settings?.ceremonies?.find(c => c.name === ceremonyType);
1069
+ const provider = ceremony?.provider ?? config.settings?.provider ?? 'claude';
1070
+ if (isOAuthProvider(provider)) return null;
1071
+ return threshold;
878
1072
  } catch { return null; }
879
1073
  }
880
1074
 
@@ -887,20 +1081,42 @@ export class CeremonyService {
887
1081
  }
888
1082
 
889
1083
  /** Relay worker exit notification received via CLI → Kanban IPC channel. */
890
- handleWorkerExit(processId, code) {
1084
+ async handleWorkerExit(processId, code) {
891
1085
  const record = this._registry?.getByProcessId(processId);
892
1086
  if (!record) return;
1087
+ if (this._cancelKillTimer) { clearTimeout(this._cancelKillTimer); this._cancelKillTimer = null; }
893
1088
  this._activeChild = null;
894
1089
  this._activeProcessId = null;
895
1090
  const isSP = record.type === 'sprint-planning';
1091
+
1092
+ // Cancel was in progress but the worker exited before sending its 'cancelled' IPC
1093
+ // message (e.g. IPC channel broke, disconnect handler fired with exit code 1).
1094
+ // Treat this as a successful cancellation — run cleanup and broadcast.
1095
+ if (this.state.status === 'cancelling') {
1096
+ const itemsKept = isSP && this._keepItemsOnCancel;
1097
+ if (isSP && !this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
1098
+ if (isSP) {
1099
+ await this._reloadCallback?.();
1100
+ this.websocket?.broadcastRefresh();
1101
+ }
1102
+ this.state.status = 'idle';
1103
+ this._keepItemsOnCancel = false;
1104
+ this._runningType = null;
1105
+ this._registry.setStatus(record.id, 'cancelled');
1106
+ if (isSP) this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
1107
+ else this.websocket?.broadcastCeremonyCancelled();
1108
+ return;
1109
+ }
1110
+
896
1111
  const wasActive = this._runningType === record.type &&
897
- (this.state.status === 'running' || this.state.status === 'cost-limit-pending');
1112
+ (this.state.status === 'running' || this.state.status === 'cost-limit-pending' || this.state.status === 'quota-limit-pending');
898
1113
  if (wasActive) {
899
1114
  const error = `Worker exited unexpectedly (code ${code})`;
900
1115
  this._registry.setStatus(record.id, 'error', { error });
901
1116
  this.state.status = 'error';
902
1117
  this.state.error = error;
903
1118
  this.state.costLimitInfo = null;
1119
+ this.state.quotaLimitInfo = null;
904
1120
  if (isSP) this.websocket?.broadcastSprintPlanningError(error);
905
1121
  else this.websocket?.broadcastCeremonyError(error);
906
1122
  }
@@ -919,7 +1135,7 @@ export class CeremonyService {
919
1135
  * @param {ProcessRegistry} registry
920
1136
  * @returns {string} processId
921
1137
  */
922
- async runSprintPlanningInProcess(registry) {
1138
+ async runSprintPlanningInProcess(registry, resumeFrom = null) {
923
1139
  if (this.state.status === 'running') {
924
1140
  throw new Error('Ceremony already running');
925
1141
  }
@@ -946,7 +1162,7 @@ export class CeremonyService {
946
1162
  };
947
1163
  this._activeChild = proxy;
948
1164
  registry.attach(record.id, proxy);
949
- process.send({ type: 'ceremony:fork', ceremonyType: 'sprint-planning', processId: record.id, costThreshold });
1165
+ process.send({ type: 'ceremony:fork', ceremonyType: 'sprint-planning', processId: record.id, costThreshold, resumeFrom });
950
1166
  return record.id;
951
1167
  }
952
1168
 
@@ -954,6 +1170,7 @@ export class CeremonyService {
954
1170
  const workerPath = path.join(__dirname, '../workers/sprint-planning-worker.js');
955
1171
  const child = fork(workerPath, [], {
956
1172
  stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
1173
+ cwd: this.projectRoot,
957
1174
  env: { ...process.env },
958
1175
  });
959
1176
  child.stdout?.on('data', d => process.stdout.write(d));
@@ -961,13 +1178,26 @@ export class CeremonyService {
961
1178
 
962
1179
  registry.attach(record.id, child);
963
1180
  this._activeChild = child;
964
- child.send({ type: 'init', costThreshold });
1181
+ child.send({ type: 'init', costThreshold, resumeFrom });
965
1182
 
966
1183
  child.on('message', (msg) => this._dispatchWorkerMessage(msg, record, registry));
967
1184
 
968
- child.on('exit', (code) => {
1185
+ child.on('exit', async (code) => {
969
1186
  this._activeChild = null;
970
1187
  this._activeProcessId = null;
1188
+ // Cancel was in progress but worker exited before sending 'cancelled' IPC
1189
+ if (this.state.status === 'cancelling') {
1190
+ const itemsKept = this._keepItemsOnCancel;
1191
+ if (!this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
1192
+ await this._reloadCallback?.();
1193
+ this.websocket?.broadcastRefresh();
1194
+ this.state.status = 'idle';
1195
+ this._keepItemsOnCancel = false;
1196
+ this._runningType = null;
1197
+ registry.setStatus(record.id, 'cancelled');
1198
+ this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
1199
+ return;
1200
+ }
971
1201
  if (this._runningType === 'sprint-planning' && this.state.status === 'running') {
972
1202
  registry.setStatus(record.id, 'error', { error: `Worker exited unexpectedly (code ${code})` });
973
1203
  this.state.status = 'error';
@@ -1019,6 +1249,7 @@ export class CeremonyService {
1019
1249
  const workerPath = path.join(__dirname, '../workers/sponsor-call-worker.js');
1020
1250
  const child = fork(workerPath, [], {
1021
1251
  stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
1252
+ cwd: this.projectRoot,
1022
1253
  env: { ...process.env },
1023
1254
  });
1024
1255
  child.stdout?.on('data', d => process.stdout.write(d));
@@ -1033,6 +1264,15 @@ export class CeremonyService {
1033
1264
  child.on('exit', (code) => {
1034
1265
  this._activeChild = null;
1035
1266
  this._activeProcessId = null;
1267
+ // Cancel was in progress but worker exited before sending 'cancelled' IPC
1268
+ if (this.state.status === 'cancelling') {
1269
+ this.state.status = 'idle';
1270
+ this._keepItemsOnCancel = false;
1271
+ this._runningType = null;
1272
+ registry.setStatus(record.id, 'cancelled');
1273
+ this.websocket?.broadcastCeremonyCancelled();
1274
+ return;
1275
+ }
1036
1276
  if (this._runningType === 'sponsor-call' && this.state.status === 'running') {
1037
1277
  registry.setStatus(record.id, 'error', { error: `Worker exited unexpectedly (code ${code})` });
1038
1278
  this.state.status = 'error';
@@ -1111,11 +1351,17 @@ export class CeremonyService {
1111
1351
  this.websocket?.broadcastRefresh();
1112
1352
  } catch (err) {
1113
1353
  if (err.message === 'CEREMONY_CANCELLED') {
1114
- this._cleanupCancelledSprintPlanning();
1354
+ const itemsKept = this._keepItemsOnCancel;
1355
+ if (!this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
1356
+ // Reload + refresh regardless of keep/delete so the board reflects disk state
1357
+ await this._reloadCallback?.();
1358
+ this.websocket?.broadcastRefresh();
1359
+ this._keepItemsOnCancel = false;
1360
+ this._runningType = null;
1115
1361
  this.state.status = 'idle';
1116
- log.info('_runSprintPlanningAsync() cancelled by user');
1362
+ log.info('_runSprintPlanningAsync() cancelled by user', { itemsKept });
1117
1363
  log.finish(true, 'cancelled');
1118
- this.websocket?.broadcastSprintPlanningCancelled();
1364
+ this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
1119
1365
  } else {
1120
1366
  this.state.status = 'error';
1121
1367
  this.state.error = err.message;
@@ -1163,6 +1409,11 @@ export class CeremonyService {
1163
1409
 
1164
1410
  const result = await initiator.sponsorCallWithAnswers(requirements, progressCallback);
1165
1411
 
1412
+ // sponsorCallWithAnswers returns { error: true, message } on validation failure instead of throwing
1413
+ if (result?.error === true) {
1414
+ throw new Error(result.message || 'Ceremony failed');
1415
+ }
1416
+
1166
1417
  this.state.status = 'complete';
1167
1418
  this.state.result = result;
1168
1419
  log.info('_runAsync() completed successfully', {