@agile-vibe-coding/avc 0.1.0 → 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 (290) hide show
  1. package/README.md +2 -0
  2. package/cli/agent-loader.js +21 -0
  3. package/cli/agents/agent-selector.md +129 -0
  4. package/cli/agents/architecture-recommender.md +418 -0
  5. package/cli/agents/database-deep-dive.md +470 -0
  6. package/cli/agents/database-recommender.md +634 -0
  7. package/cli/agents/doc-distributor.md +176 -0
  8. package/cli/agents/documentation-updater.md +203 -0
  9. package/cli/agents/epic-story-decomposer.md +280 -0
  10. package/cli/agents/feature-context-generator.md +91 -0
  11. package/cli/agents/gap-checker-epic.md +52 -0
  12. package/cli/agents/impact-checker-story.md +51 -0
  13. package/cli/agents/migration-guide-generator.md +305 -0
  14. package/cli/agents/mission-scope-generator.md +79 -0
  15. package/cli/agents/mission-scope-validator.md +112 -0
  16. package/cli/agents/project-context-extractor.md +107 -0
  17. package/cli/agents/project-documentation-creator.json +226 -0
  18. package/cli/agents/project-documentation-creator.md +595 -0
  19. package/cli/agents/question-prefiller.md +269 -0
  20. package/cli/agents/refiner-epic.md +39 -0
  21. package/cli/agents/refiner-story.md +42 -0
  22. package/cli/agents/solver-epic-api.json +15 -0
  23. package/cli/agents/solver-epic-api.md +39 -0
  24. package/cli/agents/solver-epic-backend.json +15 -0
  25. package/cli/agents/solver-epic-backend.md +39 -0
  26. package/cli/agents/solver-epic-cloud.json +15 -0
  27. package/cli/agents/solver-epic-cloud.md +39 -0
  28. package/cli/agents/solver-epic-data.json +15 -0
  29. package/cli/agents/solver-epic-data.md +39 -0
  30. package/cli/agents/solver-epic-database.json +15 -0
  31. package/cli/agents/solver-epic-database.md +39 -0
  32. package/cli/agents/solver-epic-developer.json +15 -0
  33. package/cli/agents/solver-epic-developer.md +39 -0
  34. package/cli/agents/solver-epic-devops.json +15 -0
  35. package/cli/agents/solver-epic-devops.md +39 -0
  36. package/cli/agents/solver-epic-frontend.json +15 -0
  37. package/cli/agents/solver-epic-frontend.md +39 -0
  38. package/cli/agents/solver-epic-mobile.json +15 -0
  39. package/cli/agents/solver-epic-mobile.md +39 -0
  40. package/cli/agents/solver-epic-qa.json +15 -0
  41. package/cli/agents/solver-epic-qa.md +39 -0
  42. package/cli/agents/solver-epic-security.json +15 -0
  43. package/cli/agents/solver-epic-security.md +39 -0
  44. package/cli/agents/solver-epic-solution-architect.json +15 -0
  45. package/cli/agents/solver-epic-solution-architect.md +39 -0
  46. package/cli/agents/solver-epic-test-architect.json +15 -0
  47. package/cli/agents/solver-epic-test-architect.md +39 -0
  48. package/cli/agents/solver-epic-ui.json +15 -0
  49. package/cli/agents/solver-epic-ui.md +39 -0
  50. package/cli/agents/solver-epic-ux.json +15 -0
  51. package/cli/agents/solver-epic-ux.md +39 -0
  52. package/cli/agents/solver-story-api.json +15 -0
  53. package/cli/agents/solver-story-api.md +39 -0
  54. package/cli/agents/solver-story-backend.json +15 -0
  55. package/cli/agents/solver-story-backend.md +39 -0
  56. package/cli/agents/solver-story-cloud.json +15 -0
  57. package/cli/agents/solver-story-cloud.md +39 -0
  58. package/cli/agents/solver-story-data.json +15 -0
  59. package/cli/agents/solver-story-data.md +39 -0
  60. package/cli/agents/solver-story-database.json +15 -0
  61. package/cli/agents/solver-story-database.md +39 -0
  62. package/cli/agents/solver-story-developer.json +15 -0
  63. package/cli/agents/solver-story-developer.md +39 -0
  64. package/cli/agents/solver-story-devops.json +15 -0
  65. package/cli/agents/solver-story-devops.md +39 -0
  66. package/cli/agents/solver-story-frontend.json +15 -0
  67. package/cli/agents/solver-story-frontend.md +39 -0
  68. package/cli/agents/solver-story-mobile.json +15 -0
  69. package/cli/agents/solver-story-mobile.md +39 -0
  70. package/cli/agents/solver-story-qa.json +15 -0
  71. package/cli/agents/solver-story-qa.md +39 -0
  72. package/cli/agents/solver-story-security.json +15 -0
  73. package/cli/agents/solver-story-security.md +39 -0
  74. package/cli/agents/solver-story-solution-architect.json +15 -0
  75. package/cli/agents/solver-story-solution-architect.md +39 -0
  76. package/cli/agents/solver-story-test-architect.json +15 -0
  77. package/cli/agents/solver-story-test-architect.md +39 -0
  78. package/cli/agents/solver-story-ui.json +15 -0
  79. package/cli/agents/solver-story-ui.md +39 -0
  80. package/cli/agents/solver-story-ux.json +15 -0
  81. package/cli/agents/solver-story-ux.md +39 -0
  82. package/cli/agents/story-doc-enricher.md +133 -0
  83. package/cli/agents/suggestion-business-analyst.md +88 -0
  84. package/cli/agents/suggestion-deployment-architect.md +263 -0
  85. package/cli/agents/suggestion-product-manager.md +129 -0
  86. package/cli/agents/suggestion-security-specialist.md +156 -0
  87. package/cli/agents/suggestion-technical-architect.md +269 -0
  88. package/cli/agents/suggestion-ux-researcher.md +93 -0
  89. package/cli/agents/task-subtask-decomposer.md +188 -0
  90. package/cli/agents/validator-documentation.json +152 -0
  91. package/cli/agents/validator-documentation.md +453 -0
  92. package/cli/agents/validator-epic-api.json +93 -0
  93. package/cli/agents/validator-epic-api.md +137 -0
  94. package/cli/agents/validator-epic-backend.json +93 -0
  95. package/cli/agents/validator-epic-backend.md +130 -0
  96. package/cli/agents/validator-epic-cloud.json +93 -0
  97. package/cli/agents/validator-epic-cloud.md +137 -0
  98. package/cli/agents/validator-epic-data.json +93 -0
  99. package/cli/agents/validator-epic-data.md +130 -0
  100. package/cli/agents/validator-epic-database.json +93 -0
  101. package/cli/agents/validator-epic-database.md +137 -0
  102. package/cli/agents/validator-epic-developer.json +74 -0
  103. package/cli/agents/validator-epic-developer.md +153 -0
  104. package/cli/agents/validator-epic-devops.json +74 -0
  105. package/cli/agents/validator-epic-devops.md +153 -0
  106. package/cli/agents/validator-epic-frontend.json +74 -0
  107. package/cli/agents/validator-epic-frontend.md +153 -0
  108. package/cli/agents/validator-epic-mobile.json +93 -0
  109. package/cli/agents/validator-epic-mobile.md +130 -0
  110. package/cli/agents/validator-epic-qa.json +93 -0
  111. package/cli/agents/validator-epic-qa.md +130 -0
  112. package/cli/agents/validator-epic-security.json +74 -0
  113. package/cli/agents/validator-epic-security.md +154 -0
  114. package/cli/agents/validator-epic-solution-architect.json +74 -0
  115. package/cli/agents/validator-epic-solution-architect.md +156 -0
  116. package/cli/agents/validator-epic-test-architect.json +93 -0
  117. package/cli/agents/validator-epic-test-architect.md +130 -0
  118. package/cli/agents/validator-epic-ui.json +93 -0
  119. package/cli/agents/validator-epic-ui.md +130 -0
  120. package/cli/agents/validator-epic-ux.json +93 -0
  121. package/cli/agents/validator-epic-ux.md +130 -0
  122. package/cli/agents/validator-selector.md +211 -0
  123. package/cli/agents/validator-story-api.json +104 -0
  124. package/cli/agents/validator-story-api.md +152 -0
  125. package/cli/agents/validator-story-backend.json +104 -0
  126. package/cli/agents/validator-story-backend.md +152 -0
  127. package/cli/agents/validator-story-cloud.json +104 -0
  128. package/cli/agents/validator-story-cloud.md +152 -0
  129. package/cli/agents/validator-story-data.json +104 -0
  130. package/cli/agents/validator-story-data.md +152 -0
  131. package/cli/agents/validator-story-database.json +104 -0
  132. package/cli/agents/validator-story-database.md +152 -0
  133. package/cli/agents/validator-story-developer.json +104 -0
  134. package/cli/agents/validator-story-developer.md +152 -0
  135. package/cli/agents/validator-story-devops.json +104 -0
  136. package/cli/agents/validator-story-devops.md +152 -0
  137. package/cli/agents/validator-story-frontend.json +104 -0
  138. package/cli/agents/validator-story-frontend.md +152 -0
  139. package/cli/agents/validator-story-mobile.json +104 -0
  140. package/cli/agents/validator-story-mobile.md +152 -0
  141. package/cli/agents/validator-story-qa.json +104 -0
  142. package/cli/agents/validator-story-qa.md +152 -0
  143. package/cli/agents/validator-story-security.json +104 -0
  144. package/cli/agents/validator-story-security.md +152 -0
  145. package/cli/agents/validator-story-solution-architect.json +104 -0
  146. package/cli/agents/validator-story-solution-architect.md +152 -0
  147. package/cli/agents/validator-story-test-architect.json +104 -0
  148. package/cli/agents/validator-story-test-architect.md +152 -0
  149. package/cli/agents/validator-story-ui.json +104 -0
  150. package/cli/agents/validator-story-ui.md +152 -0
  151. package/cli/agents/validator-story-ux.json +104 -0
  152. package/cli/agents/validator-story-ux.md +152 -0
  153. package/cli/ansi-colors.js +21 -0
  154. package/cli/build-docs.js +298 -0
  155. package/cli/ceremony-history.js +369 -0
  156. package/cli/command-logger.js +245 -0
  157. package/cli/components/static-output.js +63 -0
  158. package/cli/console-output-manager.js +94 -0
  159. package/cli/docs-sync.js +306 -0
  160. package/cli/epic-story-validator.js +1174 -0
  161. package/cli/evaluation-prompts.js +1008 -0
  162. package/cli/execution-context.js +195 -0
  163. package/cli/generate-summary-table.js +340 -0
  164. package/cli/index.js +3 -25
  165. package/cli/init-model-config.js +697 -0
  166. package/cli/init.js +1765 -100
  167. package/cli/kanban-server-manager.js +228 -0
  168. package/cli/llm-claude.js +109 -0
  169. package/cli/llm-gemini.js +115 -0
  170. package/cli/llm-mock.js +233 -0
  171. package/cli/llm-openai.js +233 -0
  172. package/cli/llm-provider.js +300 -0
  173. package/cli/llm-token-limits.js +102 -0
  174. package/cli/llm-verifier.js +454 -0
  175. package/cli/logger.js +32 -5
  176. package/cli/message-constants.js +58 -0
  177. package/cli/message-manager.js +334 -0
  178. package/cli/message-types.js +96 -0
  179. package/cli/messaging-api.js +297 -0
  180. package/cli/model-pricing.js +169 -0
  181. package/cli/model-query-engine.js +468 -0
  182. package/cli/model-recommendation-analyzer.js +495 -0
  183. package/cli/model-selector.js +269 -0
  184. package/cli/output-buffer.js +107 -0
  185. package/cli/process-manager.js +332 -0
  186. package/cli/repl-ink.js +5840 -504
  187. package/cli/repl-old.js +4 -4
  188. package/cli/seed-processor.js +792 -0
  189. package/cli/sprint-planning-processor.js +1813 -0
  190. package/cli/template-processor.js +2306 -108
  191. package/cli/templates/project.md +25 -8
  192. package/cli/templates/vitepress-config.mts.template +34 -0
  193. package/cli/token-tracker.js +520 -0
  194. package/cli/tools/generate-story-validators.js +317 -0
  195. package/cli/tools/generate-validators.js +669 -0
  196. package/cli/update-checker.js +19 -17
  197. package/cli/update-notifier.js +4 -4
  198. package/cli/validation-router.js +605 -0
  199. package/cli/verification-tracker.js +563 -0
  200. package/kanban/README.md +386 -0
  201. package/kanban/client/README.md +205 -0
  202. package/kanban/client/components.json +20 -0
  203. package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
  204. package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
  205. package/kanban/client/dist/index.html +16 -0
  206. package/kanban/client/dist/vite.svg +1 -0
  207. package/kanban/client/index.html +15 -0
  208. package/kanban/client/package-lock.json +9442 -0
  209. package/kanban/client/package.json +44 -0
  210. package/kanban/client/postcss.config.js +6 -0
  211. package/kanban/client/public/vite.svg +1 -0
  212. package/kanban/client/src/App.jsx +622 -0
  213. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  214. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
  215. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
  216. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
  217. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  218. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
  219. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
  220. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  221. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
  222. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  223. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  224. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  225. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
  226. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
  227. package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
  228. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  229. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  230. package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
  231. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  232. package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
  233. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  234. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
  235. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  236. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  237. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  238. package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
  239. package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
  240. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
  241. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
  242. package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
  243. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  244. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  245. package/kanban/client/src/components/stats/CostModal.jsx +353 -0
  246. package/kanban/client/src/components/ui/badge.jsx +27 -0
  247. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  248. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  249. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  250. package/kanban/client/src/hooks/useGrouping.js +118 -0
  251. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  252. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  253. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  254. package/kanban/client/src/lib/api.js +401 -0
  255. package/kanban/client/src/lib/status-grouping.js +144 -0
  256. package/kanban/client/src/lib/utils.js +11 -0
  257. package/kanban/client/src/main.jsx +10 -0
  258. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  259. package/kanban/client/src/store/ceremonyStore.js +172 -0
  260. package/kanban/client/src/store/filterStore.js +201 -0
  261. package/kanban/client/src/store/kanbanStore.js +115 -0
  262. package/kanban/client/src/store/processStore.js +65 -0
  263. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  264. package/kanban/client/src/styles/globals.css +59 -0
  265. package/kanban/client/tailwind.config.js +77 -0
  266. package/kanban/client/vite.config.js +28 -0
  267. package/kanban/client/vitest.config.js +28 -0
  268. package/kanban/dev-start.sh +47 -0
  269. package/kanban/package.json +12 -0
  270. package/kanban/server/index.js +516 -0
  271. package/kanban/server/routes/ceremony.js +305 -0
  272. package/kanban/server/routes/costs.js +157 -0
  273. package/kanban/server/routes/processes.js +50 -0
  274. package/kanban/server/routes/settings.js +303 -0
  275. package/kanban/server/routes/websocket.js +276 -0
  276. package/kanban/server/routes/work-items.js +347 -0
  277. package/kanban/server/services/CeremonyService.js +1190 -0
  278. package/kanban/server/services/FileSystemScanner.js +95 -0
  279. package/kanban/server/services/FileWatcher.js +144 -0
  280. package/kanban/server/services/HierarchyBuilder.js +196 -0
  281. package/kanban/server/services/ProcessRegistry.js +122 -0
  282. package/kanban/server/services/WorkItemReader.js +123 -0
  283. package/kanban/server/services/WorkItemRefineService.js +510 -0
  284. package/kanban/server/start.js +49 -0
  285. package/kanban/server/utils/kanban-logger.js +132 -0
  286. package/kanban/server/utils/markdown.js +91 -0
  287. package/kanban/server/utils/status-grouping.js +107 -0
  288. package/kanban/server/workers/sponsor-call-worker.js +84 -0
  289. package/kanban/server/workers/sprint-planning-worker.js +130 -0
  290. package/package.json +34 -7
@@ -0,0 +1,298 @@
1
+ import { execSync, spawn, exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import net from 'net';
6
+ import http from 'http';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ /**
11
+ * Build and serve VitePress documentation
12
+ * Builds the docs and starts a local preview server
13
+ */
14
+ export class DocumentationBuilder {
15
+ constructor(projectRoot = process.cwd()) {
16
+ this.projectRoot = projectRoot;
17
+ this.docsDir = path.join(projectRoot, '.avc', 'documentation');
18
+ this.distDir = path.join(this.docsDir, '.vitepress', 'dist');
19
+ }
20
+
21
+ /**
22
+ * Check if documentation directory exists
23
+ */
24
+ hasDocumentation() {
25
+ return fs.existsSync(this.docsDir);
26
+ }
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
+
35
+ /**
36
+ * Get documentation server port from avc.json config
37
+ * Returns default port 4173 if not configured
38
+ */
39
+ getPort() {
40
+ const configPath = path.join(this.projectRoot, '.avc', 'avc.json');
41
+
42
+ if (!fs.existsSync(configPath)) {
43
+ return 4173; // Default port
44
+ }
45
+
46
+ try {
47
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
48
+ return config.settings?.documentation?.port || 4173;
49
+ } catch (error) {
50
+ console.warn(`Could not read port from avc.json: ${error.message}`);
51
+ return 4173;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if a port is in use
57
+ * @param {number} port - Port number to check
58
+ * @returns {Promise<boolean>} - True if port is in use
59
+ */
60
+ async isPortInUse(port) {
61
+ return new Promise((resolve) => {
62
+ const server = net.createServer();
63
+
64
+ server.once('error', (err) => {
65
+ if (err.code === 'EADDRINUSE') {
66
+ resolve(true); // Port is in use
67
+ } else {
68
+ resolve(false);
69
+ }
70
+ });
71
+
72
+ server.once('listening', () => {
73
+ server.close();
74
+ resolve(false); // Port is available
75
+ });
76
+
77
+ server.listen(port, '127.0.0.1');
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Check if the server on this port is serving our AVC documentation
83
+ * Makes HTTP request and checks for specific AVC metatag to positively identify
84
+ * @param {number} port - Port number to check
85
+ * @returns {Promise<boolean>} - True if it's confirmed to be AVC documentation server
86
+ */
87
+ async isDocumentationServer(port) {
88
+ return new Promise((resolve) => {
89
+ const req = http.get(`http://localhost:${port}/`, {
90
+ timeout: 2000
91
+ }, (res) => {
92
+ let data = '';
93
+
94
+ res.on('data', (chunk) => {
95
+ data += chunk;
96
+ });
97
+
98
+ res.on('end', () => {
99
+ // ONLY return true if we find the specific AVC documentation metatag
100
+ // This ensures we only kill processes we're 100% certain about
101
+ const hasAvcMetatag = data.includes('<meta name="avc-documentation" content="true">') ||
102
+ data.includes('name="avc-documentation"') ||
103
+ data.includes('name="generator" content="Agile Vibe Coding"');
104
+ resolve(hasAvcMetatag);
105
+ });
106
+ });
107
+
108
+ req.on('error', () => {
109
+ resolve(false); // Can't connect or verify
110
+ });
111
+
112
+ req.on('timeout', () => {
113
+ req.destroy();
114
+ resolve(false);
115
+ });
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Find which process is using a port
121
+ * Works cross-platform (Linux, macOS, Windows)
122
+ * @param {number} port - Port number to check
123
+ * @returns {Promise<{pid: number, command: string} | null>} - Process info or null if not found
124
+ */
125
+ async findProcessUsingPort(port) {
126
+ try {
127
+ let command;
128
+ let parseOutput;
129
+
130
+ if (process.platform === 'win32') {
131
+ // Windows: netstat -ano | findstr :PORT
132
+ command = `netstat -ano | findstr :${port}`;
133
+ parseOutput = (output) => {
134
+ const lines = output.split('\n');
135
+ for (const line of lines) {
136
+ if (line.includes(`0.0.0.0:${port}`) || line.includes(`127.0.0.1:${port}`) || line.includes(`[::]:${port}`)) {
137
+ const parts = line.trim().split(/\s+/);
138
+ const pid = parseInt(parts[parts.length - 1]);
139
+ if (pid && !isNaN(pid)) {
140
+ return { pid, command: 'Unknown' }; // Windows netstat doesn't show command
141
+ }
142
+ }
143
+ }
144
+ return null;
145
+ };
146
+ } else {
147
+ // Linux/macOS: lsof -i :PORT
148
+ command = `lsof -i :${port} -t -sTCP:LISTEN`;
149
+ parseOutput = (output) => {
150
+ const pid = parseInt(output.trim());
151
+ if (pid && !isNaN(pid)) {
152
+ // Try to get process name
153
+ try {
154
+ const psOutput = execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf8' });
155
+ return { pid, command: psOutput.trim() };
156
+ } catch {
157
+ return { pid, command: 'Unknown' };
158
+ }
159
+ }
160
+ return null;
161
+ };
162
+ }
163
+
164
+ const output = execSync(command, { encoding: 'utf8', stdio: 'pipe' });
165
+ return parseOutput(output);
166
+
167
+ } catch (error) {
168
+ // Command failed (port not in use or lsof not available)
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Kill a process by PID
175
+ * Works cross-platform (Linux, macOS, Windows)
176
+ * @param {number} pid - Process ID to kill
177
+ * @returns {Promise<boolean>} - True if successfully killed, false otherwise
178
+ */
179
+ async killProcess(pid) {
180
+ try {
181
+ if (process.platform === 'win32') {
182
+ // Windows: taskkill /F /PID <pid>
183
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' });
184
+ } else {
185
+ // Linux/macOS: kill -9 <pid>
186
+ execSync(`kill -9 ${pid}`, { stdio: 'pipe' });
187
+ }
188
+ return true;
189
+ } catch (error) {
190
+ // Failed to kill (permission denied, process not found, etc.)
191
+ return false;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Build and serve the documentation
197
+ * Returns the server URL
198
+ */
199
+ async buildAndServe() {
200
+ if (!this.hasDocumentation()) {
201
+ throw new Error('Documentation not found. Run /init first to create documentation structure.');
202
+ }
203
+
204
+ const port = this.getPort();
205
+
206
+ try {
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)], {
210
+ cwd: this.docsDir,
211
+ stdio: 'pipe'
212
+ });
213
+
214
+ // Wait for server to be ready
215
+ return new Promise((resolve, reject) => {
216
+ let serverReady = false;
217
+
218
+ serverProcess.stdout.on('data', (data) => {
219
+ const output = data.toString();
220
+ console.log(output);
221
+
222
+ // Check if server is ready
223
+ if (output.includes(`http://localhost:${port}`) || output.includes(`localhost:${port}`)) {
224
+ if (!serverReady) {
225
+ serverReady = true;
226
+ resolve({
227
+ url: `http://localhost:${port}`,
228
+ process: serverProcess
229
+ });
230
+ }
231
+ }
232
+ });
233
+
234
+ serverProcess.stderr.on('data', (data) => {
235
+ console.error(data.toString());
236
+ });
237
+
238
+ serverProcess.on('error', (error) => {
239
+ reject(error);
240
+ });
241
+
242
+ serverProcess.on('exit', (code) => {
243
+ if (code !== 0 && !serverReady) {
244
+ reject(new Error(`Server exited with code ${code}`));
245
+ }
246
+ });
247
+
248
+ // Timeout after 10 seconds
249
+ setTimeout(() => {
250
+ if (!serverReady) {
251
+ serverProcess.kill();
252
+ reject(new Error('Server start timeout'));
253
+ }
254
+ }, 10000);
255
+ });
256
+
257
+ } catch (error) {
258
+ throw new Error(`Failed to build documentation: ${error.message}`);
259
+ }
260
+ }
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
+
278
+ /**
279
+ * Build documentation without starting server
280
+ */
281
+ async build() {
282
+ if (!this.hasDocumentation()) {
283
+ throw new Error('Documentation not found. Run /init first to create documentation structure.');
284
+ }
285
+
286
+ this._ensureIgnoreDeadLinks();
287
+
288
+ try {
289
+ // Build asynchronously to avoid blocking the event loop
290
+ await execAsync('npx vitepress build', {
291
+ cwd: this.docsDir
292
+ });
293
+
294
+ } catch (error) {
295
+ throw new Error(`Failed to build documentation: ${error.message}`);
296
+ }
297
+ }
298
+ }
@@ -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
+ }