@agile-vibe-coding/avc 0.1.1 → 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 (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
package/cli/init.js CHANGED
@@ -6,6 +6,77 @@ import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { execSync } from 'child_process';
8
8
  import { TemplateProcessor } from './template-processor.js';
9
+ import { ModelConfigurator } from './init-model-config.js';
10
+ import { MESSAGES, getCeremonyHeader } from './message-constants.js';
11
+ import { sendError, sendWarning, sendSuccess, sendInfo, sendOutput, sendIndented, sendSectionHeader } from './messaging-api.js';
12
+ import { boldCyan, yellow, green, cyan } from './ansi-colors.js';
13
+
14
+ /**
15
+ * Trace log written directly to /tmp/avc-init.log so we can diagnose mkdir
16
+ * failures that occur before .avc/ exists (where normal logging goes).
17
+ */
18
+ function initTrace(message, data = null) {
19
+ try {
20
+ const ts = new Date().toISOString();
21
+ const line = data
22
+ ? `[${ts}] ${message} ${JSON.stringify(data)}\n`
23
+ : `[${ts}] ${message}\n`;
24
+ fs.appendFileSync('/tmp/avc-init.log', line);
25
+ } catch { /* never let tracing break init */ }
26
+ }
27
+
28
+ /**
29
+ * Reliable recursive mkdir for WSL2 /mnt/ Windows paths.
30
+ *
31
+ * Two-layer strategy:
32
+ * 1. Walk the tree manually and call plain mkdirSync() for each segment
33
+ * (avoids Node's { recursive:true } which can race on DrvFS/9P).
34
+ * 2. If mkdirSync() still fails with ENOENT (VFS dentry-cache stale —
35
+ * stat() says parent exists but mkdir() syscall disagrees), fall back
36
+ * to a subprocess `mkdir -p`. The subprocess starts with a clean
37
+ * dentry cache and sees the filesystem state the kernel actually has.
38
+ */
39
+ function mkdirp(dirPath) {
40
+ initTrace('mkdirp enter', { dirPath, exists: fs.existsSync(dirPath) });
41
+ if (fs.existsSync(dirPath)) return;
42
+ const parent = path.dirname(dirPath);
43
+ if (parent !== dirPath) mkdirp(parent);
44
+ try {
45
+ fs.mkdirSync(dirPath);
46
+ initTrace('mkdirp created via mkdirSync', { dirPath });
47
+ } catch (err) {
48
+ if (err.code === 'EEXIST') return;
49
+ if (err.code === 'ENOENT') {
50
+ // WSL2/DrvFS: VFS dentry cache reports parent as present but the 9P
51
+ // server disagrees at mkdir() time. Spawn a fresh process whose cache
52
+ // is clean — shell mkdir -p goes straight to the 9P server correctly.
53
+ initTrace('mkdirp mkdirSync ENOENT — falling back to shell mkdir -p', { dirPath, parentExists: fs.existsSync(parent) });
54
+ try {
55
+ execSync(`mkdir -p ${JSON.stringify(dirPath)}`, { stdio: 'pipe' });
56
+ initTrace('mkdirp created via shell mkdir -p', { dirPath });
57
+ return;
58
+ } catch (shellErr) {
59
+ initTrace('mkdirp shell mkdir -p also failed', { dirPath, shellErr: shellErr.message });
60
+ if (!fs.existsSync(dirPath)) throw err; // throw original ENOENT
61
+ return; // shell failed but dir now exists — tolerate
62
+ }
63
+ }
64
+ throw err;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Write a structured entry to the active command log file only.
70
+ * Uses [DEBUG] prefix so ConsoleOutputManager routes to file, never terminal.
71
+ */
72
+ function fileLog(level, message, data = null) {
73
+ const ts = new Date().toISOString();
74
+ if (data !== null) {
75
+ console.log(`[DEBUG] [${level}] [${ts}] ${message}`, JSON.stringify(data, null, 2));
76
+ } else {
77
+ console.log(`[DEBUG] [${level}] [${ts}] ${message}`);
78
+ }
79
+ }
9
80
 
10
81
  const __filename = fileURLToPath(import.meta.url);
11
82
  const __dirname = path.dirname(__filename);
@@ -23,11 +94,16 @@ class ProjectInitiator {
23
94
  constructor(projectRoot = null) {
24
95
  this.projectRoot = projectRoot || process.cwd();
25
96
  this.avcDir = path.join(this.projectRoot, '.avc');
97
+ this.srcDir = path.join(this.projectRoot, 'src');
98
+ this.worktreesDir = path.join(this.avcDir, 'worktrees');
26
99
  this.avcConfigPath = path.join(this.avcDir, 'avc.json');
27
100
  // Progress files are ceremony-specific
28
101
  this.initProgressPath = path.join(this.avcDir, 'init-progress.json');
29
102
  this.sponsorCallProgressPath = path.join(this.avcDir, 'sponsor-call-progress.json');
30
103
 
104
+ // Template processor for token usage tracking
105
+ this._lastTemplateProcessor = null;
106
+
31
107
  // Load environment variables from project .env file
32
108
  // Use override: true to reload even if already set (user may have edited .env)
33
109
  dotenv.config({
@@ -103,11 +179,45 @@ class ProjectInitiator {
103
179
  */
104
180
  createAvcFolder() {
105
181
  if (!this.hasAvcFolder()) {
106
- fs.mkdirSync(this.avcDir, { recursive: true });
107
- console.log('✓ Created .avc/ folder');
182
+ mkdirp(this.avcDir);
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+
188
+ /**
189
+ * Check if src folder exists
190
+ */
191
+ hasSrcFolder() {
192
+ return fs.existsSync(this.srcDir);
193
+ }
194
+
195
+ /**
196
+ * Create src folder for AVC-managed code
197
+ */
198
+ createSrcFolder() {
199
+ if (!this.hasSrcFolder()) {
200
+ mkdirp(this.srcDir);
201
+ return true;
202
+ }
203
+ return false;
204
+ }
205
+
206
+ /**
207
+ * Check if worktrees folder exists
208
+ */
209
+ hasWorktreesFolder() {
210
+ return fs.existsSync(this.worktreesDir);
211
+ }
212
+
213
+ /**
214
+ * Create worktrees folder for git worktree management
215
+ */
216
+ createWorktreesFolder() {
217
+ if (!this.hasWorktreesFolder()) {
218
+ mkdirp(this.worktreesDir);
108
219
  return true;
109
220
  }
110
- console.log('✓ .avc/ folder already exists');
111
221
  return false;
112
222
  }
113
223
 
@@ -123,7 +233,6 @@ class ProjectInitiator {
123
233
  framework: 'avc',
124
234
  created: new Date().toISOString(),
125
235
  settings: {
126
- contextScopes: ['epic', 'story', 'task', 'subtask'],
127
236
  workItemStatuses: ['ready', 'pending', 'implementing', 'implemented', 'testing', 'completed', 'blocked', 'feedback'],
128
237
  agentTypes: ['product-owner', 'server', 'client', 'infrastructure', 'testing'],
129
238
  documentation: {
@@ -132,20 +241,569 @@ class ProjectInitiator {
132
241
  ceremonies: [
133
242
  {
134
243
  name: 'sponsor-call',
135
- defaultModel: 'claude-sonnet-4-5-20250929',
136
244
  provider: 'claude',
245
+ defaultModel: 'claude-sonnet-4-6',
246
+ stages: {
247
+ suggestions: {
248
+ provider: 'claude',
249
+ model: 'claude-sonnet-4-6'
250
+ },
251
+ documentation: {
252
+ provider: 'claude',
253
+ model: 'claude-sonnet-4-6'
254
+ },
255
+ 'architecture-recommendation': {
256
+ provider: 'claude',
257
+ model: 'claude-sonnet-4-6'
258
+ },
259
+ 'question-prefilling': {
260
+ provider: 'claude',
261
+ model: 'claude-sonnet-4-6'
262
+ }
263
+ },
264
+ providerPresets: {
265
+ claude: {
266
+ provider: 'claude', defaultModel: 'claude-sonnet-4-6',
267
+ stages: {
268
+ suggestions: { provider: 'claude', model: 'claude-sonnet-4-6' },
269
+ documentation: { provider: 'claude', model: 'claude-sonnet-4-6' },
270
+ 'architecture-recommendation': { provider: 'claude', model: 'claude-opus-4-6' },
271
+ 'question-prefilling': { provider: 'claude', model: 'claude-haiku-4-5-20251001' }
272
+ },
273
+ validation: {
274
+ provider: 'claude', model: 'claude-haiku-4-5-20251001',
275
+ documentation: { provider: 'claude', model: 'claude-haiku-4-5-20251001' },
276
+ refinement: { provider: 'claude', model: 'claude-sonnet-4-6' }
277
+ }
278
+ },
279
+ gemini: {
280
+ provider: 'gemini', defaultModel: 'gemini-2.5-flash',
281
+ stages: {
282
+ suggestions: { provider: 'gemini', model: 'gemini-2.5-flash' },
283
+ documentation: { provider: 'gemini', model: 'gemini-2.5-flash' },
284
+ 'architecture-recommendation': { provider: 'gemini', model: 'gemini-2.5-pro' },
285
+ 'question-prefilling': { provider: 'gemini', model: 'gemini-2.5-flash-lite' }
286
+ },
287
+ validation: {
288
+ provider: 'gemini', model: 'gemini-2.5-flash-lite',
289
+ documentation: { provider: 'gemini', model: 'gemini-2.5-flash-lite' },
290
+ refinement: { provider: 'gemini', model: 'gemini-2.5-flash' }
291
+ }
292
+ },
293
+ openai: {
294
+ provider: 'openai', defaultModel: 'gpt-5.4',
295
+ stages: {
296
+ suggestions: { provider: 'openai', model: 'gpt-5.4' },
297
+ documentation: { provider: 'openai', model: 'gpt-5.4' },
298
+ 'architecture-recommendation': { provider: 'openai', model: 'gpt-5.4' },
299
+ 'question-prefilling': { provider: 'openai', model: 'gpt-5-mini' }
300
+ },
301
+ validation: {
302
+ provider: 'openai', model: 'gpt-5.4',
303
+ documentation: { provider: 'openai', model: 'gpt-5.4' },
304
+ refinement: { provider: 'openai', model: 'gpt-5.4' }
305
+ }
306
+ },
307
+ xiaomi: {
308
+ provider: 'xiaomi', defaultModel: 'mimo-v2-pro',
309
+ stages: {
310
+ suggestions: { provider: 'xiaomi', model: 'mimo-v2-flash' },
311
+ documentation: { provider: 'xiaomi', model: 'mimo-v2-pro' },
312
+ 'architecture-recommendation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
313
+ 'question-prefilling': { provider: 'xiaomi', model: 'mimo-v2-pro' }
314
+ },
315
+ validation: {
316
+ provider: 'xiaomi', model: 'mimo-v2-flash',
317
+ documentation: { provider: 'xiaomi', model: 'mimo-v2-pro' },
318
+ refinement: { provider: 'xiaomi', model: 'mimo-v2-pro' }
319
+ }
320
+ }
321
+ },
137
322
  agents: [
138
323
  {
139
- name: 'documentation',
140
- instruction: 'documentation.md',
141
- stage: 'enhancement'
142
- }
324
+ name: 'project-documentation-creator',
325
+ instruction: 'project-documentation-creator.md',
326
+ stage: 'documentation-generation'
327
+ },
328
+ {
329
+ name: 'validator-documentation',
330
+ instruction: 'validator-documentation.md',
331
+ stage: 'documentation-validation',
332
+ group: 'validators'
333
+ },
143
334
  ],
144
- guidelines: {
145
- technicalConsiderations: 'Use AWS serverless stack with Lambda functions for compute, API Gateway for REST APIs, DynamoDB for database, S3 for storage. Use CloudFormation for infrastructure definition and AWS CodePipeline/CodeBuild for CI/CD deployment.'
335
+ validation: {
336
+ enabled: true,
337
+ maxIterations: 100,
338
+ acceptanceThreshold: 95,
339
+ skipOnCriticalIssues: false,
340
+ provider: 'claude',
341
+ model: 'claude-haiku-4-5-20251001',
342
+ documentation: {
343
+ provider: 'claude',
344
+ model: 'claude-haiku-4-5-20251001'
345
+ },
346
+ refinement: {
347
+ provider: 'claude',
348
+ model: 'claude-sonnet-4-6'
349
+ }
350
+ },
351
+ crossValidation: {
352
+ enabled: true,
353
+ maxIterations: 3
146
354
  }
355
+ },
356
+ {
357
+ name: 'sprint-planning',
358
+ provider: 'claude',
359
+ defaultModel: 'claude-sonnet-4-6',
360
+ stages: {
361
+ decomposition: {
362
+ provider: 'claude',
363
+ model: 'claude-opus-4-6'
364
+ },
365
+ validation: {
366
+ provider: 'claude',
367
+ model: 'claude-sonnet-4-6',
368
+ useContextualSelection: true,
369
+ epicConcurrency: 2,
370
+ concurrency: 5,
371
+ maxFixAttempts: 3
372
+ },
373
+ 'context-generation': {
374
+ provider: 'claude',
375
+ model: 'claude-sonnet-4-6'
376
+ },
377
+ 'doc-generation': {
378
+ provider: 'claude',
379
+ model: 'claude-sonnet-4-6'
380
+ },
381
+ enrichment: {
382
+ provider: 'claude',
383
+ model: 'claude-sonnet-4-6'
384
+ }
385
+ },
386
+ providerPresets: {
387
+ claude: {
388
+ provider: 'claude', defaultModel: 'claude-sonnet-4-6',
389
+ stages: {
390
+ decomposition: { provider: 'claude', model: 'claude-opus-4-6' },
391
+ validation: { provider: 'claude', model: 'claude-sonnet-4-6', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, maxFixAttempts: 3 },
392
+ 'context-generation': { provider: 'claude', model: 'claude-sonnet-4-6' },
393
+ 'doc-generation': { provider: 'claude', model: 'claude-sonnet-4-6' },
394
+ enrichment: { provider: 'claude', model: 'claude-sonnet-4-6' }
395
+ }
396
+ },
397
+ gemini: {
398
+ provider: 'gemini', defaultModel: 'gemini-2.5-flash',
399
+ stages: {
400
+ decomposition: { provider: 'gemini', model: 'gemini-2.5-pro' },
401
+ validation: { provider: 'gemini', model: 'gemini-2.5-flash', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, maxFixAttempts: 3 },
402
+ 'context-generation': { provider: 'gemini', model: 'gemini-2.5-flash' },
403
+ 'doc-generation': { provider: 'gemini', model: 'gemini-2.5-flash' },
404
+ enrichment: { provider: 'gemini', model: 'gemini-2.5-flash' }
405
+ }
406
+ },
407
+ openai: {
408
+ provider: 'openai', defaultModel: 'gpt-5.4',
409
+ stages: {
410
+ decomposition: { provider: 'openai', model: 'gpt-5.4' },
411
+ validation: { provider: 'openai', model: 'gpt-5.4', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, maxFixAttempts: 3 },
412
+ 'context-generation': { provider: 'openai', model: 'gpt-5.4' },
413
+ 'doc-generation': { provider: 'openai', model: 'gpt-5.4' },
414
+ enrichment: { provider: 'openai', model: 'gpt-5.4' }
415
+ }
416
+ },
417
+ xiaomi: {
418
+ provider: 'xiaomi', defaultModel: 'mimo-v2-pro',
419
+ stages: {
420
+ decomposition: { provider: 'xiaomi', model: 'mimo-v2-pro' },
421
+ validation: { provider: 'xiaomi', model: 'mimo-v2-flash', useContextualSelection: true, epicConcurrency: 2, concurrency: 5, maxFixAttempts: 3 },
422
+ 'context-generation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
423
+ 'doc-generation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
424
+ enrichment: { provider: 'xiaomi', model: 'mimo-v2-pro' }
425
+ }
426
+ }
427
+ },
428
+ agents: [
429
+ {
430
+ name: 'epic-story-decomposer',
431
+ instruction: 'epic-story-decomposer.md',
432
+ stage: 'decomposition'
433
+ },
434
+ {
435
+ name: 'project-context-extractor',
436
+ instruction: 'project-context-extractor.md',
437
+ stage: 'validation'
438
+ },
439
+ {
440
+ name: 'agent-selector',
441
+ instruction: 'agent-selector.md',
442
+ stage: 'validation'
443
+ },
444
+ {
445
+ name: 'context-writer-epic',
446
+ instruction: 'context-writer-epic.md',
447
+ stage: 'context-generation'
448
+ },
449
+ {
450
+ name: 'context-writer-story',
451
+ instruction: 'context-writer-story.md',
452
+ stage: 'context-generation'
453
+ },
454
+ {
455
+ name: 'doc-writer-epic',
456
+ instruction: 'doc-writer-epic.md',
457
+ stage: 'doc-generation'
458
+ },
459
+ {
460
+ name: 'doc-writer-story',
461
+ instruction: 'doc-writer-story.md',
462
+ stage: 'doc-generation'
463
+ },
464
+ {
465
+ name: 'story-doc-enricher',
466
+ instruction: 'story-doc-enricher.md',
467
+ stage: 'enrichment'
468
+ }
469
+ ]
470
+ },
471
+ {
472
+ name: 'seed',
473
+ provider: 'claude',
474
+ defaultModel: 'claude-sonnet-4-6',
475
+ stages: {
476
+ decomposition: {
477
+ provider: 'claude',
478
+ model: 'claude-opus-4-6'
479
+ },
480
+ 'context-generation': {
481
+ provider: 'claude',
482
+ model: 'claude-sonnet-4-6'
483
+ }
484
+ },
485
+ providerPresets: {
486
+ claude: {
487
+ provider: 'claude', defaultModel: 'claude-sonnet-4-6',
488
+ stages: {
489
+ decomposition: { provider: 'claude', model: 'claude-opus-4-6' },
490
+ 'context-generation': { provider: 'claude', model: 'claude-sonnet-4-6' }
491
+ }
492
+ },
493
+ gemini: {
494
+ provider: 'gemini', defaultModel: 'gemini-2.5-flash',
495
+ stages: {
496
+ decomposition: { provider: 'gemini', model: 'gemini-2.5-pro' },
497
+ 'context-generation': { provider: 'gemini', model: 'gemini-2.5-flash' }
498
+ }
499
+ },
500
+ openai: {
501
+ provider: 'openai', defaultModel: 'gpt-5.4',
502
+ stages: {
503
+ decomposition: { provider: 'openai', model: 'gpt-5.4' },
504
+ 'context-generation': { provider: 'openai', model: 'gpt-5.4' }
505
+ }
506
+ },
507
+ xiaomi: {
508
+ provider: 'xiaomi', defaultModel: 'mimo-v2-pro',
509
+ stages: {
510
+ decomposition: { provider: 'xiaomi', model: 'mimo-v2-pro' },
511
+ 'context-generation': { provider: 'xiaomi', model: 'mimo-v2-pro' }
512
+ }
513
+ },
514
+ local: {
515
+ provider: 'local', defaultModel: 'qwen/qwen3-coder-next',
516
+ stages: {
517
+ decomposition: { provider: 'local', model: 'qwen/qwen3-coder-next' },
518
+ 'context-generation': { provider: 'local', model: 'qwen/qwen3-coder-next' }
519
+ }
520
+ }
521
+ },
522
+ agents: [
523
+ {
524
+ name: 'task-subtask-decomposer',
525
+ instruction: 'task-subtask-decomposer.md',
526
+ stage: 'decomposition'
527
+ },
528
+ {
529
+ name: 'doc-distributor',
530
+ instruction: 'doc-distributor.md',
531
+ stage: 'doc-distribution'
532
+ },
533
+ {
534
+ name: 'feature-context-generator',
535
+ instruction: 'feature-context-generator.md',
536
+ stage: 'decomposition'
537
+ }
538
+ ]
539
+ },
540
+ {
541
+ name: 'run',
542
+ provider: 'claude',
543
+ defaultModel: 'claude-sonnet-4-6',
544
+ stages: {
545
+ 'code-generation': {
546
+ provider: 'claude',
547
+ model: 'claude-sonnet-4-6'
548
+ },
549
+ 'code-validation': {
550
+ provider: 'claude',
551
+ model: 'claude-sonnet-4-6'
552
+ }
553
+ },
554
+ providerPresets: {
555
+ claude: {
556
+ provider: 'claude', defaultModel: 'claude-sonnet-4-6',
557
+ stages: {
558
+ 'code-generation': { provider: 'claude', model: 'claude-sonnet-4-6' },
559
+ 'code-validation': { provider: 'claude', model: 'claude-sonnet-4-6' }
560
+ }
561
+ },
562
+ gemini: {
563
+ provider: 'gemini', defaultModel: 'gemini-2.5-flash',
564
+ stages: {
565
+ 'code-generation': { provider: 'gemini', model: 'gemini-2.5-flash' },
566
+ 'code-validation': { provider: 'gemini', model: 'gemini-2.5-flash' }
567
+ }
568
+ },
569
+ openai: {
570
+ provider: 'openai', defaultModel: 'gpt-5.4',
571
+ stages: {
572
+ 'code-generation': { provider: 'openai', model: 'gpt-5.4' },
573
+ 'code-validation': { provider: 'openai', model: 'gpt-5.4' }
574
+ }
575
+ },
576
+ xiaomi: {
577
+ provider: 'xiaomi', defaultModel: 'mimo-v2-pro',
578
+ stages: {
579
+ 'code-generation': { provider: 'xiaomi', model: 'mimo-v2-pro' },
580
+ 'code-validation': { provider: 'xiaomi', model: 'mimo-v2-flash' }
581
+ }
582
+ },
583
+ local: {
584
+ provider: 'local', defaultModel: 'qwen/qwen3-coder-next',
585
+ stages: {
586
+ 'code-generation': { provider: 'local', model: 'qwen/qwen3-coder-next' },
587
+ 'code-validation': { provider: 'local', model: 'qwen/qwen3-coder-next' }
588
+ }
589
+ }
590
+ },
591
+ maxValidationIterations: 3,
592
+ acceptanceThreshold: 80,
593
+ agents: [
594
+ {
595
+ name: 'code-implementer',
596
+ instruction: 'code-implementer.md',
597
+ stage: 'code-generation'
598
+ },
599
+ {
600
+ name: 'code-validator',
601
+ instruction: 'code-validator.md',
602
+ stage: 'code-validation'
603
+ }
604
+ ]
147
605
  }
148
- ]
606
+ ],
607
+ missionGenerator: {
608
+ validation: {
609
+ maxIterations: 3,
610
+ acceptanceThreshold: 95
611
+ }
612
+ },
613
+ costThresholds: {
614
+ 'sponsor-call': 2,
615
+ 'sprint-planning': 2,
616
+ 'seed': 2,
617
+ 'run': 2
618
+ },
619
+ questionnaire: {
620
+ defaults: {
621
+ MISSION_STATEMENT: null,
622
+ TARGET_USERS: null,
623
+ INITIAL_SCOPE: null,
624
+ DEPLOYMENT_TARGET: null,
625
+ TECHNICAL_CONSIDERATIONS: null,
626
+ SECURITY_AND_COMPLIANCE_REQUIREMENTS: null
627
+ }
628
+ },
629
+ models: {
630
+ // Anthropic Claude models (prices per 1M tokens in USD)
631
+ // Source: https://www.anthropic.com/pricing
632
+ 'claude-opus-4-6': {
633
+ provider: 'claude',
634
+ displayName: 'Claude Opus 4.6',
635
+ pricing: {
636
+ input: 5.00,
637
+ output: 25.00,
638
+ unit: 'million',
639
+ source: 'https://www.anthropic.com/pricing',
640
+ lastUpdated: '2026-02-24'
641
+ }
642
+ },
643
+ 'claude-sonnet-4-6': {
644
+ provider: 'claude',
645
+ displayName: 'Claude Sonnet 4.6',
646
+ pricing: {
647
+ input: 3.00,
648
+ output: 15.00,
649
+ unit: 'million',
650
+ source: 'https://www.anthropic.com/pricing',
651
+ lastUpdated: '2026-02-24'
652
+ }
653
+ },
654
+ 'claude-haiku-4-5-20251001': {
655
+ provider: 'claude',
656
+ displayName: 'Claude Haiku 4.5',
657
+ pricing: {
658
+ input: 1.00,
659
+ output: 5.00,
660
+ unit: 'million',
661
+ source: 'https://www.anthropic.com/pricing',
662
+ lastUpdated: '2026-02-24'
663
+ }
664
+ },
665
+
666
+ // Google Gemini models (prices per 1M tokens in USD)
667
+ // Source: https://ai.google.dev/pricing
668
+ 'gemini-3.1-pro-preview': {
669
+ provider: 'gemini',
670
+ displayName: 'Gemini 3.1 Pro Preview',
671
+ pricing: {
672
+ input: 2.00,
673
+ output: 12.00,
674
+ unit: 'million',
675
+ source: 'https://ai.google.dev/pricing',
676
+ lastUpdated: '2026-03-05'
677
+ }
678
+ },
679
+ 'gemini-3-flash-preview': {
680
+ provider: 'gemini',
681
+ displayName: 'Gemini 3 Flash Preview',
682
+ pricing: {
683
+ input: 0.50,
684
+ output: 3.00,
685
+ unit: 'million',
686
+ source: 'https://ai.google.dev/pricing',
687
+ lastUpdated: '2026-03-05'
688
+ }
689
+ },
690
+ 'gemini-2.5-pro': {
691
+ provider: 'gemini',
692
+ displayName: 'Gemini 2.5 Pro',
693
+ pricing: {
694
+ input: 1.25,
695
+ output: 10.00,
696
+ unit: 'million',
697
+ source: 'https://ai.google.dev/pricing',
698
+ lastUpdated: '2026-02-24'
699
+ }
700
+ },
701
+ 'gemini-2.5-flash': {
702
+ provider: 'gemini',
703
+ displayName: 'Gemini 2.5 Flash',
704
+ pricing: {
705
+ input: 0.30,
706
+ output: 2.50,
707
+ unit: 'million',
708
+ source: 'https://ai.google.dev/pricing',
709
+ lastUpdated: '2026-02-24'
710
+ }
711
+ },
712
+ 'gemini-2.5-flash-lite': {
713
+ provider: 'gemini',
714
+ displayName: 'Gemini 2.5 Flash-Lite',
715
+ pricing: {
716
+ input: 0.10,
717
+ output: 0.40,
718
+ unit: 'million',
719
+ source: 'https://ai.google.dev/pricing',
720
+ lastUpdated: '2026-02-24'
721
+ }
722
+ },
723
+
724
+ // OpenAI models (prices per 1M tokens in USD)
725
+ // Source: https://openai.com/api/pricing
726
+ 'gpt-5.4': {
727
+ provider: 'openai',
728
+ displayName: 'GPT-5.4',
729
+ pricing: {
730
+ input: 2.50,
731
+ output: 15.00,
732
+ unit: 'million',
733
+ source: 'https://openai.com/api/pricing',
734
+ lastUpdated: '2026-03-06'
735
+ }
736
+ },
737
+ 'gpt-5.4-pro': {
738
+ provider: 'openai',
739
+ displayName: 'GPT-5.4 Pro',
740
+ pricing: {
741
+ input: 30.00,
742
+ output: 180.00,
743
+ unit: 'million',
744
+ source: 'https://openai.com/api/pricing',
745
+ lastUpdated: '2026-03-06'
746
+ }
747
+ },
748
+ 'gpt-5-mini': {
749
+ provider: 'openai',
750
+ displayName: 'GPT-5 mini',
751
+ pricing: {
752
+ input: 0.25,
753
+ output: 2.00,
754
+ unit: 'million',
755
+ source: 'https://openai.com/api/pricing',
756
+ lastUpdated: '2026-02-24'
757
+ }
758
+ },
759
+ 'gpt-5-nano': {
760
+ provider: 'openai',
761
+ displayName: 'GPT-5 nano',
762
+ pricing: {
763
+ input: 0.05,
764
+ output: 0.40,
765
+ unit: 'million',
766
+ source: 'https://openai.com/api/pricing',
767
+ lastUpdated: '2026-03-06'
768
+ }
769
+ },
770
+
771
+ // Xiaomi MiMo models (prices per 1M tokens in USD)
772
+ // Source: https://platform.xiaomimimo.com
773
+ 'mimo-v2-flash': {
774
+ provider: 'xiaomi',
775
+ displayName: 'MiMo V2 Flash',
776
+ pricing: {
777
+ input: 0.09,
778
+ output: 0.29,
779
+ unit: 'million',
780
+ source: 'https://platform.xiaomimimo.com',
781
+ lastUpdated: '2026-03-25'
782
+ }
783
+ },
784
+ 'mimo-v2-pro': {
785
+ provider: 'xiaomi',
786
+ displayName: 'MiMo V2 Pro',
787
+ pricing: {
788
+ input: 1.00,
789
+ output: 3.00,
790
+ unit: 'million',
791
+ source: 'https://platform.xiaomimimo.com',
792
+ lastUpdated: '2026-03-25'
793
+ }
794
+ },
795
+ 'mimo-v2-omni': {
796
+ provider: 'xiaomi',
797
+ displayName: 'MiMo V2 Omni',
798
+ pricing: {
799
+ input: 0.40,
800
+ output: 2.00,
801
+ unit: 'million',
802
+ source: 'https://platform.xiaomimimo.com',
803
+ lastUpdated: '2026-03-25'
804
+ }
805
+ },
806
+ }
149
807
  }
150
808
  };
151
809
 
@@ -156,7 +814,6 @@ class ProjectInitiator {
156
814
  JSON.stringify(defaultConfig, null, 2),
157
815
  'utf8'
158
816
  );
159
- console.log('✓ Created .avc/avc.json configuration file');
160
817
  return true;
161
818
  }
162
819
 
@@ -167,6 +824,15 @@ class ProjectInitiator {
167
824
  // Merge: add new keys, keep existing values
168
825
  const mergedConfig = this.deepMerge(existingConfig, defaultConfig);
169
826
 
827
+ // Upgrade null cost thresholds to default (2 USD)
828
+ if (mergedConfig.settings?.costThresholds) {
829
+ for (const ceremony of ['sponsor-call', 'sprint-planning', 'seed', 'run']) {
830
+ if (mergedConfig.settings.costThresholds[ceremony] === null) {
831
+ mergedConfig.settings.costThresholds[ceremony] = 2;
832
+ }
833
+ }
834
+ }
835
+
170
836
  // Update avcVersion to track CLI version
171
837
  mergedConfig.avcVersion = this.getAvcVersion();
172
838
  mergedConfig.updated = new Date().toISOString();
@@ -177,15 +843,12 @@ class ProjectInitiator {
177
843
 
178
844
  if (existingJson !== mergedJson) {
179
845
  fs.writeFileSync(this.avcConfigPath, mergedJson, 'utf8');
180
- console.log('✓ Updated .avc/avc.json with new configuration attributes');
181
846
  return true;
182
847
  }
183
848
 
184
- console.log('✓ .avc/avc.json is up to date');
185
849
  return false;
186
850
  } catch (error) {
187
- console.error(`⚠️ Warning: Could not merge avc.json: ${error.message}`);
188
- console.log('✓ .avc/avc.json already exists (merge skipped)');
851
+ console.error(`Warning: Could not merge avc.json: ${error.message}`);
189
852
  return false;
190
853
  }
191
854
  }
@@ -204,24 +867,75 @@ class ProjectInitiator {
204
867
 
205
868
  /**
206
869
  * Create .env file for API keys
870
+ * If .env exists, check and add any missing API key variables
207
871
  */
208
872
  createEnvFile() {
209
873
  const envPath = path.join(this.projectRoot, '.env');
210
874
 
211
- if (!fs.existsSync(envPath)) {
212
- const envContent = `# Anthropic API Key for AI-powered Sponsor Call ceremony
213
- # Get your key at: https://console.anthropic.com/settings/keys
214
- ANTHROPIC_API_KEY=
875
+ // Define required API key variables with metadata
876
+ const requiredApiKeys = [
877
+ {
878
+ key: 'ANTHROPIC_API_KEY',
879
+ comment: 'Anthropic API Key for AI-powered Sponsor Call ceremony',
880
+ url: 'https://console.anthropic.com/settings/keys'
881
+ },
882
+ {
883
+ key: 'GEMINI_API_KEY',
884
+ comment: 'Google Gemini API Key (alternative LLM provider)',
885
+ url: 'https://aistudio.google.com/app/apikey'
886
+ },
887
+ {
888
+ key: 'OPENAI_API_KEY',
889
+ comment: 'OpenAI API Key (alternative LLM provider)',
890
+ url: 'https://platform.openai.com/api-keys'
891
+ },
892
+ {
893
+ key: 'XIAOMI_API_KEY',
894
+ comment: 'Xiaomi MiMo API Key (alternative LLM provider)',
895
+ url: 'https://platform.xiaomimimo.com/#/console/api-keys'
896
+ }
897
+ ];
215
898
 
216
- # Google Gemini API Key (alternative LLM provider)
217
- # Get your key at: https://aistudio.google.com/app/apikey
218
- GEMINI_API_KEY=
219
- `;
899
+ if (!fs.existsSync(envPath)) {
900
+ // Create new .env file with all API keys
901
+ let envContent = '';
902
+ requiredApiKeys.forEach(({ key, comment, url }, index) => {
903
+ if (index > 0) envContent += '\n';
904
+ envContent += `# ${comment}\n`;
905
+ envContent += `# Get your key at: ${url}\n`;
906
+ envContent += `${key}=\n`;
907
+ });
220
908
  fs.writeFileSync(envPath, envContent, 'utf8');
221
- console.log('✓ Created .env file for API keys');
222
909
  return true;
223
910
  }
224
- console.log('✓ .env file already exists');
911
+
912
+ // .env exists - check for missing API keys
913
+ const existingContent = fs.readFileSync(envPath, 'utf8');
914
+ const missingKeys = [];
915
+
916
+ // Check which API keys are missing
917
+ requiredApiKeys.forEach(({ key }) => {
918
+ const keyPattern = new RegExp(`^${key}=`, 'm');
919
+ if (!keyPattern.test(existingContent)) {
920
+ missingKeys.push(key);
921
+ }
922
+ });
923
+
924
+ if (missingKeys.length > 0) {
925
+ // Add missing API keys to .env file
926
+ let appendContent = '\n';
927
+ requiredApiKeys.forEach(({ key, comment, url }) => {
928
+ if (missingKeys.includes(key)) {
929
+ appendContent += `\n# ${comment}\n`;
930
+ appendContent += `# Get your key at: ${url}\n`;
931
+ appendContent += `${key}=\n`;
932
+ }
933
+ });
934
+
935
+ fs.appendFileSync(envPath, appendContent, 'utf8');
936
+ return true;
937
+ }
938
+
225
939
  return false;
226
940
  }
227
941
 
@@ -245,7 +959,9 @@ GEMINI_API_KEY=
245
959
  { pattern: '.env', comment: 'Environment variables' },
246
960
  { pattern: '.avc/documentation/.vitepress/dist', comment: 'VitePress build output' },
247
961
  { pattern: '.avc/documentation/.vitepress/cache', comment: 'VitePress cache' },
248
- { pattern: '.avc/logs', comment: 'Command execution logs' }
962
+ { pattern: '.avc/logs', comment: 'Command execution logs' },
963
+ { pattern: '.avc/token-history.json', comment: 'Token usage tracking' },
964
+ { pattern: '.avc/ceremonies-history.json', comment: 'Ceremony execution history' }
249
965
  ];
250
966
 
251
967
  let newContent = gitignoreContent;
@@ -266,33 +982,25 @@ GEMINI_API_KEY=
266
982
 
267
983
  if (addedItems.length > 0) {
268
984
  fs.writeFileSync(gitignorePath, newContent, 'utf8');
269
- console.log(`✓ Added to .gitignore: ${addedItems.join(', ')}`);
270
- } else {
271
- console.log('✓ .gitignore already up to date');
272
985
  }
273
986
  }
274
987
 
275
988
  /**
276
- * Create VitePress documentation setup
989
+ * Create VitePress documentation structure (folders and config files)
990
+ * Note: VitePress is bundled with AVC, no need to modify user's package.json
277
991
  */
278
- createVitePressSetup() {
992
+ createVitePressStructure() {
279
993
  const docsDir = path.join(this.avcDir, 'documentation');
280
994
  const vitepressDir = path.join(docsDir, '.vitepress');
281
995
  const publicDir = path.join(docsDir, 'public');
282
996
 
283
997
  // Create directory structure
284
998
  if (!fs.existsSync(vitepressDir)) {
285
- fs.mkdirSync(vitepressDir, { recursive: true });
286
- console.log('✓ Created .avc/documentation/.vitepress/ folder');
287
- } else {
288
- console.log('✓ .avc/documentation/.vitepress/ folder already exists');
999
+ mkdirp(vitepressDir);
289
1000
  }
290
1001
 
291
1002
  if (!fs.existsSync(publicDir)) {
292
- fs.mkdirSync(publicDir, { recursive: true });
293
- console.log('✓ Created .avc/documentation/public/ folder');
294
- } else {
295
- console.log('✓ .avc/documentation/public/ folder already exists');
1003
+ mkdirp(publicDir);
296
1004
  }
297
1005
 
298
1006
  // Create VitePress config
@@ -302,9 +1010,6 @@ GEMINI_API_KEY=
302
1010
  let configContent = fs.readFileSync(templatePath, 'utf8');
303
1011
  configContent = configContent.replace('{{PROJECT_NAME}}', this.getProjectName());
304
1012
  fs.writeFileSync(configPath, configContent, 'utf8');
305
- console.log('✓ Created .avc/documentation/.vitepress/config.mts');
306
- } else {
307
- console.log('✓ .avc/documentation/.vitepress/config.mts already exists');
308
1013
  }
309
1014
 
310
1015
  // Create initial index.md
@@ -312,107 +1017,9 @@ GEMINI_API_KEY=
312
1017
  if (!fs.existsSync(indexPath)) {
313
1018
  const indexContent = `# ${this.getProjectName()}
314
1019
 
315
- ## Project Status
316
-
317
- This project is being developed using the [Agile Vibe Coding](https://agilevibecoding.org) framework.
318
-
319
- **Current Stage**: Initial Setup
320
-
321
- Project documentation will be generated automatically as the project is defined and developed.
322
-
323
- ## About This Documentation
324
-
325
- This site provides comprehensive documentation about **${this.getProjectName()}**, including:
326
-
327
- - Project overview and objectives
328
- - Feature specifications organized by epics and stories
329
- - Technical architecture and design decisions
330
- - Implementation progress and status
331
-
332
- Documentation is automatically updated from the AVC project structure as development progresses.
333
-
334
- ## Getting Started with AVC
335
-
336
- If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilevibecoding.org) to learn about:
337
-
338
- - [CLI Commands](https://agilevibecoding.org/commands) - Available commands and their usage
339
- - [Installation Guide](https://agilevibecoding.org/install) - Setup instructions
340
- - [Framework Overview](https://agilevibecoding.org) - Core concepts and workflow
341
-
342
- ---
343
-
344
- *Documentation powered by [Agile Vibe Coding](https://agilevibecoding.org)*
1020
+ Documentation for this project will be generated automatically once the project is defined via the Sponsor Call ceremony. Use the **Start Project** button in the [Kanban board](http://localhost:4174) to get started.
345
1021
  `;
346
1022
  fs.writeFileSync(indexPath, indexContent, 'utf8');
347
- console.log('✓ Created .avc/documentation/index.md');
348
- } else {
349
- console.log('✓ .avc/documentation/index.md already exists');
350
- }
351
-
352
- // Update package.json with VitePress scripts
353
- this.updatePackageJsonForVitePress();
354
- }
355
-
356
- /**
357
- * Update package.json with VitePress dependencies and scripts
358
- */
359
- updatePackageJsonForVitePress() {
360
- const packagePath = path.join(this.projectRoot, 'package.json');
361
-
362
- let packageJson;
363
- if (fs.existsSync(packagePath)) {
364
- packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
365
- } else {
366
- packageJson = {
367
- name: this.getProjectName(),
368
- version: '1.0.0',
369
- private: true
370
- };
371
- }
372
-
373
- // Add scripts
374
- if (!packageJson.scripts) {
375
- packageJson.scripts = {};
376
- }
377
-
378
- const scriptsToAdd = {
379
- 'docs:dev': 'vitepress dev .avc/documentation',
380
- 'docs:build': 'vitepress build .avc/documentation',
381
- 'docs:preview': 'vitepress preview .avc/documentation'
382
- };
383
-
384
- let addedScripts = [];
385
- for (const [name, command] of Object.entries(scriptsToAdd)) {
386
- if (!packageJson.scripts[name]) {
387
- packageJson.scripts[name] = command;
388
- addedScripts.push(name);
389
- }
390
- }
391
-
392
- // Add devDependencies
393
- if (!packageJson.devDependencies) {
394
- packageJson.devDependencies = {};
395
- }
396
-
397
- let addedDeps = false;
398
- if (!packageJson.devDependencies.vitepress) {
399
- packageJson.devDependencies.vitepress = '^1.6.4';
400
- addedDeps = true;
401
- }
402
-
403
- // Write package.json
404
- fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
405
-
406
- if (addedScripts.length > 0 || addedDeps) {
407
- console.log('✓ Updated package.json with VitePress configuration');
408
- if (addedScripts.length > 0) {
409
- console.log(` Added scripts: ${addedScripts.join(', ')}`);
410
- }
411
- if (addedDeps) {
412
- console.log(' Added devDependency: vitepress');
413
- }
414
- } else {
415
- console.log('✓ package.json already has VitePress configuration');
416
1023
  }
417
1024
  }
418
1025
 
@@ -440,7 +1047,7 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
440
1047
  */
441
1048
  writeProgress(progress, progressPath) {
442
1049
  if (!fs.existsSync(this.avcDir)) {
443
- fs.mkdirSync(this.avcDir, { recursive: true });
1050
+ mkdirp(this.avcDir);
444
1051
  }
445
1052
  fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf8');
446
1053
  }
@@ -455,6 +1062,74 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
455
1062
  }
456
1063
 
457
1064
 
1065
+ /**
1066
+ * Parse and simplify API error messages for better UX
1067
+ * @param {string|object} error - Error from API call
1068
+ * @returns {string} - Human-readable error message
1069
+ */
1070
+ parseApiError(error) {
1071
+ let errorStr = typeof error === 'string' ? error : JSON.stringify(error);
1072
+
1073
+ // Try to parse as JSON to extract meaningful information
1074
+ let errorObj = null;
1075
+ try {
1076
+ errorObj = typeof error === 'object' ? error : JSON.parse(error);
1077
+ } catch (e) {
1078
+ // Not JSON, use as-is
1079
+ }
1080
+
1081
+ // Check for common error patterns
1082
+ if (errorStr.includes('quota') || errorStr.includes('RESOURCE_EXHAUSTED')) {
1083
+ return 'API quota exceeded. You have reached your free tier limit.\n\n Please check your usage at the provider dashboard or upgrade your plan.';
1084
+ }
1085
+
1086
+ if (errorStr.includes('rate limit') || errorStr.includes('429')) {
1087
+ const retryMatch = errorStr.match(/retry.*?(\d+)\.?\d*s/i);
1088
+ const retryTime = retryMatch ? ` Try again in ${Math.ceil(parseFloat(retryMatch[1]))} seconds.` : '';
1089
+ return `Rate limit exceeded.${retryTime}\n\n Please wait before making more requests.`;
1090
+ }
1091
+
1092
+ if (errorStr.includes('401') || errorStr.includes('authentication') || errorStr.includes('unauthorized')) {
1093
+ return 'Invalid API key or authentication failed.\n\n Please verify your API key is correct.';
1094
+ }
1095
+
1096
+ if (errorStr.includes('403') || errorStr.includes('forbidden')) {
1097
+ return 'Access forbidden. Your API key may not have permission for this operation.\n\n Check your API key permissions or contact support.';
1098
+ }
1099
+
1100
+ if (errorStr.includes('404') || errorStr.includes('not found')) {
1101
+ return 'API endpoint not found. The model or API version may not be available.\n\n Check that you\'re using a valid model name.';
1102
+ }
1103
+
1104
+ if (errorStr.includes('timeout') || errorStr.includes('ETIMEDOUT')) {
1105
+ return 'Request timed out.\n\n Check your internet connection and try again.';
1106
+ }
1107
+
1108
+ if (errorStr.includes('ENOTFOUND') || errorStr.includes('DNS')) {
1109
+ return 'Network error: Could not reach API server.\n\n Check your internet connection.';
1110
+ }
1111
+
1112
+ // Extract just the error message if it's an object with a message field
1113
+ if (errorObj) {
1114
+ if (errorObj.error?.message) {
1115
+ // Take first line or first 150 characters of the message
1116
+ const msg = errorObj.error.message.split('\n')[0];
1117
+ return msg.length > 150 ? msg.substring(0, 150) + '...' : msg;
1118
+ }
1119
+ if (errorObj.message) {
1120
+ const msg = errorObj.message.split('\n')[0];
1121
+ return msg.length > 150 ? msg.substring(0, 150) + '...' : msg;
1122
+ }
1123
+ }
1124
+
1125
+ // Fallback: truncate the error if it's too long
1126
+ if (errorStr.length > 200) {
1127
+ return errorStr.substring(0, 200) + '...\n\n (Full error logged to console)';
1128
+ }
1129
+
1130
+ return errorStr;
1131
+ }
1132
+
458
1133
  /**
459
1134
  * Validate that the configured provider's API key is present and working
460
1135
  */
@@ -470,7 +1145,7 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
470
1145
  };
471
1146
  }
472
1147
 
473
- // Read provider config from avc.json
1148
+ // Read ceremony config
474
1149
  const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
475
1150
  const ceremony = config.settings?.ceremonies?.[0];
476
1151
 
@@ -481,62 +1156,291 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
481
1156
  };
482
1157
  }
483
1158
 
484
- const providerName = ceremony.provider || 'claude';
485
- const modelName = ceremony.defaultModel || 'claude-sonnet-4-5-20250929';
1159
+ const mainProvider = ceremony.provider || 'claude';
1160
+ const mainModel = ceremony.defaultModel || 'claude-sonnet-4-6';
1161
+
1162
+ // Check validation provider if validation is enabled
1163
+ const validationEnabled = ceremony.validation?.enabled !== false;
1164
+ const validationProvider = ceremony.validation?.provider || null;
1165
+ const validationModel = ceremony.validation?.model || null;
486
1166
 
487
- // Check which env var is required
488
1167
  const envVarMap = {
489
1168
  'claude': 'ANTHROPIC_API_KEY',
490
- 'gemini': 'GEMINI_API_KEY'
1169
+ 'gemini': 'GEMINI_API_KEY',
1170
+ 'openai': 'OPENAI_API_KEY',
1171
+ 'xiaomi': 'XIAOMI_API_KEY'
1172
+ };
1173
+
1174
+ const urlMap = {
1175
+ 'claude': 'https://console.anthropic.com/settings/keys',
1176
+ 'gemini': 'https://aistudio.google.com/app/apikey',
1177
+ 'openai': 'https://platform.openai.com/api-keys',
1178
+ 'xiaomi': 'https://platform.xiaomimimo.com/#/console/api-keys'
1179
+ };
1180
+
1181
+ // Check if a provider has any valid auth credential (API key OR oauth token)
1182
+ const hasProviderAuth = (provider) => {
1183
+ if (provider === 'openai') {
1184
+ const oauthFile = path.join(this.projectRoot, '.avc', 'openai-oauth.json');
1185
+ return !!(process.env.OPENAI_API_KEY ||
1186
+ (process.env.OPENAI_AUTH_MODE === 'oauth' && fs.existsSync(oauthFile)));
1187
+ }
1188
+ return !!process.env[envVarMap[provider]];
1189
+ };
1190
+
1191
+ const authLabel = (provider) => {
1192
+ if (provider === 'openai' && process.env.OPENAI_AUTH_MODE === 'oauth') return 'OpenAI OAuth token';
1193
+ return envVarMap[provider];
491
1194
  };
492
1195
 
493
- const requiredEnvVar = envVarMap[providerName];
494
- if (!requiredEnvVar) {
1196
+ // Local provider needs no API key — skip validation entirely
1197
+ if (mainProvider === 'local') {
1198
+ // Still validate the validation provider if it's a cloud provider
1199
+ if (validationEnabled && validationProvider && validationProvider !== 'local') {
1200
+ const validationEnvVar = envVarMap[validationProvider];
1201
+ if (!validationEnvVar) {
1202
+ return {
1203
+ valid: false,
1204
+ message: `Unknown validation provider "${validationProvider}".\n Supported providers: claude, gemini, openai, xiaomi, local`
1205
+ };
1206
+ }
1207
+ if (!hasProviderAuth(validationProvider)) {
1208
+ return {
1209
+ valid: false,
1210
+ validationProviderMissing: true,
1211
+ message: `${validationEnvVar} not found in .env file (needed for validation provider).`
1212
+ };
1213
+ }
1214
+ }
1215
+ return { valid: true };
1216
+ }
1217
+
1218
+ // Validate main provider
1219
+ const mainEnvVar = envVarMap[mainProvider];
1220
+ if (!mainEnvVar) {
495
1221
  return {
496
1222
  valid: false,
497
- message: `Unknown provider "${providerName}".\n Supported providers: claude, gemini`
1223
+ message: `Unknown provider "${mainProvider}".\n Supported providers: claude, gemini, openai, xiaomi, local`
498
1224
  };
499
1225
  }
500
1226
 
501
- // Check if API key is set in environment
502
- if (!process.env[requiredEnvVar]) {
1227
+ // Helper: switch ALL ceremonies in avc.json to the fallback provider/model.
1228
+ // Updates ceremony-level provider + defaultModel and every stage override.
1229
+ const switchCeremonyProvider = (resolved) => {
1230
+ try {
1231
+ const freshConfig = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
1232
+ for (const c of (freshConfig.settings?.ceremonies || [])) {
1233
+ const preset = c.providerPresets?.[resolved.provider];
1234
+ const fallbackModel = preset?.defaultModel || resolved.model;
1235
+
1236
+ c.provider = resolved.provider;
1237
+ c.defaultModel = fallbackModel;
1238
+
1239
+ // Apply preset stage config (model + extra props like concurrency)
1240
+ if (c.stages && typeof c.stages === 'object') {
1241
+ for (const stageName of Object.keys(c.stages)) {
1242
+ const presetStage = preset?.stages?.[stageName];
1243
+ if (presetStage) {
1244
+ Object.assign(c.stages[stageName], presetStage);
1245
+ } else {
1246
+ c.stages[stageName].provider = resolved.provider;
1247
+ c.stages[stageName].model = fallbackModel;
1248
+ }
1249
+ }
1250
+ }
1251
+
1252
+ // Apply preset validation models or fall back to default model
1253
+ if (c.validation && typeof c.validation === 'object') {
1254
+ const presetVal = preset?.validation;
1255
+ c.validation.provider = resolved.provider;
1256
+ c.validation.model = presetVal?.model || fallbackModel;
1257
+ for (const [k, v] of Object.entries(c.validation)) {
1258
+ if (v && typeof v === 'object' && typeof v.provider === 'string') {
1259
+ const presetSub = presetVal?.[k];
1260
+ v.provider = resolved.provider;
1261
+ v.model = presetSub?.model || presetVal?.model || fallbackModel;
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+ fs.writeFileSync(this.avcConfigPath, JSON.stringify(freshConfig, null, 2), 'utf8');
1267
+ fileLog('INFO', `Switched all ceremony models to ${resolved.provider} using presets in avc.json`);
1268
+ } catch (writeErr) {
1269
+ fileLog('WARN', `Could not persist provider switch: ${writeErr.message}`);
1270
+ }
1271
+ };
1272
+
1273
+ if (!hasProviderAuth(mainProvider)) {
1274
+ // Try to find any available fallback provider
1275
+ const resolved = await LLMProvider.resolveAvailableProvider(mainProvider, mainModel);
1276
+ if (resolved.fellBack) {
1277
+ fileLog('WARN', `Pre-ceremony fallback: ${mainProvider}→${resolved.provider} (no API key for ${mainProvider})`);
1278
+ switchCeremonyProvider(resolved);
1279
+ return { valid: true };
1280
+ }
503
1281
  return {
504
1282
  valid: false,
505
- message: `${requiredEnvVar} not found in .env file.\n\n Steps to fix:\n 1. Open .env file in the current directory\n 2. Add your API key: ${requiredEnvVar}=your-key-here\n 3. Save the file and run /init again\n\n Get your API key:\n ${providerName === 'claude' ? '• https://console.anthropic.com/settings/keys' : '• https://aistudio.google.com/app/apikey'}`
1283
+ message: `${mainEnvVar} not found in .env file.\n\n Steps to fix:\n 1. Open .env file in the current directory\n 2. Add your API key: ${mainEnvVar}=your-key-here\n 3. Save the file and run /sponsor-call again\n\n Get your API key:\n ${urlMap[mainProvider]}`
506
1284
  };
507
1285
  }
508
1286
 
509
- console.log(`\n🔑 Validating ${providerName} API key...`);
510
-
511
1287
  // Test the API key with a minimal call
512
1288
  let result;
513
1289
  try {
514
- result = await LLMProvider.validate(providerName, modelName);
1290
+ result = await LLMProvider.validate(mainProvider, mainModel);
515
1291
  } catch (error) {
1292
+ // API key exists but validation failed — try fallback
1293
+ const resolved = await LLMProvider.resolveAvailableProvider(mainProvider, mainModel);
1294
+ if (resolved.fellBack) {
1295
+ fileLog('WARN', `Pre-ceremony fallback after validation failure: ${mainProvider}→${resolved.provider}`);
1296
+ switchCeremonyProvider(resolved);
1297
+ return { valid: true };
1298
+ }
1299
+ const parsedError = this.parseApiError(error.message || error);
516
1300
  return {
517
1301
  valid: false,
518
- message: `${requiredEnvVar} validation failed.\n\n Error: ${error.message}\n\n This could be due to:\n • Network connectivity issues\n • API service temporarily unavailable\n • Invalid API key\n\n Please check your connection and try again.`
1302
+ message: `${mainEnvVar} validation failed.\n\n ${parsedError}\n\n Get a new API key if needed:\n • ${urlMap[mainProvider]}`
519
1303
  };
520
1304
  }
521
1305
 
522
1306
  if (!result.valid) {
523
- const errorMsg = result.error || 'Unknown error';
1307
+ // API call failed try fallback
1308
+ const resolved = await LLMProvider.resolveAvailableProvider(mainProvider, mainModel);
1309
+ if (resolved.fellBack) {
1310
+ fileLog('WARN', `Pre-ceremony fallback after API failure: ${mainProvider}→${resolved.provider}`);
1311
+ switchCeremonyProvider(resolved);
1312
+ return { valid: true };
1313
+ }
1314
+ const parsedError = this.parseApiError(result.error || 'Unknown error');
524
1315
  return {
525
1316
  valid: false,
526
- message: `${requiredEnvVar} is set but API call failed.\n\n Error: ${errorMsg}\n\n Steps to fix:\n 1. Verify your API key is correct in .env file\n 2. Check that the key has not expired\n 3. Ensure you have API credits/quota available\n\n Get a new API key if needed:\n ${providerName === 'claude' ? '• https://console.anthropic.com/settings/keys' : '• https://aistudio.google.com/app/apikey'}`
1317
+ message: `${mainEnvVar} is set but API call failed.\n\n ${parsedError}\n\n Get a new API key if needed:\n ${urlMap[mainProvider]}`
527
1318
  };
528
1319
  }
529
1320
 
530
- console.log(`✓ API key validated successfully\n`);
1321
+ // Validate validation provider if enabled and different from main
1322
+ if (validationEnabled && validationProvider && validationProvider !== mainProvider && validationProvider !== 'local') {
1323
+ const validationEnvVar = envVarMap[validationProvider];
1324
+
1325
+ if (!validationEnvVar) {
1326
+ return {
1327
+ valid: false,
1328
+ message: `Unknown validation provider "${validationProvider}".\n Supported providers: claude, gemini, openai, xiaomi, local`
1329
+ };
1330
+ }
1331
+
1332
+ if (!hasProviderAuth(validationProvider)) {
1333
+ // Auto-switch validation to use the main provider (which we already verified works)
1334
+ fileLog('WARN', `Validation provider ${validationProvider} has no key — switching to main provider ${mainProvider}`);
1335
+ try {
1336
+ const freshConfig = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
1337
+ for (const c of (freshConfig.settings?.ceremonies || [])) {
1338
+ if (c.validation && typeof c.validation === 'object') {
1339
+ c.validation.provider = mainProvider;
1340
+ c.validation.model = mainModel;
1341
+ for (const [k, v] of Object.entries(c.validation)) {
1342
+ if (v && typeof v === 'object' && typeof v.provider === 'string') {
1343
+ v.provider = mainProvider;
1344
+ v.model = mainModel;
1345
+ }
1346
+ }
1347
+ }
1348
+ }
1349
+ fs.writeFileSync(this.avcConfigPath, JSON.stringify(freshConfig, null, 2), 'utf8');
1350
+ } catch {}
1351
+ return { valid: true };
1352
+ }
1353
+
1354
+ try {
1355
+ result = await LLMProvider.validate(validationProvider, validationModel);
1356
+ } catch (error) {
1357
+ // Validation provider failed — auto-switch to main provider
1358
+ fileLog('WARN', `Validation provider ${validationProvider} failed — switching to main provider ${mainProvider}`);
1359
+ try {
1360
+ const freshConfig = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
1361
+ for (const c of (freshConfig.settings?.ceremonies || [])) {
1362
+ if (c.validation && typeof c.validation === 'object') {
1363
+ c.validation.provider = mainProvider;
1364
+ c.validation.model = mainModel;
1365
+ for (const [k, v] of Object.entries(c.validation)) {
1366
+ if (v && typeof v === 'object' && typeof v.provider === 'string') {
1367
+ v.provider = mainProvider;
1368
+ v.model = mainModel;
1369
+ }
1370
+ }
1371
+ }
1372
+ }
1373
+ fs.writeFileSync(this.avcConfigPath, JSON.stringify(freshConfig, null, 2), 'utf8');
1374
+ } catch {}
1375
+ return { valid: true };
1376
+ }
1377
+
1378
+ if (!result.valid) {
1379
+ // Validation provider API call failed — auto-switch to main provider
1380
+ fileLog('WARN', `Validation provider ${validationProvider} API call failed — switching to main provider ${mainProvider}`);
1381
+ try {
1382
+ const freshConfig = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
1383
+ for (const c of (freshConfig.settings?.ceremonies || [])) {
1384
+ if (c.validation && typeof c.validation === 'object') {
1385
+ c.validation.provider = mainProvider;
1386
+ c.validation.model = mainModel;
1387
+ for (const [k, v] of Object.entries(c.validation)) {
1388
+ if (v && typeof v === 'object' && typeof v.provider === 'string') {
1389
+ v.provider = mainProvider;
1390
+ v.model = mainModel;
1391
+ }
1392
+ }
1393
+ }
1394
+ }
1395
+ fs.writeFileSync(this.avcConfigPath, JSON.stringify(freshConfig, null, 2), 'utf8');
1396
+ } catch {}
1397
+ return { valid: true };
1398
+ }
1399
+
1400
+ }
1401
+
531
1402
  return { valid: true };
532
1403
  }
533
1404
 
534
1405
  /**
535
1406
  * Generate project document via Sponsor Call ceremony
536
1407
  */
537
- async generateProjectDocument(progress = null, progressPath = null, nonInteractive = false) {
538
- const processor = new TemplateProcessor(progressPath || this.sponsorCallProgressPath, nonInteractive);
539
- await processor.processTemplate(progress);
1408
+ async generateProjectDocument(progress = null, progressPath = null, nonInteractive = false, progressCallback = null, options = {}) {
1409
+ const processor = new TemplateProcessor('sponsor-call', progressPath || this.sponsorCallProgressPath, nonInteractive, options);
1410
+
1411
+ // Set before await so processor is reachable even if processTemplate throws
1412
+ this._lastTemplateProcessor = processor;
1413
+
1414
+ if (progressCallback) {
1415
+ processor.setProgressCallback(progressCallback);
1416
+ }
1417
+
1418
+ return await processor.processTemplate(progress);
1419
+ }
1420
+
1421
+ /**
1422
+ * Get token usage from last template processor execution
1423
+ * @returns {Object|null} Token usage object or null
1424
+ */
1425
+ getLastTokenUsage() {
1426
+ if (this._lastTemplateProcessor) {
1427
+ return this._lastTemplateProcessor.getLastTokenUsage();
1428
+ }
1429
+ return null;
1430
+ }
1431
+
1432
+ /**
1433
+ * Read ceremony configuration from avc.json
1434
+ * @param {string} ceremonyName - Name of the ceremony
1435
+ * @returns {Object|null} Ceremony config or null
1436
+ */
1437
+ readCeremonyConfig(ceremonyName) {
1438
+ try {
1439
+ const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
1440
+ return config.settings?.ceremonies?.find(c => c.name === ceremonyName);
1441
+ } catch (error) {
1442
+ return null;
1443
+ }
540
1444
  }
541
1445
 
542
1446
  /**
@@ -551,73 +1455,225 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
551
1455
  * Creates .avc folder, avc.json config, .env file, and gitignore entry
552
1456
  */
553
1457
  async init() {
554
- console.log('\n🚀 AVC Project Initiator\n');
555
- console.log(`Project directory: ${this.projectRoot}\n`);
1458
+ const startTime = Date.now();
1459
+ fileLog('INFO', 'init() started', { projectRoot: this.projectRoot });
556
1460
 
557
1461
  if (this.isAvcProject()) {
558
1462
  // Project already initialized
559
- console.log(' AVC project already initialized');
560
- console.log('\nProject is ready to use.');
1463
+ fileLog('INFO', 'Project already initialized — skipping structure creation');
1464
+ sendOutput('Project already initialized.');
1465
+ sendOutput('');
561
1466
  return;
562
1467
  }
563
1468
 
1469
+ fileLog('INFO', 'New project — creating structure');
1470
+ fileLog('DEBUG', 'Creating components: .avc/, src/, worktrees/, avc.json, .env, .gitignore, VitePress');
1471
+
564
1472
  // Suppress all console output during initialization
565
1473
  const originalLog = console.log;
566
- console.log = () => {};
1474
+ console.log = () => { };
567
1475
 
1476
+ let initError = null;
568
1477
  try {
569
1478
  // Create project structure silently
570
1479
  this.createAvcFolder();
1480
+ this.createSrcFolder();
1481
+ this.createWorktreesFolder();
571
1482
  this.createAvcConfig();
572
1483
  this.createEnvFile();
573
1484
  this.addToGitignore();
574
- this.createVitePressSetup();
1485
+ this.createVitePressStructure();
1486
+ } catch (err) {
1487
+ initError = err;
575
1488
  } finally {
576
1489
  console.log = originalLog;
577
1490
  }
578
1491
 
579
- console.log('\n✅ AVC project initialized!\n');
580
- console.log('Next steps:');
581
- console.log(' 1. Add your API key(s) to .env file');
582
- console.log(' • ANTHROPIC_API_KEY for Claude');
583
- console.log(' • GEMINI_API_KEY for Gemini');
584
- console.log(' 2. Run /sponsor-call to start\n');
1492
+ if (initError) {
1493
+ fileLog('ERROR', 'Structure creation failed', { error: initError.message, stack: initError.stack });
1494
+ throw initError;
1495
+ }
1496
+
1497
+ const duration = Date.now() - startTime;
1498
+ fileLog('INFO', 'Structure creation complete', {
1499
+ duration: `${duration}ms`,
1500
+ avcFolder: this.hasAvcFolder(),
1501
+ srcFolder: this.hasSrcFolder(),
1502
+ worktreesFolder: this.hasWorktreesFolder(),
1503
+ avcConfig: this.hasAvcConfig(),
1504
+ });
1505
+
1506
+ sendOutput('Project initialized — open the Kanban board to get started.');
1507
+
1508
+ return;
1509
+ }
1510
+
1511
+ /**
1512
+ * Configure models command
1513
+ * Shows current model configuration and offers interactive editing
1514
+ */
1515
+ async models() {
1516
+ sendOutput('Model Configuration\n');
1517
+ sendOutput('Ceremonies are structured workflows (sponsor-call, sprint-planning, seed) that guide your project through key decisions. Each ceremony runs multiple stages in sequence, and you can assign a different LLM model to each stage.\n');
1518
+
1519
+ // Check if project is initialized
1520
+ if (!this.isAvcProject()) {
1521
+ sendError(MESSAGES.PROJECT_NOT_INITIALIZED.error);
1522
+ sendOutput('');
1523
+ sendOutput(MESSAGES.PROJECT_NOT_INITIALIZED.help);
1524
+ sendOutput('');
1525
+ return;
1526
+ }
1527
+
1528
+ // Use the shared configuration method
1529
+ return this.configureModelsInteractively();
1530
+ }
1531
+
1532
+ /**
1533
+ * Interactive model configuration flow
1534
+ * Shared by both /init and /models commands
1535
+ */
1536
+ configureModelsInteractively() {
1537
+ fileLog('INFO', 'configureModels() called', { projectRoot: this.projectRoot });
1538
+
1539
+ const configurator = new ModelConfigurator(this.projectRoot);
1540
+
1541
+ // Detect available providers (used for model indicators)
1542
+ configurator.availableProviders = configurator.detectAvailableProviders();
1543
+ configurator.readConfig();
1544
+
1545
+ fileLog('DEBUG', 'Model configurator loaded', {
1546
+ availableProviders: configurator.availableProviders,
1547
+ configPath: this.avcConfigPath,
1548
+ });
1549
+
1550
+ // Show current configuration
1551
+ const ceremonies = configurator.getCeremonies();
1552
+ fileLog('INFO', 'Ceremony model configs', {
1553
+ count: ceremonies.length,
1554
+ names: ceremonies.map(c => c.name),
1555
+ });
1556
+
1557
+ ceremonies.forEach(c => {
1558
+ const hasMainKey = configurator.availableProviders.includes(c.mainProvider);
1559
+ const stageDetails = {};
1560
+ Object.keys(c.stages).forEach(stageName => {
1561
+ const stage = c.stages[stageName];
1562
+ stageDetails[stageName] = {
1563
+ model: stage.model,
1564
+ provider: stage.provider,
1565
+ hasApiKey: configurator.availableProviders.includes(stage.provider),
1566
+ };
1567
+ });
1568
+ fileLog('DEBUG', `Ceremony: ${c.name}`, {
1569
+ mainModel: c.mainModel,
1570
+ mainProvider: c.mainProvider,
1571
+ hasMainKey,
1572
+ validationModel: c.validationModel || null,
1573
+ validationProvider: c.validationProvider || null,
1574
+ hasValidationKey: c.validationProvider ? configurator.availableProviders.includes(c.validationProvider) : null,
1575
+ stages: stageDetails,
1576
+ });
1577
+
1578
+ sendOutput(boldCyan(c.name));
1579
+ sendOutput(`${yellow('default')}: ${green(c.mainModel)} (${c.mainProvider})`);
1580
+ if (c.validationProvider) {
1581
+ const hasValidationKey = configurator.availableProviders.includes(c.validationProvider);
1582
+ const keyWarning = hasValidationKey ? '' : ' [no API key]';
1583
+ sendOutput(`${yellow('validation')}: ${green(c.validationModel)} (${c.validationProvider})${keyWarning}`);
1584
+ }
1585
+ Object.keys(c.stages).forEach(stageName => {
1586
+ const stage = c.stages[stageName];
1587
+ const hasStageKey = configurator.availableProviders.includes(stage.provider);
1588
+ const keyWarning = hasStageKey ? '' : ' [no API key]';
1589
+ sendOutput(`${yellow(stageName)}: ${green(stage.model)} (${stage.provider})${keyWarning}`);
1590
+ });
1591
+ sendOutput('');
1592
+ });
1593
+
1594
+ fileLog('INFO', 'configureModels() complete');
1595
+ // Return configurator for REPL to use
1596
+ return {
1597
+ shouldConfigure: true,
1598
+ configurator,
1599
+ ceremonies: ceremonies.map(c => c.name) // List of ceremony names for selection
1600
+ };
585
1601
  }
586
1602
 
587
1603
  /**
588
1604
  * Run Sponsor Call ceremony with pre-filled answers from REPL questionnaire
589
1605
  * Used when all answers are collected via REPL UI
590
1606
  */
591
- async sponsorCallWithAnswers(answers) {
592
- console.log('\n🎯 Sponsor Call Ceremony\n');
593
- console.log(`Project directory: ${this.projectRoot}\n`);
1607
+ async sponsorCallWithAnswers(answers, progressCallback = null, options = {}) {
1608
+ const startTime = Date.now();
1609
+ fileLog('INFO', 'sponsorCallWithAnswers() called', {
1610
+ answerKeys: Object.keys(answers || {}),
1611
+ answeredCount: Object.values(answers || {}).filter(v => v !== null && v !== '').length,
1612
+ hasProgressCallback: !!progressCallback,
1613
+ projectRoot: this.projectRoot,
1614
+ });
1615
+
1616
+ // Remove initial ceremony banner - will be shown in summary
594
1617
 
595
1618
  // Check if project is initialized
596
1619
  if (!this.isAvcProject()) {
597
- console.log(' Project not initialized\n');
598
- console.log(' Please run /init first to create the project structure.\n');
1620
+ fileLog('ERROR', 'Project not initialized — aborting sponsor call');
1621
+ sendError(MESSAGES.PROJECT_NOT_INITIALIZED.error);
599
1622
  return;
600
1623
  }
601
1624
 
602
1625
  const progressPath = this.sponsorCallProgressPath;
603
1626
 
604
- console.log('Starting Sponsor Call ceremony with provided answers...\n');
1627
+ // Initialize ceremony history
1628
+ const { CeremonyHistory } = await import('./ceremony-history.js');
1629
+ const history = new CeremonyHistory(this.avcDir);
1630
+ history.init();
1631
+
1632
+ // Get or create execution record
1633
+ let executionId;
1634
+ const lastExecution = history.getLastExecution('sponsor-call');
605
1635
 
606
- // Count answers provided
1636
+ if (lastExecution && lastExecution.status === 'in-progress' && lastExecution.stage === 'llm-generation') {
1637
+ // Resume existing execution (from REPL flow)
1638
+ executionId = lastExecution.id;
1639
+ } else {
1640
+ // This shouldn't normally happen if called from REPL, but handle it
1641
+ executionId = history.startExecution('sponsor-call', 'llm-generation');
1642
+ }
1643
+
1644
+ // Count answers provided (for logging)
607
1645
  const answeredCount = Object.values(answers).filter(v => v !== null && v !== '').length;
608
- console.log(`📊 Received ${answeredCount}/5 answers from questionnaire\n`);
1646
+ fileLog('DEBUG', 'Ceremony history state', {
1647
+ executionId,
1648
+ lastExecutionStatus: lastExecution?.status,
1649
+ lastExecutionStage: lastExecution?.stage,
1650
+ answeredCount,
1651
+ totalQuestions: Object.keys(answers).length,
1652
+ });
609
1653
 
610
1654
  // Validate API key before starting ceremony
611
- console.log('Step 1/3: Validating API configuration...');
1655
+ fileLog('INFO', 'Validating API key before ceremony start');
612
1656
  const validationResult = await this.validateProviderApiKey();
1657
+ fileLog(validationResult.valid ? 'INFO' : 'ERROR', 'API key validation result', {
1658
+ valid: validationResult.valid,
1659
+ message: validationResult.message || 'OK',
1660
+ });
613
1661
  if (!validationResult.valid) {
614
- console.log('\n❌ API Key Validation Failed\n');
615
- console.log(` ${validationResult.message}\n`);
616
- return;
1662
+ // Mark execution as aborted
1663
+ history.completeExecution('sponsor-call', executionId, 'abrupt-termination', {
1664
+ answers,
1665
+ stage: 'llm-generation',
1666
+ error: 'API key validation failed'
1667
+ });
1668
+
1669
+ // Return error for REPL to display
1670
+ return {
1671
+ error: true,
1672
+ message: `API Key Validation Failed: ${validationResult.message}`
1673
+ };
617
1674
  }
618
1675
 
619
1676
  // Create progress with pre-filled answers
620
- console.log('Step 2/3: Processing questionnaire answers...');
621
1677
  const progress = {
622
1678
  stage: 'questionnaire',
623
1679
  totalQuestions: 5,
@@ -627,20 +1683,226 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
627
1683
  };
628
1684
  this.writeProgress(progress, progressPath);
629
1685
 
630
- // Generate project document with pre-filled answers
631
- console.log('Step 3/3: Generating project document...');
632
- await this.generateProjectDocument(progress, progressPath, true); // nonInteractive = true
1686
+ try {
1687
+ // Generate project document with pre-filled answers
1688
+ const result = await this.generateProjectDocument(progress, progressPath, true, progressCallback, options);
633
1689
 
634
- // Mark as completed and clean up
635
- progress.stage = 'completed';
636
- progress.lastUpdate = new Date().toISOString();
637
- this.writeProgress(progress, progressPath);
638
- this.clearProgress(progressPath);
1690
+ fileLog('INFO', 'generateProjectDocument() complete', { resultKeys: result ? Object.keys(result) : [] });
1691
+
1692
+ // Notify progress during cleanup
1693
+ if (progressCallback) await progressCallback(null, 'Calculating token usage costs...');
1694
+
1695
+ // Get token usage from template processor
1696
+ const tokenUsage = this.getLastTokenUsage();
1697
+
1698
+ // Get model ID from ceremony config
1699
+ const ceremony = this.readCeremonyConfig('sponsor-call');
1700
+ const modelId = ceremony?.defaultModel || 'claude-sonnet-4-6';
1701
+
1702
+ // Calculate cost using token tracker
1703
+ const { TokenTracker } = await import('./token-tracker.js');
1704
+ const tracker = new TokenTracker(this.avcDir);
1705
+ const cost = tracker.calculateCost(
1706
+ tokenUsage?.inputTokens || 0,
1707
+ tokenUsage?.outputTokens || 0,
1708
+ modelId
1709
+ );
639
1710
 
640
- console.log('\n✅ Project defined successfully!');
641
- console.log('\nNext steps:');
642
- console.log(' 1. Review .avc/project/doc.md for your project definition');
643
- console.log(' 2. Review .avc/avc.json configuration');
1711
+ // Mark execution as completed with metadata
1712
+ if (progressCallback) await progressCallback(null, 'Saving ceremony history...');
1713
+ history.completeExecution('sponsor-call', executionId, 'success', {
1714
+ answers,
1715
+ filesGenerated: [
1716
+ path.join(this.avcDir, 'project/doc.md')
1717
+ ],
1718
+ tokenUsage: {
1719
+ input: tokenUsage?.inputTokens || 0,
1720
+ output: tokenUsage?.outputTokens || 0,
1721
+ total: tokenUsage?.totalTokens || 0
1722
+ },
1723
+ model: modelId,
1724
+ cost: cost,
1725
+ stage: 'completed'
1726
+ });
1727
+
1728
+ // Mark progress as completed and clean up
1729
+ if (progressCallback) await progressCallback(null, 'Finalizing ceremony...');
1730
+ progress.stage = 'completed';
1731
+ progress.lastUpdate = new Date().toISOString();
1732
+ this.writeProgress(progress, progressPath);
1733
+ this.clearProgress(progressPath);
1734
+
1735
+ // Emit a final main progress message so the UI log clearly shows completion
1736
+ if (progressCallback) await progressCallback('✓ Documentation generated successfully!');
1737
+
1738
+ fileLog('INFO', 'sponsorCallWithAnswers() complete', {
1739
+ duration: `${Date.now() - startTime}ms`,
1740
+ outputPath: result?.outputPath,
1741
+ tokenInput: result?.tokenUsage?.input,
1742
+ tokenOutput: result?.tokenUsage?.output,
1743
+ estimatedCost: result?.cost?.total,
1744
+ });
1745
+
1746
+ // Persist answers and generate Q&A documentation page (non-fatal)
1747
+ this.saveProjectBriefAnswers(answers);
1748
+
1749
+ // Return result for display in REPL
1750
+ return result;
1751
+
1752
+ } catch (error) {
1753
+ const isCancelled = error.message === 'CEREMONY_CANCELLED';
1754
+
1755
+ // Save any tokens spent before cancellation/error
1756
+ try {
1757
+ this._lastTemplateProcessor?.saveCurrentTokenTracking();
1758
+ } catch (trackErr) {
1759
+ fileLog('WARN', 'Could not save partial token tracking', { error: trackErr.message });
1760
+ }
1761
+
1762
+ if (!isCancelled) {
1763
+ fileLog('ERROR', 'sponsorCallWithAnswers() failed', {
1764
+ error: error.message,
1765
+ stack: error.stack,
1766
+ duration: `${Date.now() - startTime}ms`,
1767
+ });
1768
+ }
1769
+
1770
+ // Mark execution as aborted on error/cancel
1771
+ history.completeExecution('sponsor-call', executionId, 'abrupt-termination', {
1772
+ answers,
1773
+ stage: isCancelled ? 'cancelled' : 'llm-generation',
1774
+ error: error.message
1775
+ });
1776
+
1777
+ throw error;
1778
+ }
1779
+ }
1780
+
1781
+ /**
1782
+ * Display token usage statistics and costs
1783
+ */
1784
+ async showTokenStats() {
1785
+ fileLog('INFO', 'showTokenStats() called', { avcDir: this.avcDir });
1786
+
1787
+ const { TokenTracker } = await import('./token-tracker.js');
1788
+ const tracker = new TokenTracker(this.avcDir);
1789
+ tracker.init();
1790
+ tracker.load();
1791
+
1792
+ const data = tracker.data;
1793
+ fileLog('DEBUG', 'Token history loaded', {
1794
+ version: data.version,
1795
+ lastUpdated: data.lastUpdated,
1796
+ ceremonyTypes: Object.keys(data).filter(k => !['version', 'lastUpdated', 'totals'].includes(k)),
1797
+ allTimeTotal: data.totals?.allTime?.total,
1798
+ allTimeExecutions: data.totals?.allTime?.executions,
1799
+ });
1800
+
1801
+ const allTime = data.totals.allTime;
1802
+ const costStr = (allTime.cost && allTime.cost.total > 0) ? ` / $${allTime.cost.total.toFixed(4)}` : '';
1803
+ sendOutput(`All-time: ${allTime.total.toLocaleString()} tokens / ${allTime.executions} executions${costStr}`);
1804
+
1805
+ const ceremonyTypes = Object.keys(data).filter(k => !['version', 'lastUpdated', 'totals'].includes(k));
1806
+ for (const ceremonyType of ceremonyTypes) {
1807
+ const ceremony = data[ceremonyType];
1808
+ if (ceremony.allTime && ceremony.allTime.executions > 0) {
1809
+ const cCostStr = (ceremony.allTime.cost && ceremony.allTime.cost.total > 0) ? ` / $${ceremony.allTime.cost.total.toFixed(4)}` : '';
1810
+ sendIndented(`${ceremonyType}: ${ceremony.allTime.total.toLocaleString()} tokens / ${ceremony.allTime.executions} runs${cCostStr}`, 1);
1811
+ }
1812
+ }
1813
+
1814
+ fileLog('INFO', 'showTokenStats() complete', {
1815
+ allTimeInput: data.totals?.allTime?.input,
1816
+ allTimeOutput: data.totals?.allTime?.output,
1817
+ allTimeTotal: data.totals?.allTime?.total,
1818
+ estimatedCost: data.totals?.allTime?.cost?.total,
1819
+ });
1820
+ }
1821
+
1822
+ /**
1823
+ * Run Sprint Planning ceremony to create/expand Epics and Stories
1824
+ */
1825
+ async sprintPlanning() {
1826
+ const startTime = Date.now();
1827
+ fileLog('INFO', 'sprintPlanning() called', { projectRoot: this.projectRoot });
1828
+
1829
+ if (!this.isAvcProject()) {
1830
+ fileLog('ERROR', 'Project not initialized — aborting sprint planning');
1831
+ sendError('Project not initialized. Run /init first.');
1832
+ return;
1833
+ }
1834
+
1835
+ fileLog('INFO', 'Loading SprintPlanningProcessor');
1836
+ const { SprintPlanningProcessor } = await import('./sprint-planning-processor.js');
1837
+ const processor = new SprintPlanningProcessor();
1838
+ fileLog('DEBUG', 'SprintPlanningProcessor created', {
1839
+ projectPath: processor.projectPath,
1840
+ provider: processor._providerName,
1841
+ model: processor._modelName,
1842
+ });
1843
+
1844
+ await processor.execute();
1845
+
1846
+ // Non-fatal docs sync after sprint planning
1847
+ try {
1848
+ const { DocsSyncProcessor } = await import('./docs-sync.js');
1849
+ const syncer = new DocsSyncProcessor(process.cwd());
1850
+ if (fs.existsSync(syncer.docsDir)) {
1851
+ await syncer.sync();
1852
+ sendInfo('Documentation synced.');
1853
+ }
1854
+ } catch (_) { /* non-fatal */ }
1855
+
1856
+ fileLog('INFO', 'sprintPlanning() complete', { duration: `${Date.now() - startTime}ms` });
1857
+ }
1858
+
1859
+ /**
1860
+ * Run Sprint Planning ceremony with a progress callback (used by kanban board)
1861
+ * @param {Function|null} progressCallback - Called with (msg, substep, meta) on each stage
1862
+ * @returns {Promise<object>} Result with epicsCreated, storiesCreated, tokenUsage, model
1863
+ */
1864
+ async sprintPlanningWithCallback(progressCallback = null, options = {}) {
1865
+ fileLog('INFO', 'sprintPlanningWithCallback() called', { projectRoot: this.projectRoot, resumeFrom: options.resumeFrom || null });
1866
+ if (!this.isAvcProject()) {
1867
+ throw new Error('Project not initialized. Run /init first.');
1868
+ }
1869
+ const { SprintPlanningProcessor } = await import('./sprint-planning-processor.js');
1870
+ const processor = new SprintPlanningProcessor(options);
1871
+ return await processor.execute(progressCallback, { resumeFrom: options.resumeFrom || null });
1872
+ }
1873
+
1874
+ /**
1875
+ * Run Seed ceremony to decompose a Story into Tasks and Subtasks
1876
+ * @param {string} storyId - Story ID (e.g., context-0001-0001)
1877
+ */
1878
+ async seed(storyId) {
1879
+ const startTime = Date.now();
1880
+ fileLog('INFO', 'seed() called', { storyId, projectRoot: this.projectRoot });
1881
+
1882
+ if (!this.isAvcProject()) {
1883
+ fileLog('ERROR', 'Project not initialized — aborting seed');
1884
+ sendError('Project not initialized. Run /init first.');
1885
+ return;
1886
+ }
1887
+
1888
+ if (!storyId) {
1889
+ fileLog('ERROR', 'No story ID provided — aborting seed');
1890
+ sendError('Story ID required. Usage: /seed <story-id>');
1891
+ return;
1892
+ }
1893
+
1894
+ fileLog('INFO', 'Loading SeedProcessor', { storyId });
1895
+ const { SeedProcessor } = await import('./seed-processor.js');
1896
+ const processor = new SeedProcessor(storyId);
1897
+ fileLog('DEBUG', 'SeedProcessor created', {
1898
+ storyId,
1899
+ storyPath: processor.storyPath,
1900
+ provider: processor._providerName,
1901
+ model: processor._modelName,
1902
+ });
1903
+
1904
+ await processor.execute();
1905
+ fileLog('INFO', 'seed() complete', { storyId, duration: `${Date.now() - startTime}ms` });
644
1906
  }
645
1907
 
646
1908
  /**
@@ -648,25 +1910,46 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
648
1910
  * Requires API keys to be configured in .env file
649
1911
  */
650
1912
  async sponsorCall() {
651
- console.log('\n🎯 Sponsor Call Ceremony\n');
652
- console.log(`Project directory: ${this.projectRoot}\n`);
1913
+ const header = getCeremonyHeader('sponsor-call');
1914
+ sendOutput('');
1915
+ sendOutput(header.title);
1916
+ sendOutput('');
1917
+ sendOutput(`Project directory: ${this.projectRoot}`);
1918
+ sendOutput('');
653
1919
 
654
1920
  // Check if running in REPL mode
655
1921
  const isReplMode = process.env.AVC_REPL_MODE === 'true';
656
1922
  if (isReplMode) {
657
1923
  // REPL mode is handled by repl-ink.js questionnaire display
658
1924
  // This code path shouldn't be reached from REPL
659
- console.log('⚠️ Unexpected: Ceremony called directly from REPL');
1925
+ sendWarning('Unexpected: Ceremony called directly from REPL');
660
1926
  return;
661
1927
  }
662
1928
 
663
1929
  // Check if project is initialized
664
1930
  if (!this.isAvcProject()) {
665
- console.log('❌ Project not initialized\n');
666
- console.log(' Please run /init first to create the project structure.\n');
1931
+ sendError(MESSAGES.PROJECT_NOT_INITIALIZED.error);
1932
+ sendOutput('');
1933
+ sendOutput(MESSAGES.PROJECT_NOT_INITIALIZED.help);
1934
+ sendOutput('');
667
1935
  return; // Don't exit in REPL mode
668
1936
  }
669
1937
 
1938
+ // Check if sponsor call has already completed successfully
1939
+ const { CeremonyHistory } = await import('./ceremony-history.js');
1940
+ const history = new CeremonyHistory(this.avcDir);
1941
+
1942
+ if (history.hasSuccessfulCompletion('sponsor-call')) {
1943
+ sendError('Sponsor Call has already completed successfully');
1944
+ sendOutput('');
1945
+ sendOutput('Project documentation already exists at .avc/project/doc.md');
1946
+ sendOutput('');
1947
+ sendOutput('To regenerate documentation, first run /remove to clear the project,');
1948
+ sendOutput('then run /init followed by /sponsor-call again.');
1949
+ sendOutput('');
1950
+ return; // Don't allow re-running
1951
+ }
1952
+
670
1953
  let progress = null;
671
1954
  const progressPath = this.sponsorCallProgressPath;
672
1955
 
@@ -675,22 +1958,28 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
675
1958
  progress = this.readProgress(progressPath);
676
1959
 
677
1960
  if (progress && progress.stage !== 'completed') {
678
- console.log('⚠️ Found incomplete ceremony from previous session');
679
- console.log(` Last activity: ${new Date(progress.lastUpdate).toLocaleString()}`);
680
- console.log(` Stage: ${progress.stage}`);
681
- console.log(` Progress: ${progress.answeredQuestions || 0}/${progress.totalQuestions || 0} questions answered`);
682
- console.log('\n▶️ Continuing from where you left off...\n');
1961
+ sendWarning('Found incomplete ceremony from previous session');
1962
+ sendIndented(`Last activity: ${new Date(progress.lastUpdate).toLocaleString()}`, 1);
1963
+ sendIndented(`Stage: ${progress.stage}`, 1);
1964
+ sendIndented(`Progress: ${progress.answeredQuestions || 0}/${progress.totalQuestions || 0} questions answered`, 1);
1965
+ sendOutput('');
1966
+ sendInfo('Continuing from where you left off...');
1967
+ sendOutput('');
683
1968
  }
684
1969
  } else {
685
1970
  // Fresh start
686
- console.log('Starting Sponsor Call ceremony...\n');
1971
+ sendOutput('Starting Sponsor Call ceremony...');
1972
+ sendOutput('');
687
1973
  }
688
1974
 
689
1975
  // Validate API key before starting ceremony
690
1976
  const validationResult = await this.validateProviderApiKey();
691
1977
  if (!validationResult.valid) {
692
- console.log('\n❌ API Key Validation Failed\n');
693
- console.log(` ${validationResult.message}\n`);
1978
+ sendOutput('');
1979
+ sendError('API Key Validation Failed');
1980
+ sendOutput('');
1981
+ sendIndented(validationResult.message, 1);
1982
+ sendOutput('');
694
1983
  return; // Don't exit in REPL mode
695
1984
  }
696
1985
 
@@ -715,30 +2004,44 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
715
2004
  this.writeProgress(progress, progressPath);
716
2005
  this.clearProgress(progressPath);
717
2006
 
718
- console.log('\n✅ Project defined successfully!');
719
- console.log('\nNext steps:');
720
- console.log(' 1. Review .avc/project/doc.md for your project definition');
721
- console.log(' 2. Review .avc/avc.json configuration');
722
- console.log(' 3. Create your project context and work items');
723
- console.log(' 4. Use AI agents to implement features');
2007
+ sendOutput('');
2008
+ sendSuccess('Project defined successfully!');
2009
+ sendOutput('');
2010
+ sendOutput('Next steps:');
2011
+ sendIndented('1. Review .avc/project/doc.md for your project definition', 1);
2012
+ sendIndented('2. Review .avc/avc.json configuration', 1);
2013
+ sendIndented('3. Create your project documentation and work items', 1);
2014
+ sendIndented('4. Use AI agents to implement features', 1);
724
2015
  }
725
2016
 
726
2017
  /**
727
2018
  * Display current project status
728
2019
  */
729
2020
  status() {
730
- console.log('\n📊 AVC Project Status\n');
731
- console.log(`Project directory: ${this.projectRoot}`);
732
- console.log(`Project name: ${this.getProjectName()}\n`);
733
-
734
- console.log('Components:');
735
- console.log(` .avc/ folder: ${this.hasAvcFolder() ? '✓' : '✗'}`);
736
- console.log(` avc.json: ${this.hasAvcConfig() ? '✓' : '✗'}`);
737
-
738
- console.log(`\nStatus: ${this.isAvcProject() ? '✅ Initialized' : '⚠️ Not initialized'}`);
2021
+ fileLog('INFO', 'status() called', { projectRoot: this.projectRoot });
2022
+
2023
+ const hasAvc = this.hasAvcFolder();
2024
+ const hasSrc = this.hasSrcFolder();
2025
+ const hasWorktrees = this.hasWorktreesFolder();
2026
+ const hasConfig = this.hasAvcConfig();
2027
+ const isInitialized = this.isAvcProject();
2028
+ const projectName = this.getProjectName();
2029
+
2030
+ fileLog('DEBUG', 'Component check results', {
2031
+ '.avc/': hasAvc,
2032
+ 'src/': hasSrc,
2033
+ 'worktrees/': hasWorktrees,
2034
+ 'avc.json': hasConfig,
2035
+ isAvcProject: isInitialized,
2036
+ projectName,
2037
+ });
739
2038
 
740
- if (!this.isAvcProject()) {
741
- console.log('\nRun "avc init" to initialize the project.');
2039
+ if (isInitialized) {
2040
+ sendOutput(`${projectName}: Initialized`);
2041
+ fileLog('INFO', 'status() complete — project is initialized');
2042
+ } else {
2043
+ fileLog('WARNING', 'Project not initialized — components missing', { hasAvc, hasConfig });
2044
+ sendOutput(`${projectName}: Not initialized. Run /init to start.`);
742
2045
  }
743
2046
  }
744
2047
 
@@ -747,40 +2050,63 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
747
2050
  * Requires confirmation by typing "delete all"
748
2051
  */
749
2052
  async remove() {
750
- console.log('\n🗑️ Remove AVC Project Structure\n');
751
- console.log(`Project directory: ${this.projectRoot}\n`);
2053
+ sendSectionHeader('Remove AVC Project Structure');
2054
+ sendOutput('');
2055
+ sendOutput(`Project directory: ${this.projectRoot}`);
2056
+ sendOutput('');
752
2057
 
753
2058
  // Check if project is initialized
754
2059
  if (!this.isAvcProject()) {
755
- console.log('⚠️ No AVC project found in this directory.\n');
756
- console.log('Nothing to remove.\n');
2060
+ sendWarning('No AVC project found in this directory.');
2061
+ sendOutput('Nothing to remove.');
2062
+ sendOutput('');
757
2063
  return;
758
2064
  }
759
2065
 
760
2066
  // Show what will be deleted
761
- console.log('⚠️ WARNING: This is a DESTRUCTIVE operation!\n');
762
- console.log('The following will be PERMANENTLY DELETED:\n');
2067
+ sendWarning('WARNING: This is a DESTRUCTIVE operation!');
2068
+ sendOutput('');
2069
+ sendOutput('The following will be PERMANENTLY DELETED:');
2070
+ sendOutput('');
763
2071
 
764
2072
  // List contents of .avc folder
765
2073
  const avcContents = this.getAvcContents();
766
2074
  if (avcContents.length > 0) {
767
- console.log('📁 .avc/ folder contents:');
2075
+ sendOutput('.avc/ folder contents:');
2076
+ sendOutput('');
768
2077
  avcContents.forEach(item => {
769
- console.log(` • ${item}`);
2078
+ sendIndented(`• ${item}`, 1);
770
2079
  });
771
- console.log('');
2080
+ sendOutput('');
772
2081
  }
773
2082
 
774
- console.log('All project definitions, epics, stories, tasks, and documentation will be lost.');
775
- console.log('All VitePress documentation will be deleted.');
776
- console.log('This action CANNOT be undone.\n');
2083
+ sendError('All project definitions, epics, stories, tasks, and documentation will be lost.');
2084
+ sendError('All VitePress documentation will be deleted.');
2085
+ sendError('This action CANNOT be undone.');
2086
+ sendOutput('');
777
2087
 
778
2088
  // Check for .env file
779
2089
  const envPath = path.join(this.projectRoot, '.env');
780
2090
  const hasEnvFile = fs.existsSync(envPath);
781
2091
  if (hasEnvFile) {
782
- console.log('ℹ️ Note: The .env file will NOT be deleted.');
783
- console.log(' You may want to manually remove API keys if no longer needed.\n');
2092
+ sendInfo('Note: The .env file will NOT be deleted.');
2093
+ sendOutput('You may want to manually remove API keys if no longer needed.');
2094
+ sendOutput('');
2095
+ }
2096
+
2097
+ // Check for src folder
2098
+ const hasSrcFolder = this.hasSrcFolder();
2099
+ if (hasSrcFolder) {
2100
+ sendSuccess('IMPORTANT: The src/ folder will NOT be deleted.');
2101
+ sendOutput('All your AVC-managed code will be preserved.');
2102
+ sendOutput('');
2103
+ }
2104
+
2105
+ // Check for worktrees folder
2106
+ const hasWorktreesFolder = this.hasWorktreesFolder();
2107
+ if (hasWorktreesFolder) {
2108
+ sendWarning('The .avc/worktrees/ folder and all git worktrees inside it WILL be deleted.');
2109
+ sendOutput('');
784
2110
  }
785
2111
 
786
2112
  // Check if running in REPL mode
@@ -789,16 +2115,16 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
789
2115
  if (isReplMode) {
790
2116
  // In REPL mode, interactive confirmation is handled by repl-ink.js
791
2117
  // This code path shouldn't be reached from REPL
792
- console.log('⚠️ Unexpected: Remove called directly from REPL');
793
- console.log('Interactive confirmation should be handled by REPL interface.');
2118
+ sendWarning('Unexpected: Remove called directly from REPL');
2119
+ sendOutput('Interactive confirmation should be handled by REPL interface.');
794
2120
  return;
795
2121
  }
796
2122
 
797
- console.log('─'.repeat(60));
798
- console.log('To confirm deletion, type exactly: delete all');
799
- console.log('To cancel, type anything else or press Ctrl+C');
800
- console.log('─'.repeat(60));
801
- console.log('');
2123
+ sendOutput('─'.repeat(60));
2124
+ sendOutput('To confirm deletion, type exactly: delete all');
2125
+ sendOutput('To cancel, type anything else or press Ctrl+C');
2126
+ sendOutput('─'.repeat(60));
2127
+ sendOutput('');
802
2128
 
803
2129
  // Create readline interface for confirmation
804
2130
  const readline = await import('readline');
@@ -810,11 +2136,12 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
810
2136
  return new Promise((resolve) => {
811
2137
  rl.question('Confirmation: ', (answer) => {
812
2138
  rl.close();
813
- console.log('');
2139
+ sendOutput('');
814
2140
 
815
2141
  if (answer.trim() === 'delete all') {
816
2142
  // Proceed with deletion
817
- console.log('🗑️ Deleting AVC project structure...\n');
2143
+ sendOutput('Deleting AVC project structure...');
2144
+ sendOutput('');
818
2145
 
819
2146
  try {
820
2147
  // Get list of what's being deleted before deletion
@@ -823,39 +2150,60 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
823
2150
  // Delete .avc folder
824
2151
  fs.rmSync(this.avcDir, { recursive: true, force: true });
825
2152
 
826
- console.log('Successfully deleted:\n');
827
- console.log(' 📁 .avc/ folder and all contents:');
2153
+ sendSuccess('Successfully deleted:');
2154
+ sendOutput('');
2155
+ sendOutput('.avc/ folder and all contents:');
2156
+ sendOutput('');
828
2157
  deletedItems.forEach(item => {
829
- console.log(` • ${item}`);
2158
+ sendIndented(`• ${item}`, 3);
830
2159
  });
831
- console.log('');
832
-
833
- // Reminder about .env file
834
- if (hasEnvFile) {
835
- console.log('ℹ️ Manual cleanup reminder:\n');
836
- console.log(' The .env file was NOT deleted and still contains:');
837
- console.log(' • ANTHROPIC_API_KEY');
838
- console.log(' • GEMINI_API_KEY');
839
- console.log(' (and any other API keys you added)\n');
840
- console.log(' If these API keys are not used elsewhere in your project,');
841
- console.log(' you may want to manually delete the .env file or remove');
842
- console.log(' the unused keys.\n');
2160
+ sendOutput('');
2161
+
2162
+ // Reminder about preserved files
2163
+ if (hasEnvFile || hasSrcFolder) {
2164
+ sendInfo('Preserved files:');
2165
+ sendOutput('');
2166
+
2167
+ if (hasEnvFile) {
2168
+ sendOutput('The .env file was NOT deleted and still contains:');
2169
+ sendOutput('');
2170
+ sendIndented(' ANTHROPIC_API_KEY', 1);
2171
+ sendIndented(' GEMINI_API_KEY', 1);
2172
+ sendIndented('• OPENAI_API_KEY', 1);
2173
+ sendIndented('• XIAOMI_API_KEY', 1);
2174
+ sendIndented('• (and any other API keys you added)', 1);
2175
+ sendOutput('If these API keys are not used elsewhere in your project,');
2176
+ sendOutput('you may want to manually delete the .env file or remove');
2177
+ sendOutput('the unused keys.');
2178
+ sendOutput('');
2179
+ }
2180
+
2181
+ if (hasSrcFolder) {
2182
+ sendSuccess('The src/ folder was NOT deleted.');
2183
+ sendOutput('All your AVC-managed code has been preserved.');
2184
+ sendOutput('');
2185
+ }
2186
+
843
2187
  }
844
2188
 
845
- console.log('AVC project structure has been completely removed.\n');
846
- console.log('You can re-initialize anytime by running /init\n');
2189
+ sendSuccess('AVC project structure has been completely removed.');
2190
+ sendOutput('You can re-initialize anytime by running /init');
2191
+ sendOutput('');
847
2192
 
848
2193
  resolve();
849
2194
  } catch (error) {
850
- console.log(`❌ Error during deletion: ${error.message}\n`);
851
- console.log('The .avc folder may be partially deleted.');
852
- console.log('You may need to manually remove it.\n');
2195
+ sendError(`Error during deletion: ${error.message}`);
2196
+ sendOutput('');
2197
+ sendOutput('The .avc folder may be partially deleted.');
2198
+ sendOutput('You may need to manually remove it.');
2199
+ sendOutput('');
853
2200
  resolve();
854
2201
  }
855
2202
  } else {
856
2203
  // Cancellation
857
- console.log('Operation cancelled.\n');
858
- console.log('No files were deleted.\n');
2204
+ sendError('Operation cancelled.');
2205
+ sendOutput('No files were deleted.');
2206
+ sendOutput('');
859
2207
  resolve();
860
2208
  }
861
2209
  });
@@ -895,6 +2243,114 @@ If you're new to Agile Vibe Coding, visit the [AVC Documentation](https://agilev
895
2243
  return contents;
896
2244
  }
897
2245
 
2246
+ /**
2247
+ * Save sponsor-call Q&A answers to avc.json and generate VitePress Q&A page.
2248
+ * Wrapped in try/catch — failures are non-fatal.
2249
+ */
2250
+ saveProjectBriefAnswers(answers) {
2251
+ try {
2252
+ // Normalize: CLI path has top-level keys; Kanban path may nest under .requirements
2253
+ const qa = answers.MISSION_STATEMENT ? answers : (answers.requirements || answers);
2254
+
2255
+ // Read current avc.json and persist answers
2256
+ const config = JSON.parse(fs.readFileSync(this.avcConfigPath, 'utf8'));
2257
+ if (!config.settings) config.settings = {};
2258
+ if (!config.settings.projectBrief) config.settings.projectBrief = {};
2259
+ config.settings.projectBrief.answers = {
2260
+ MISSION_STATEMENT: qa.MISSION_STATEMENT || null,
2261
+ TARGET_USERS: qa.TARGET_USERS || null,
2262
+ INITIAL_SCOPE: qa.INITIAL_SCOPE || null,
2263
+ DEPLOYMENT_TARGET: qa.DEPLOYMENT_TARGET || null,
2264
+ TECHNICAL_CONSIDERATIONS: qa.TECHNICAL_CONSIDERATIONS || null,
2265
+ TECHNICAL_EXCLUSIONS: qa.TECHNICAL_EXCLUSIONS || null,
2266
+ SECURITY_AND_COMPLIANCE_REQUIREMENTS: qa.SECURITY_AND_COMPLIANCE_REQUIREMENTS || null,
2267
+ };
2268
+ config.settings.projectBrief.savedAt = new Date().toISOString();
2269
+ fs.writeFileSync(this.avcConfigPath, JSON.stringify(config, null, 2), 'utf8');
2270
+
2271
+ fileLog('INFO', 'saveProjectBriefAnswers() — answers persisted to avc.json');
2272
+
2273
+ this.generateQADocumentationPage(qa);
2274
+ } catch (error) {
2275
+ fileLog('WARN', 'saveProjectBriefAnswers() failed (non-fatal)', { error: error.message });
2276
+ }
2277
+ }
2278
+
2279
+ /**
2280
+ * Generate .avc/documentation/questions-and-answers.md with the sponsor-call answers.
2281
+ * No-ops silently if the documentation directory doesn't exist yet.
2282
+ */
2283
+ generateQADocumentationPage(qa) {
2284
+ const docDir = path.join(this.avcDir, 'documentation');
2285
+ if (!fs.existsSync(docDir)) {
2286
+ fileLog('INFO', 'generateQADocumentationPage() — documentation dir not found, skipping');
2287
+ return;
2288
+ }
2289
+
2290
+ const lines = ['# Questions & Answers', ''];
2291
+ lines.push('Sponsor-call questionnaire answers captured during project brief generation.', '');
2292
+
2293
+ lines.push('## Mission Statement', '');
2294
+ lines.push(qa.MISSION_STATEMENT || '_Not provided._', '');
2295
+
2296
+ lines.push('## Initial Scope', '');
2297
+ lines.push(qa.INITIAL_SCOPE || '_Not provided._', '');
2298
+
2299
+ lines.push('## Target Users', '');
2300
+ lines.push(qa.TARGET_USERS || '_Not provided._', '');
2301
+
2302
+ lines.push('## Deployment Target', '');
2303
+ lines.push(qa.DEPLOYMENT_TARGET || '_Not provided._', '');
2304
+
2305
+ lines.push('## Technical Considerations', '');
2306
+ lines.push(qa.TECHNICAL_CONSIDERATIONS || '_Not provided._', '');
2307
+
2308
+ if (qa.TECHNICAL_EXCLUSIONS) {
2309
+ lines.push('## Technical Exclusions', '');
2310
+ lines.push(qa.TECHNICAL_EXCLUSIONS, '');
2311
+ }
2312
+
2313
+ lines.push('## Security & Compliance Requirements', '');
2314
+ lines.push(qa.SECURITY_AND_COMPLIANCE_REQUIREMENTS || '_Not provided._', '');
2315
+
2316
+ const qaPath = path.join(docDir, 'questions-and-answers.md');
2317
+ fs.writeFileSync(qaPath, lines.join('\n'), 'utf8');
2318
+
2319
+ fileLog('INFO', 'generateQADocumentationPage() — written', { qaPath });
2320
+
2321
+ this.addQAToVitePressConfig();
2322
+ }
2323
+
2324
+ /**
2325
+ * Insert "Questions & Answers" into the VitePress sidebar, immediately after "Project Brief".
2326
+ * Idempotent — skips if already present.
2327
+ */
2328
+ addQAToVitePressConfig() {
2329
+ const configPath = path.join(this.avcDir, 'documentation', '.vitepress', 'config.mts');
2330
+ if (!fs.existsSync(configPath)) {
2331
+ fileLog('INFO', 'addQAToVitePressConfig() — config.mts not found, skipping');
2332
+ return;
2333
+ }
2334
+
2335
+ let content = fs.readFileSync(configPath, 'utf8');
2336
+
2337
+ if (content.includes('questions-and-answers')) {
2338
+ fileLog('INFO', 'addQAToVitePressConfig() — already present, skipping');
2339
+ return;
2340
+ }
2341
+
2342
+ // Insert after the Project Brief link line
2343
+ const projectBriefLine = "{ text: 'Project Brief', link: '/' }";
2344
+ const qaLine = "{ text: 'Questions & Answers', link: '/questions-and-answers' }";
2345
+ content = content.replace(
2346
+ projectBriefLine,
2347
+ `${projectBriefLine},\n ${qaLine}`
2348
+ );
2349
+
2350
+ fs.writeFileSync(configPath, content, 'utf8');
2351
+ fileLog('INFO', 'addQAToVitePressConfig() — sidebar updated', { configPath });
2352
+ }
2353
+
898
2354
  /**
899
2355
  * Recursively count items in a directory
900
2356
  */
@@ -939,11 +2395,14 @@ if (import.meta.url === `file://${process.argv[1]}`) {
939
2395
  case 'status':
940
2396
  initiator.status();
941
2397
  break;
2398
+ case 'models':
2399
+ initiator.models();
2400
+ break;
942
2401
  case 'remove':
943
2402
  initiator.remove();
944
2403
  break;
945
2404
  default:
946
- console.log('Unknown command. Available commands: init, sponsor-call, status, remove');
2405
+ sendError('Unknown command. Available commands: init, sponsor-call, status, models, remove');
947
2406
  process.exit(1);
948
2407
  }
949
2408
  }