@agile-vibe-coding/avc 0.1.1 → 0.2.3

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 (289) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +129 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/database-deep-dive.md +470 -0
  5. package/cli/agents/database-recommender.md +634 -0
  6. package/cli/agents/doc-distributor.md +176 -0
  7. package/cli/agents/documentation-updater.md +203 -0
  8. package/cli/agents/epic-story-decomposer.md +280 -0
  9. package/cli/agents/feature-context-generator.md +91 -0
  10. package/cli/agents/gap-checker-epic.md +52 -0
  11. package/cli/agents/impact-checker-story.md +51 -0
  12. package/cli/agents/migration-guide-generator.md +305 -0
  13. package/cli/agents/mission-scope-generator.md +79 -0
  14. package/cli/agents/mission-scope-validator.md +112 -0
  15. package/cli/agents/project-context-extractor.md +107 -0
  16. package/cli/agents/project-documentation-creator.json +226 -0
  17. package/cli/agents/project-documentation-creator.md +595 -0
  18. package/cli/agents/question-prefiller.md +269 -0
  19. package/cli/agents/refiner-epic.md +39 -0
  20. package/cli/agents/refiner-story.md +42 -0
  21. package/cli/agents/solver-epic-api.json +15 -0
  22. package/cli/agents/solver-epic-api.md +39 -0
  23. package/cli/agents/solver-epic-backend.json +15 -0
  24. package/cli/agents/solver-epic-backend.md +39 -0
  25. package/cli/agents/solver-epic-cloud.json +15 -0
  26. package/cli/agents/solver-epic-cloud.md +39 -0
  27. package/cli/agents/solver-epic-data.json +15 -0
  28. package/cli/agents/solver-epic-data.md +39 -0
  29. package/cli/agents/solver-epic-database.json +15 -0
  30. package/cli/agents/solver-epic-database.md +39 -0
  31. package/cli/agents/solver-epic-developer.json +15 -0
  32. package/cli/agents/solver-epic-developer.md +39 -0
  33. package/cli/agents/solver-epic-devops.json +15 -0
  34. package/cli/agents/solver-epic-devops.md +39 -0
  35. package/cli/agents/solver-epic-frontend.json +15 -0
  36. package/cli/agents/solver-epic-frontend.md +39 -0
  37. package/cli/agents/solver-epic-mobile.json +15 -0
  38. package/cli/agents/solver-epic-mobile.md +39 -0
  39. package/cli/agents/solver-epic-qa.json +15 -0
  40. package/cli/agents/solver-epic-qa.md +39 -0
  41. package/cli/agents/solver-epic-security.json +15 -0
  42. package/cli/agents/solver-epic-security.md +39 -0
  43. package/cli/agents/solver-epic-solution-architect.json +15 -0
  44. package/cli/agents/solver-epic-solution-architect.md +39 -0
  45. package/cli/agents/solver-epic-test-architect.json +15 -0
  46. package/cli/agents/solver-epic-test-architect.md +39 -0
  47. package/cli/agents/solver-epic-ui.json +15 -0
  48. package/cli/agents/solver-epic-ui.md +39 -0
  49. package/cli/agents/solver-epic-ux.json +15 -0
  50. package/cli/agents/solver-epic-ux.md +39 -0
  51. package/cli/agents/solver-story-api.json +15 -0
  52. package/cli/agents/solver-story-api.md +39 -0
  53. package/cli/agents/solver-story-backend.json +15 -0
  54. package/cli/agents/solver-story-backend.md +39 -0
  55. package/cli/agents/solver-story-cloud.json +15 -0
  56. package/cli/agents/solver-story-cloud.md +39 -0
  57. package/cli/agents/solver-story-data.json +15 -0
  58. package/cli/agents/solver-story-data.md +39 -0
  59. package/cli/agents/solver-story-database.json +15 -0
  60. package/cli/agents/solver-story-database.md +39 -0
  61. package/cli/agents/solver-story-developer.json +15 -0
  62. package/cli/agents/solver-story-developer.md +39 -0
  63. package/cli/agents/solver-story-devops.json +15 -0
  64. package/cli/agents/solver-story-devops.md +39 -0
  65. package/cli/agents/solver-story-frontend.json +15 -0
  66. package/cli/agents/solver-story-frontend.md +39 -0
  67. package/cli/agents/solver-story-mobile.json +15 -0
  68. package/cli/agents/solver-story-mobile.md +39 -0
  69. package/cli/agents/solver-story-qa.json +15 -0
  70. package/cli/agents/solver-story-qa.md +39 -0
  71. package/cli/agents/solver-story-security.json +15 -0
  72. package/cli/agents/solver-story-security.md +39 -0
  73. package/cli/agents/solver-story-solution-architect.json +15 -0
  74. package/cli/agents/solver-story-solution-architect.md +39 -0
  75. package/cli/agents/solver-story-test-architect.json +15 -0
  76. package/cli/agents/solver-story-test-architect.md +39 -0
  77. package/cli/agents/solver-story-ui.json +15 -0
  78. package/cli/agents/solver-story-ui.md +39 -0
  79. package/cli/agents/solver-story-ux.json +15 -0
  80. package/cli/agents/solver-story-ux.md +39 -0
  81. package/cli/agents/story-doc-enricher.md +133 -0
  82. package/cli/agents/suggestion-business-analyst.md +88 -0
  83. package/cli/agents/suggestion-deployment-architect.md +263 -0
  84. package/cli/agents/suggestion-product-manager.md +129 -0
  85. package/cli/agents/suggestion-security-specialist.md +156 -0
  86. package/cli/agents/suggestion-technical-architect.md +269 -0
  87. package/cli/agents/suggestion-ux-researcher.md +93 -0
  88. package/cli/agents/task-subtask-decomposer.md +188 -0
  89. package/cli/agents/validator-documentation.json +152 -0
  90. package/cli/agents/validator-documentation.md +453 -0
  91. package/cli/agents/validator-epic-api.json +93 -0
  92. package/cli/agents/validator-epic-api.md +137 -0
  93. package/cli/agents/validator-epic-backend.json +93 -0
  94. package/cli/agents/validator-epic-backend.md +130 -0
  95. package/cli/agents/validator-epic-cloud.json +93 -0
  96. package/cli/agents/validator-epic-cloud.md +137 -0
  97. package/cli/agents/validator-epic-data.json +93 -0
  98. package/cli/agents/validator-epic-data.md +130 -0
  99. package/cli/agents/validator-epic-database.json +93 -0
  100. package/cli/agents/validator-epic-database.md +137 -0
  101. package/cli/agents/validator-epic-developer.json +74 -0
  102. package/cli/agents/validator-epic-developer.md +153 -0
  103. package/cli/agents/validator-epic-devops.json +74 -0
  104. package/cli/agents/validator-epic-devops.md +153 -0
  105. package/cli/agents/validator-epic-frontend.json +74 -0
  106. package/cli/agents/validator-epic-frontend.md +153 -0
  107. package/cli/agents/validator-epic-mobile.json +93 -0
  108. package/cli/agents/validator-epic-mobile.md +130 -0
  109. package/cli/agents/validator-epic-qa.json +93 -0
  110. package/cli/agents/validator-epic-qa.md +130 -0
  111. package/cli/agents/validator-epic-security.json +74 -0
  112. package/cli/agents/validator-epic-security.md +154 -0
  113. package/cli/agents/validator-epic-solution-architect.json +74 -0
  114. package/cli/agents/validator-epic-solution-architect.md +156 -0
  115. package/cli/agents/validator-epic-test-architect.json +93 -0
  116. package/cli/agents/validator-epic-test-architect.md +130 -0
  117. package/cli/agents/validator-epic-ui.json +93 -0
  118. package/cli/agents/validator-epic-ui.md +130 -0
  119. package/cli/agents/validator-epic-ux.json +93 -0
  120. package/cli/agents/validator-epic-ux.md +130 -0
  121. package/cli/agents/validator-selector.md +211 -0
  122. package/cli/agents/validator-story-api.json +104 -0
  123. package/cli/agents/validator-story-api.md +152 -0
  124. package/cli/agents/validator-story-backend.json +104 -0
  125. package/cli/agents/validator-story-backend.md +152 -0
  126. package/cli/agents/validator-story-cloud.json +104 -0
  127. package/cli/agents/validator-story-cloud.md +152 -0
  128. package/cli/agents/validator-story-data.json +104 -0
  129. package/cli/agents/validator-story-data.md +152 -0
  130. package/cli/agents/validator-story-database.json +104 -0
  131. package/cli/agents/validator-story-database.md +152 -0
  132. package/cli/agents/validator-story-developer.json +104 -0
  133. package/cli/agents/validator-story-developer.md +152 -0
  134. package/cli/agents/validator-story-devops.json +104 -0
  135. package/cli/agents/validator-story-devops.md +152 -0
  136. package/cli/agents/validator-story-frontend.json +104 -0
  137. package/cli/agents/validator-story-frontend.md +152 -0
  138. package/cli/agents/validator-story-mobile.json +104 -0
  139. package/cli/agents/validator-story-mobile.md +152 -0
  140. package/cli/agents/validator-story-qa.json +104 -0
  141. package/cli/agents/validator-story-qa.md +152 -0
  142. package/cli/agents/validator-story-security.json +104 -0
  143. package/cli/agents/validator-story-security.md +152 -0
  144. package/cli/agents/validator-story-solution-architect.json +104 -0
  145. package/cli/agents/validator-story-solution-architect.md +152 -0
  146. package/cli/agents/validator-story-test-architect.json +104 -0
  147. package/cli/agents/validator-story-test-architect.md +152 -0
  148. package/cli/agents/validator-story-ui.json +104 -0
  149. package/cli/agents/validator-story-ui.md +152 -0
  150. package/cli/agents/validator-story-ux.json +104 -0
  151. package/cli/agents/validator-story-ux.md +152 -0
  152. package/cli/ansi-colors.js +21 -0
  153. package/cli/build-docs.js +29 -8
  154. package/cli/ceremony-history.js +369 -0
  155. package/cli/command-logger.js +49 -12
  156. package/cli/components/static-output.js +63 -0
  157. package/cli/console-output-manager.js +94 -0
  158. package/cli/docs-sync.js +306 -0
  159. package/cli/epic-story-validator.js +1174 -0
  160. package/cli/evaluation-prompts.js +1008 -0
  161. package/cli/execution-context.js +195 -0
  162. package/cli/generate-summary-table.js +340 -0
  163. package/cli/index.js +0 -0
  164. package/cli/init-model-config.js +697 -0
  165. package/cli/init.js +1311 -274
  166. package/cli/kanban-server-manager.js +228 -0
  167. package/cli/llm-claude.js +83 -1
  168. package/cli/llm-gemini.js +85 -0
  169. package/cli/llm-mock.js +233 -0
  170. package/cli/llm-openai.js +233 -0
  171. package/cli/llm-provider.js +240 -3
  172. package/cli/llm-token-limits.js +102 -0
  173. package/cli/llm-verifier.js +454 -0
  174. package/cli/message-constants.js +58 -0
  175. package/cli/message-manager.js +334 -0
  176. package/cli/message-types.js +96 -0
  177. package/cli/messaging-api.js +297 -0
  178. package/cli/model-pricing.js +169 -0
  179. package/cli/model-query-engine.js +468 -0
  180. package/cli/model-recommendation-analyzer.js +495 -0
  181. package/cli/model-selector.js +269 -0
  182. package/cli/output-buffer.js +107 -0
  183. package/cli/process-manager.js +73 -2
  184. package/cli/repl-ink.js +4988 -1217
  185. package/cli/repl-old.js +4 -4
  186. package/cli/seed-processor.js +792 -0
  187. package/cli/sprint-planning-processor.js +1813 -0
  188. package/cli/template-processor.js +2102 -105
  189. package/cli/templates/project.md +25 -8
  190. package/cli/templates/vitepress-config.mts.template +5 -4
  191. package/cli/token-tracker.js +520 -0
  192. package/cli/tools/generate-story-validators.js +317 -0
  193. package/cli/tools/generate-validators.js +669 -0
  194. package/cli/update-checker.js +19 -17
  195. package/cli/update-notifier.js +4 -4
  196. package/cli/validation-router.js +605 -0
  197. package/cli/verification-tracker.js +563 -0
  198. package/kanban/README.md +386 -0
  199. package/kanban/client/README.md +205 -0
  200. package/kanban/client/components.json +20 -0
  201. package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
  202. package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
  203. package/kanban/client/dist/index.html +16 -0
  204. package/kanban/client/dist/vite.svg +1 -0
  205. package/kanban/client/index.html +15 -0
  206. package/kanban/client/package-lock.json +9442 -0
  207. package/kanban/client/package.json +44 -0
  208. package/kanban/client/postcss.config.js +6 -0
  209. package/kanban/client/public/vite.svg +1 -0
  210. package/kanban/client/src/App.jsx +622 -0
  211. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  212. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
  213. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
  214. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
  215. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  216. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
  217. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
  218. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  219. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
  220. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  221. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  222. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  223. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
  224. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
  225. package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
  226. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  227. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  228. package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
  229. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  230. package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
  231. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  232. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
  233. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  234. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  235. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  236. package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
  237. package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
  238. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
  239. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
  240. package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
  241. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  242. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  243. package/kanban/client/src/components/stats/CostModal.jsx +353 -0
  244. package/kanban/client/src/components/ui/badge.jsx +27 -0
  245. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  246. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  247. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  248. package/kanban/client/src/hooks/useGrouping.js +118 -0
  249. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  250. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  251. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  252. package/kanban/client/src/lib/api.js +401 -0
  253. package/kanban/client/src/lib/status-grouping.js +144 -0
  254. package/kanban/client/src/lib/utils.js +11 -0
  255. package/kanban/client/src/main.jsx +10 -0
  256. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  257. package/kanban/client/src/store/ceremonyStore.js +172 -0
  258. package/kanban/client/src/store/filterStore.js +201 -0
  259. package/kanban/client/src/store/kanbanStore.js +115 -0
  260. package/kanban/client/src/store/processStore.js +65 -0
  261. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  262. package/kanban/client/src/styles/globals.css +59 -0
  263. package/kanban/client/tailwind.config.js +77 -0
  264. package/kanban/client/vite.config.js +28 -0
  265. package/kanban/client/vitest.config.js +28 -0
  266. package/kanban/dev-start.sh +47 -0
  267. package/kanban/package.json +12 -0
  268. package/kanban/server/index.js +516 -0
  269. package/kanban/server/routes/ceremony.js +305 -0
  270. package/kanban/server/routes/costs.js +157 -0
  271. package/kanban/server/routes/processes.js +50 -0
  272. package/kanban/server/routes/settings.js +303 -0
  273. package/kanban/server/routes/websocket.js +276 -0
  274. package/kanban/server/routes/work-items.js +347 -0
  275. package/kanban/server/services/CeremonyService.js +1190 -0
  276. package/kanban/server/services/FileSystemScanner.js +95 -0
  277. package/kanban/server/services/FileWatcher.js +144 -0
  278. package/kanban/server/services/HierarchyBuilder.js +196 -0
  279. package/kanban/server/services/ProcessRegistry.js +122 -0
  280. package/kanban/server/services/WorkItemReader.js +123 -0
  281. package/kanban/server/services/WorkItemRefineService.js +510 -0
  282. package/kanban/server/start.js +49 -0
  283. package/kanban/server/utils/kanban-logger.js +132 -0
  284. package/kanban/server/utils/markdown.js +91 -0
  285. package/kanban/server/utils/status-grouping.js +107 -0
  286. package/kanban/server/workers/sponsor-call-worker.js +84 -0
  287. package/kanban/server/workers/sprint-planning-worker.js +130 -0
  288. package/package.json +18 -5
  289. package/cli/agents/documentation.md +0 -302
package/cli/build-docs.js CHANGED
@@ -25,6 +25,13 @@ export class DocumentationBuilder {
25
25
  return fs.existsSync(this.docsDir);
26
26
  }
27
27
 
28
+ /**
29
+ * Check if synced project docs directory exists inside documentation
30
+ */
31
+ hasProjectDocs() {
32
+ return fs.existsSync(path.join(this.docsDir, 'project'));
33
+ }
34
+
28
35
  /**
29
36
  * Get documentation server port from avc.json config
30
37
  * Returns default port 4173 if not configured
@@ -40,7 +47,7 @@ export class DocumentationBuilder {
40
47
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
41
48
  return config.settings?.documentation?.port || 4173;
42
49
  } catch (error) {
43
- console.warn(`⚠️ Could not read port from avc.json: ${error.message}`);
50
+ console.warn(`Could not read port from avc.json: ${error.message}`);
44
51
  return 4173;
45
52
  }
46
53
  }
@@ -197,13 +204,9 @@ export class DocumentationBuilder {
197
204
  const port = this.getPort();
198
205
 
199
206
  try {
200
- // Build the documentation asynchronously to avoid blocking
201
- await execAsync('npx vitepress build', {
202
- cwd: this.docsDir
203
- });
204
-
205
- // Start the preview server
206
- const serverProcess = spawn('npx', ['vitepress', 'preview', '--port', String(port)], {
207
+ // Start the dev server no initial build needed, vitepress dev builds on-demand
208
+ // and hot-reloads the browser whenever source .md files change
209
+ const serverProcess = spawn('npx', ['vitepress', 'dev', '--port', String(port)], {
207
210
  cwd: this.docsDir,
208
211
  stdio: 'pipe'
209
212
  });
@@ -256,6 +259,22 @@ export class DocumentationBuilder {
256
259
  }
257
260
  }
258
261
 
262
+ /**
263
+ * Ensure ignoreDeadLinks: true is present in the VitePress config.
264
+ * Patches existing project configs that were created before this option was added.
265
+ */
266
+ _ensureIgnoreDeadLinks() {
267
+ const configPath = path.join(this.docsDir, '.vitepress', 'config.mts');
268
+ if (!fs.existsSync(configPath)) return;
269
+ const content = fs.readFileSync(configPath, 'utf8');
270
+ if (content.includes('ignoreDeadLinks')) return;
271
+ const patched = content.replace(
272
+ /defineConfig\(\{/,
273
+ 'defineConfig({\n ignoreDeadLinks: true,'
274
+ );
275
+ fs.writeFileSync(configPath, patched, 'utf8');
276
+ }
277
+
259
278
  /**
260
279
  * Build documentation without starting server
261
280
  */
@@ -264,6 +283,8 @@ export class DocumentationBuilder {
264
283
  throw new Error('Documentation not found. Run /init first to create documentation structure.');
265
284
  }
266
285
 
286
+ this._ensureIgnoreDeadLinks();
287
+
267
288
  try {
268
289
  // Build asynchronously to avoid blocking the event loop
269
290
  await execAsync('npx vitepress build', {
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Ceremony History Tracker - Records all ceremony executions with status and metadata
3
+ *
4
+ * Tracks when ceremonies are run, their outcomes, and preserves historical data.
5
+ * Helps detect abrupt terminations and provides audit trail.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+
11
+ export class CeremonyHistory {
12
+ constructor(avcPath = path.join(process.cwd(), '.avc')) {
13
+ this.avcPath = avcPath;
14
+ this.historyPath = path.join(avcPath, 'ceremonies-history.json');
15
+ this.data = null;
16
+ }
17
+
18
+ /**
19
+ * Initialize ceremony history file if it doesn't exist
20
+ */
21
+ init() {
22
+ // Ensure .avc directory exists
23
+ if (!fs.existsSync(this.avcPath)) {
24
+ fs.mkdirSync(this.avcPath, { recursive: true });
25
+ }
26
+
27
+ if (!fs.existsSync(this.historyPath)) {
28
+ const initialData = {
29
+ version: "1.0",
30
+ lastUpdated: new Date().toISOString(),
31
+ ceremonies: {}
32
+ };
33
+ this._writeData(initialData);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Load ceremony history from disk
39
+ */
40
+ load() {
41
+ if (fs.existsSync(this.historyPath)) {
42
+ this.data = JSON.parse(fs.readFileSync(this.historyPath, 'utf8'));
43
+ } else {
44
+ this.init();
45
+ this.data = JSON.parse(fs.readFileSync(this.historyPath, 'utf8'));
46
+ }
47
+ return this.data;
48
+ }
49
+
50
+ /**
51
+ * Write data to disk atomically
52
+ */
53
+ _writeData(data) {
54
+ try {
55
+ const tempPath = this.historyPath + '.tmp';
56
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
57
+ fs.renameSync(tempPath, this.historyPath);
58
+ } catch (error) {
59
+ // Fallback to direct write if atomic write fails
60
+ try {
61
+ fs.writeFileSync(this.historyPath, JSON.stringify(data, null, 2), 'utf8');
62
+ } catch (fallbackError) {
63
+ console.error(`Failed to write ceremony history: ${fallbackError.message}`);
64
+ console.error(`Path: ${this.historyPath}`);
65
+ throw fallbackError;
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Start a new ceremony execution
72
+ * @param {string} ceremonyName - e.g., 'sponsor-call'
73
+ * @param {string} stage - Initial stage (default: 'questionnaire')
74
+ * @returns {string} Execution ID
75
+ */
76
+ startExecution(ceremonyName, stage = 'questionnaire') {
77
+ this.load();
78
+
79
+ const now = new Date();
80
+ const timestamp = now.toISOString()
81
+ .replace(/T/, '-')
82
+ .replace(/:/g, '-')
83
+ .replace(/\..+/, '');
84
+
85
+ const executionId = `${ceremonyName}-${timestamp}`;
86
+
87
+ // Initialize ceremony entry if doesn't exist
88
+ if (!this.data.ceremonies[ceremonyName]) {
89
+ this.data.ceremonies[ceremonyName] = {
90
+ executions: [],
91
+ totalExecutions: 0,
92
+ lastRun: null,
93
+ lastSuccess: null
94
+ };
95
+ }
96
+
97
+ // Create execution record
98
+ const execution = {
99
+ id: executionId,
100
+ startTime: now.toISOString(),
101
+ endTime: null,
102
+ status: 'in-progress',
103
+ stage: stage,
104
+ answers: null,
105
+ filesGenerated: [],
106
+ tokenUsage: null,
107
+ duration: null,
108
+ outcome: null
109
+ };
110
+
111
+ // Add to executions array
112
+ this.data.ceremonies[ceremonyName].executions.push(execution);
113
+ this.data.ceremonies[ceremonyName].lastRun = now.toISOString();
114
+ this.data.lastUpdated = now.toISOString();
115
+
116
+ this._writeData(this.data);
117
+
118
+ return executionId;
119
+ }
120
+
121
+ /**
122
+ * Update an existing execution
123
+ * @param {string} ceremonyName - Ceremony name
124
+ * @param {string} executionId - Execution ID
125
+ * @param {Object} updates - Fields to update
126
+ */
127
+ updateExecution(ceremonyName, executionId, updates) {
128
+ this.load();
129
+
130
+ if (!this.data.ceremonies[ceremonyName]) {
131
+ throw new Error(`Ceremony '${ceremonyName}' not found in history`);
132
+ }
133
+
134
+ const execution = this.data.ceremonies[ceremonyName].executions.find(
135
+ e => e.id === executionId
136
+ );
137
+
138
+ if (!execution) {
139
+ throw new Error(`Execution '${executionId}' not found for ceremony '${ceremonyName}'`);
140
+ }
141
+
142
+ // Update fields
143
+ Object.assign(execution, updates);
144
+ this.data.lastUpdated = new Date().toISOString();
145
+
146
+ this._writeData(this.data);
147
+ }
148
+
149
+ /**
150
+ * Complete an execution
151
+ * @param {string} ceremonyName - Ceremony name
152
+ * @param {string} executionId - Execution ID
153
+ * @param {string} outcome - 'success', 'user-cancelled', or 'abrupt-termination'
154
+ * @param {Object} metadata - Additional data (answers, filesGenerated, tokenUsage, etc.)
155
+ */
156
+ completeExecution(ceremonyName, executionId, outcome, metadata = {}) {
157
+ this.load();
158
+
159
+ if (!this.data.ceremonies[ceremonyName]) {
160
+ throw new Error(`Ceremony '${ceremonyName}' not found in history`);
161
+ }
162
+
163
+ const execution = this.data.ceremonies[ceremonyName].executions.find(
164
+ e => e.id === executionId
165
+ );
166
+
167
+ if (!execution) {
168
+ throw new Error(`Execution '${executionId}' not found for ceremony '${ceremonyName}'`);
169
+ }
170
+
171
+ const now = new Date();
172
+ const startTime = new Date(execution.startTime);
173
+ const duration = now - startTime;
174
+
175
+ // Determine status based on outcome
176
+ let status;
177
+ switch (outcome) {
178
+ case 'success':
179
+ status = 'completed';
180
+ break;
181
+ case 'user-cancelled':
182
+ status = 'cancelled';
183
+ break;
184
+ case 'abrupt-termination':
185
+ status = 'aborted';
186
+ break;
187
+ default:
188
+ status = 'completed';
189
+ }
190
+
191
+ // Update execution
192
+ execution.status = status;
193
+ execution.endTime = now.toISOString();
194
+ execution.outcome = outcome;
195
+ execution.duration = duration;
196
+
197
+ // Merge metadata
198
+ if (metadata.answers) execution.answers = metadata.answers;
199
+ if (metadata.filesGenerated) execution.filesGenerated = metadata.filesGenerated;
200
+ if (metadata.tokenUsage) execution.tokenUsage = metadata.tokenUsage;
201
+ if (metadata.cost) execution.cost = metadata.cost;
202
+ if (metadata.model) execution.model = metadata.model;
203
+ if (metadata.stage) execution.stage = metadata.stage;
204
+ if (metadata.error) execution.error = metadata.error;
205
+ if (metadata.note) execution.note = metadata.note;
206
+
207
+ // Update ceremony totals
208
+ this.data.ceremonies[ceremonyName].totalExecutions =
209
+ this.data.ceremonies[ceremonyName].executions.length;
210
+
211
+ if (outcome === 'success') {
212
+ this.data.ceremonies[ceremonyName].lastSuccess = now.toISOString();
213
+ }
214
+
215
+ this.data.lastUpdated = now.toISOString();
216
+
217
+ this._writeData(this.data);
218
+ }
219
+
220
+ /**
221
+ * Archive answers to an execution (before LLM generation starts)
222
+ * @param {string} ceremonyName - Ceremony name
223
+ * @param {string} executionId - Execution ID
224
+ * @param {Object} answers - Questionnaire answers
225
+ */
226
+ archiveAnswers(ceremonyName, executionId, answers) {
227
+ this.updateExecution(ceremonyName, executionId, {
228
+ answers: { ...answers }
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Get the most recent execution for a ceremony
234
+ * @param {string} ceremonyName - Ceremony name
235
+ * @returns {Object|null} Execution record or null
236
+ */
237
+ getLastExecution(ceremonyName) {
238
+ this.load();
239
+
240
+ if (!this.data.ceremonies[ceremonyName]) {
241
+ return null;
242
+ }
243
+
244
+ const executions = this.data.ceremonies[ceremonyName].executions;
245
+ if (executions.length === 0) {
246
+ return null;
247
+ }
248
+
249
+ // Return most recent (last in array)
250
+ return executions[executions.length - 1];
251
+ }
252
+
253
+ /**
254
+ * Get a specific execution by ID
255
+ * @param {string} ceremonyName - Ceremony name
256
+ * @param {string} executionId - Execution ID
257
+ * @returns {Object|null} Execution record or null
258
+ */
259
+ getExecutionById(ceremonyName, executionId) {
260
+ this.load();
261
+
262
+ if (!this.data.ceremonies[ceremonyName]) {
263
+ return null;
264
+ }
265
+
266
+ return this.data.ceremonies[ceremonyName].executions.find(
267
+ e => e.id === executionId
268
+ ) || null;
269
+ }
270
+
271
+ /**
272
+ * Get all executions for a ceremony
273
+ * @param {string} ceremonyName - Ceremony name
274
+ * @returns {Array} Array of execution records (newest first)
275
+ */
276
+ getAllExecutions(ceremonyName) {
277
+ this.load();
278
+
279
+ if (!this.data.ceremonies[ceremonyName]) {
280
+ return [];
281
+ }
282
+
283
+ // Return copy, sorted by startTime descending
284
+ return [...this.data.ceremonies[ceremonyName].executions]
285
+ .sort((a, b) => new Date(b.startTime) - new Date(a.startTime));
286
+ }
287
+
288
+ /**
289
+ * Detect if the last execution was abruptly terminated
290
+ * @param {string} ceremonyName - Ceremony name
291
+ * @returns {boolean} True if abrupt termination detected
292
+ */
293
+ detectAbruptTermination(ceremonyName) {
294
+ const lastExecution = this.getLastExecution(ceremonyName);
295
+
296
+ if (!lastExecution) {
297
+ return false;
298
+ }
299
+
300
+ // Check if last execution is still in-progress and in LLM generation stage
301
+ return lastExecution.status === 'in-progress' &&
302
+ lastExecution.stage === 'llm-generation';
303
+ }
304
+
305
+ /**
306
+ * Clean up abrupt termination (mark as aborted)
307
+ * @param {string} ceremonyName - Ceremony name
308
+ */
309
+ cleanupAbruptTermination(ceremonyName) {
310
+ const lastExecution = this.getLastExecution(ceremonyName);
311
+
312
+ if (!lastExecution || lastExecution.status !== 'in-progress') {
313
+ return;
314
+ }
315
+
316
+ this.completeExecution(ceremonyName, lastExecution.id, 'abrupt-termination', {
317
+ note: 'Process was interrupted during LLM generation',
318
+ stage: lastExecution.stage
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Get ceremony statistics
324
+ * @param {string} ceremonyName - Ceremony name
325
+ * @returns {Object} Statistics object
326
+ */
327
+ getStats(ceremonyName) {
328
+ this.load();
329
+
330
+ if (!this.data.ceremonies[ceremonyName]) {
331
+ return {
332
+ totalExecutions: 0,
333
+ successful: 0,
334
+ cancelled: 0,
335
+ aborted: 0,
336
+ lastRun: null,
337
+ lastSuccess: null
338
+ };
339
+ }
340
+
341
+ const ceremony = this.data.ceremonies[ceremonyName];
342
+ const executions = ceremony.executions;
343
+
344
+ return {
345
+ totalExecutions: ceremony.totalExecutions,
346
+ successful: executions.filter(e => e.outcome === 'success').length,
347
+ cancelled: executions.filter(e => e.outcome === 'user-cancelled').length,
348
+ aborted: executions.filter(e => e.outcome === 'abrupt-termination').length,
349
+ lastRun: ceremony.lastRun,
350
+ lastSuccess: ceremony.lastSuccess
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Check if a ceremony has completed successfully at least once
356
+ * @param {string} ceremonyName - Ceremony name
357
+ * @returns {boolean} True if ceremony has a successful completion
358
+ */
359
+ hasSuccessfulCompletion(ceremonyName) {
360
+ this.load();
361
+
362
+ if (!this.data.ceremonies[ceremonyName]) {
363
+ return false;
364
+ }
365
+
366
+ // Check if lastSuccess is set (indicates at least one successful execution)
367
+ return this.data.ceremonies[ceremonyName].lastSuccess !== null;
368
+ }
369
+ }
@@ -11,15 +11,27 @@
11
11
  import fs from 'fs';
12
12
  import path from 'path';
13
13
 
14
+ /** Format a Date as a local ISO-8601 string (with timezone offset, e.g. 2026-03-04T18:05:16.554+01:00) */
15
+ function localISO(date = new Date()) {
16
+ const p = n => String(n).padStart(2, '0');
17
+ const ms = String(date.getMilliseconds()).padStart(3, '0');
18
+ const tz = -date.getTimezoneOffset();
19
+ const sign = tz >= 0 ? '+' : '-';
20
+ const tzH = p(Math.floor(Math.abs(tz) / 60));
21
+ const tzM = p(Math.abs(tz) % 60);
22
+ return `${date.getFullYear()}-${p(date.getMonth()+1)}-${p(date.getDate())}T${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}.${ms}${sign}${tzH}:${tzM}`;
23
+ }
24
+
14
25
  class CommandLogger {
15
- constructor(commandName, projectRoot = process.cwd()) {
26
+ constructor(commandName, projectRoot = process.cwd(), inkMode = false) {
16
27
  this.commandName = commandName;
17
28
  this.projectRoot = projectRoot;
18
29
  this.logsDir = path.join(projectRoot, '.avc', 'logs');
30
+ this.inkMode = inkMode; // Don't forward to console if in Ink mode
19
31
 
20
32
  // Create timestamp for this command execution
21
33
  const now = new Date();
22
- const timestamp = now.toISOString()
34
+ const timestamp = localISO(now)
23
35
  .replace(/T/, '-')
24
36
  .replace(/:/g, '-')
25
37
  .replace(/\..+/, '');
@@ -54,7 +66,7 @@ class CommandLogger {
54
66
  const header = [
55
67
  '='.repeat(80),
56
68
  `AVC Command Log: ${this.commandName}`,
57
- `Timestamp: ${new Date().toISOString()}`,
69
+ `Timestamp: ${localISO()}`,
58
70
  `Project: ${this.projectRoot}`,
59
71
  `Log File: ${this.logFileName}`,
60
72
  '='.repeat(80),
@@ -75,19 +87,32 @@ class CommandLogger {
75
87
  if (!this.logFilePath) return;
76
88
 
77
89
  try {
78
- const timestamp = new Date().toISOString();
90
+ const timestamp = localISO();
79
91
  const message = args.map(arg => {
92
+ if (arg === undefined) return 'undefined';
93
+ if (arg === null) return 'null';
80
94
  if (typeof arg === 'object') {
81
- return JSON.stringify(arg, null, 2);
95
+ try {
96
+ // Handle circular references and non-serializable objects
97
+ return JSON.stringify(arg, (key, value) => {
98
+ if (typeof value === 'function') return '[Function]';
99
+ if (typeof value === 'symbol') return value.toString();
100
+ return value;
101
+ }, 2);
102
+ } catch (jsonError) {
103
+ return `[Object: ${arg.constructor?.name || 'Unknown'}]`;
104
+ }
82
105
  }
83
106
  return String(arg);
84
107
  }).join(' ');
85
108
 
86
- const logEntry = `[${timestamp}] [${level}] ${message}\n`;
109
+ // Preserve formatting but ensure single newline at end
110
+ const logEntry = `[${timestamp}] [${level}] ${message}${message.endsWith('\n') ? '' : '\n'}`;
87
111
 
88
112
  fs.appendFileSync(this.logFilePath, logEntry, 'utf8');
89
113
  } catch (error) {
90
- // Silently fail if we can't write to log
114
+ // Write error to stderr so it doesn't get captured in loop
115
+ process.stderr.write(`[CommandLogger] Failed to write log: ${error.message}\n`);
91
116
  }
92
117
  }
93
118
 
@@ -98,25 +123,37 @@ class CommandLogger {
98
123
  // Intercept console.log
99
124
  console.log = (...args) => {
100
125
  this.writeLog('INFO', ...args);
101
- this.originalLog(...args); // Still output to console
126
+ // Only forward to console if NOT in Ink mode
127
+ if (!this.inkMode) {
128
+ this.originalLog(...args);
129
+ }
102
130
  };
103
131
 
104
132
  // Intercept console.error
105
133
  console.error = (...args) => {
106
134
  this.writeLog('ERROR', ...args);
107
- this.originalError(...args);
135
+ // Only forward to console if NOT in Ink mode
136
+ if (!this.inkMode) {
137
+ this.originalError(...args);
138
+ }
108
139
  };
109
140
 
110
141
  // Intercept console.warn
111
142
  console.warn = (...args) => {
112
143
  this.writeLog('WARN', ...args);
113
- this.originalWarn(...args);
144
+ // Only forward to console if NOT in Ink mode
145
+ if (!this.inkMode) {
146
+ this.originalWarn(...args);
147
+ }
114
148
  };
115
149
 
116
150
  // Intercept console.info
117
151
  console.info = (...args) => {
118
152
  this.writeLog('INFO', ...args);
119
- this.originalInfo(...args);
153
+ // Only forward to console if NOT in Ink mode
154
+ if (!this.inkMode) {
155
+ this.originalInfo(...args);
156
+ }
120
157
  };
121
158
  }
122
159
 
@@ -136,7 +173,7 @@ class CommandLogger {
136
173
  const footer = [
137
174
  '',
138
175
  '='.repeat(80),
139
- `Command completed: ${new Date().toISOString()}`,
176
+ `Command completed: ${localISO()}`,
140
177
  `Log saved: ${this.logFilePath}`,
141
178
  '='.repeat(80)
142
179
  ].join('\n');
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { Static, Text, Box } from 'ink';
3
+
4
+ /**
5
+ * Badge config for message types (inspired by jest example's backgroundColor badges)
6
+ */
7
+ const BADGE = {
8
+ ERROR: { bg: 'red', fg: 'white', label: ' ERR ' },
9
+ WARNING: { bg: 'yellow', fg: 'black', label: ' WRN ' },
10
+ SUCCESS: { bg: 'green', fg: 'black', label: ' OK ' },
11
+ INFO: { bg: 'blue', fg: 'white', label: ' INF ' },
12
+ };
13
+
14
+ /**
15
+ * StaticOutput - Renders output items using Ink's built-in Static component.
16
+ *
17
+ * Ink's <Static> commits each item to the terminal once and never touches
18
+ * it again. Items are NOT part of Ink's height-tracking calculation, so
19
+ * they never cause ghost renders when the interactive section changes height.
20
+ *
21
+ * Items with a `type` field get a colored background badge prefix
22
+ * (ERR/WRN/OK/INF) inspired by the jest example's test status chips.
23
+ *
24
+ * @param {Object} props
25
+ * @param {{id: number, content: string, type?: string}[]} props.items
26
+ * @returns {React.Element|null}
27
+ */
28
+ export const StaticOutput = ({ items }) => {
29
+ if (!items || items.length === 0) return null;
30
+
31
+ return React.createElement(Static, { items },
32
+ (item) => {
33
+ const badge = item.type ? BADGE[item.type] : null;
34
+
35
+ if (badge) {
36
+ // Strip the "TYPE: " prefix from content — badge already shows the type visually
37
+ const displayContent = item.content.replace(/^(ERROR|WARNING|SUCCESS|INFO): /, '');
38
+ const lines = displayContent.split('\n');
39
+
40
+ if (lines.length === 1) {
41
+ // Single-line: badge + text side by side
42
+ return React.createElement(Box, { key: item.id, flexDirection: 'row', gap: 1 },
43
+ React.createElement(Text, { backgroundColor: badge.bg, color: badge.fg }, badge.label),
44
+ React.createElement(Text, null, displayContent)
45
+ );
46
+ }
47
+
48
+ // Multi-line: badge on first line only, subsequent lines fully left-aligned
49
+ return React.createElement(Box, { key: item.id, flexDirection: 'column' },
50
+ React.createElement(Box, { flexDirection: 'row', gap: 1 },
51
+ React.createElement(Text, { backgroundColor: badge.bg, color: badge.fg }, badge.label),
52
+ React.createElement(Text, null, lines[0])
53
+ ),
54
+ ...lines.slice(1).map((line, i) =>
55
+ React.createElement(Text, { key: i }, line)
56
+ )
57
+ );
58
+ }
59
+
60
+ return React.createElement(Text, { key: item.id }, item.content);
61
+ }
62
+ );
63
+ };