@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
@@ -0,0 +1,95 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * FileSystemScanner
6
+ * Recursively scans .avc/project/ directory to find all work.json files
7
+ */
8
+ export class FileSystemScanner {
9
+ /**
10
+ * @param {string} projectRoot - Absolute path to project root directory
11
+ */
12
+ constructor(projectRoot) {
13
+ this.projectRoot = projectRoot;
14
+ this.avcProjectPath = path.join(projectRoot, '.avc', 'project');
15
+ }
16
+
17
+ /**
18
+ * Scan the .avc/project/ directory and return all work.json file paths
19
+ * @returns {Promise<Array<string>>} Array of absolute paths to work.json files
20
+ */
21
+ async scan() {
22
+ try {
23
+ // Check if .avc/project exists
24
+ await fs.access(this.avcProjectPath);
25
+
26
+ const workFiles = [];
27
+ await this._scanDirectory(this.avcProjectPath, workFiles);
28
+
29
+ return workFiles;
30
+ } catch (error) {
31
+ if (error.code === 'ENOENT') {
32
+ // .avc/project doesn't exist yet
33
+ return [];
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Recursively scan a directory for work.json files
41
+ * @private
42
+ * @param {string} dirPath - Directory to scan
43
+ * @param {Array<string>} results - Accumulator for results
44
+ */
45
+ async _scanDirectory(dirPath, results) {
46
+ try {
47
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
48
+
49
+ for (const entry of entries) {
50
+ const fullPath = path.join(dirPath, entry.name);
51
+
52
+ if (entry.isDirectory()) {
53
+ // Recursively scan subdirectories
54
+ await this._scanDirectory(fullPath, results);
55
+ } else if (entry.isFile() && entry.name === 'work.json') {
56
+ // Found a work.json file
57
+ results.push(fullPath);
58
+ }
59
+ }
60
+ } catch (error) {
61
+ // Log error but continue scanning
62
+ console.error(`Error scanning directory ${dirPath}:`, error.message);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get the relative path from .avc/project/ to a work.json file
68
+ * Useful for extracting the work item ID from the path
69
+ * @param {string} fullPath - Absolute path to work.json
70
+ * @returns {string} Relative path from .avc/project/
71
+ */
72
+ getRelativePath(fullPath) {
73
+ return path.relative(this.avcProjectPath, fullPath);
74
+ }
75
+
76
+ /**
77
+ * Extract work item ID from file path
78
+ * Example: .avc/project/context-0001/context-0001-0001/work.json → context-0001-0001
79
+ * @param {string} fullPath - Absolute path to work.json
80
+ * @returns {string|null} Work item ID or null if invalid path
81
+ */
82
+ extractIdFromPath(fullPath) {
83
+ const relativePath = this.getRelativePath(fullPath);
84
+ const dir = path.dirname(relativePath);
85
+
86
+ // Handle root-level work.json (shouldn't exist in normal hierarchy)
87
+ if (dir === '.') {
88
+ return null;
89
+ }
90
+
91
+ // Extract the deepest folder name (the work item ID)
92
+ // Example: context-0001/context-0001-0001 → context-0001-0001
93
+ return path.basename(dir);
94
+ }
95
+ }
@@ -0,0 +1,144 @@
1
+ import chokidar from 'chokidar';
2
+ import { EventEmitter } from 'events';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * FileWatcher
7
+ * Watches .avc/project/ for changes to work.json files
8
+ * Emits events when files are added, changed, or deleted
9
+ */
10
+ export class FileWatcher extends EventEmitter {
11
+ /**
12
+ * @param {string} projectRoot - Absolute path to project root directory
13
+ */
14
+ constructor(projectRoot) {
15
+ super();
16
+ this.projectRoot = projectRoot;
17
+ this.avcPath = path.join(projectRoot, '.avc');
18
+ this.avcProjectPath = path.join(projectRoot, '.avc', 'project');
19
+ this.watcher = null;
20
+ this.dirWatcher = null;
21
+ this.avcDirWatcher = null;
22
+ }
23
+
24
+ /**
25
+ * Start watching for file changes
26
+ */
27
+ start() {
28
+ if (this.watcher) {
29
+ console.warn('FileWatcher already started');
30
+ return;
31
+ }
32
+
33
+ // Watch all work.json files in .avc/project/
34
+ const watchPattern = path.join(this.avcProjectPath, '**/work.json');
35
+
36
+ this.watcher = chokidar.watch(watchPattern, {
37
+ persistent: true,
38
+ ignoreInitial: true, // Don't emit events for initial scan
39
+ usePolling: true, // Required for WSL2 /mnt/ paths (no inotify on Windows mounts)
40
+ interval: 2000, // Poll every 2 seconds
41
+ awaitWriteFinish: {
42
+ // Wait for file writes to complete before emitting event
43
+ stabilityThreshold: 500,
44
+ pollInterval: 100,
45
+ },
46
+ });
47
+
48
+ // File added
49
+ this.watcher.on('add', (filePath) => {
50
+ this.emit('added', filePath);
51
+ });
52
+
53
+ // File changed
54
+ this.watcher.on('change', (filePath) => {
55
+ this.emit('changed', filePath);
56
+ });
57
+
58
+ // File deleted
59
+ this.watcher.on('unlink', (filePath) => {
60
+ this.emit('deleted', filePath);
61
+ });
62
+
63
+ // Directory deleted inside .avc/project/
64
+ this.watcher.on('unlinkDir', (dirPath) => {
65
+ this.emit('deleted', dirPath);
66
+ });
67
+
68
+ // Error
69
+ this.watcher.on('error', (error) => {
70
+ this.emit('error', error);
71
+ });
72
+
73
+ // Ready (initial scan complete)
74
+ this.watcher.on('ready', () => {
75
+ this.emit('ready');
76
+ });
77
+
78
+ // Secondary watcher: explicitly watch the .avc/project directory itself.
79
+ // On WSL2, polling a file-glob may not reliably emit unlinkDir when the
80
+ // entire .avc/ tree is deleted (e.g. by /remove). Watching the directory
81
+ // path directly guarantees detection on the next 2-second poll cycle.
82
+ this.dirWatcher = chokidar.watch(this.avcProjectPath, {
83
+ persistent: true,
84
+ ignoreInitial: true,
85
+ usePolling: true,
86
+ interval: 2000,
87
+ depth: 0, // Only care about the directory itself, not its contents
88
+ });
89
+
90
+ this.dirWatcher.on('unlinkDir', () => {
91
+ this.emit('deleted', this.avcProjectPath);
92
+ });
93
+
94
+ this.dirWatcher.on('error', (error) => {
95
+ this.emit('error', error);
96
+ });
97
+
98
+ // Tertiary watcher: watch .avc/ itself so that when /remove deletes the
99
+ // entire .avc/ tree, we reliably detect it on the next poll cycle.
100
+ // On WSL2, chokidar polling may not fire unlinkDir for .avc/project/ when
101
+ // its parent is deleted — watching the parent directly fixes this.
102
+ this.avcDirWatcher = chokidar.watch(this.avcPath, {
103
+ persistent: true,
104
+ ignoreInitial: true,
105
+ usePolling: true,
106
+ interval: 2000,
107
+ depth: 0,
108
+ });
109
+
110
+ this.avcDirWatcher.on('unlinkDir', () => {
111
+ this.emit('deleted', this.avcProjectPath);
112
+ });
113
+
114
+ this.avcDirWatcher.on('error', (error) => {
115
+ this.emit('error', error);
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Stop watching for file changes
121
+ */
122
+ async stop() {
123
+ if (this.watcher) {
124
+ await this.watcher.close();
125
+ this.watcher = null;
126
+ }
127
+ if (this.dirWatcher) {
128
+ await this.dirWatcher.close();
129
+ this.dirWatcher = null;
130
+ }
131
+ if (this.avcDirWatcher) {
132
+ await this.avcDirWatcher.close();
133
+ this.avcDirWatcher = null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check if watcher is active
139
+ * @returns {boolean}
140
+ */
141
+ isWatching() {
142
+ return this.watcher !== null;
143
+ }
144
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * HierarchyBuilder
3
+ * Builds parent-child relationships for work items
4
+ */
5
+ export class HierarchyBuilder {
6
+ /**
7
+ * Build hierarchical relationships for work items
8
+ * Adds _parent, _children, _type fields to each work item
9
+ * @param {Array<object>} workItems - Array of work items
10
+ * @returns {object} Object with { items: Map, roots: Array }
11
+ */
12
+ buildHierarchy(workItems) {
13
+ // Create a map of id → work item for fast lookup
14
+ const itemsMap = new Map();
15
+
16
+ // First pass: Create map and initialize fields
17
+ workItems.forEach((item) => {
18
+ // Determine type from ID structure
19
+ item._type = this.getWorkItemType(item.id);
20
+
21
+ // Determine parent ID
22
+ item._parentId = this.getParentId(item.id);
23
+
24
+ // Initialize children array
25
+ item._children = [];
26
+
27
+ // Add to map
28
+ itemsMap.set(item.id, item);
29
+ });
30
+
31
+ // Second pass: Build parent-child relationships
32
+ const roots = [];
33
+
34
+ workItems.forEach((item) => {
35
+ if (item._parentId) {
36
+ // Has a parent - add to parent's children array
37
+ const parent = itemsMap.get(item._parentId);
38
+ if (parent) {
39
+ parent._children.push(item);
40
+ item._parent = parent;
41
+ } else {
42
+ // Parent not found (orphaned item)
43
+ console.warn(`Orphaned work item: ${item.id} (parent ${item._parentId} not found)`);
44
+ roots.push(item);
45
+ }
46
+ } else {
47
+ // No parent - this is a root (epic)
48
+ roots.push(item);
49
+ }
50
+ });
51
+
52
+ // Sort children by ID for consistent ordering
53
+ itemsMap.forEach((item) => {
54
+ if (item._children.length > 0) {
55
+ item._children.sort((a, b) => a.id.localeCompare(b.id));
56
+ }
57
+ });
58
+
59
+ // Sort roots by ID
60
+ roots.sort((a, b) => a.id.localeCompare(b.id));
61
+
62
+ return {
63
+ items: itemsMap,
64
+ roots,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Determine work item type based on ID depth
70
+ * @param {string} id - Work item ID
71
+ * @returns {string} Type: 'epic' | 'story' | 'task' | 'subtask'
72
+ */
73
+ getWorkItemType(id) {
74
+ const parts = id.split('-');
75
+ const depth = parts.length - 1; // Subtract 1 for 'context' prefix
76
+
77
+ switch (depth) {
78
+ case 1:
79
+ return 'epic';
80
+ case 2:
81
+ return 'story';
82
+ case 3:
83
+ return 'task';
84
+ case 4:
85
+ return 'subtask';
86
+ default:
87
+ return 'unknown';
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get parent ID from a work item ID
93
+ * @param {string} id - Work item ID
94
+ * @returns {string|null} Parent ID or null if root level
95
+ */
96
+ getParentId(id) {
97
+ const parts = id.split('-');
98
+
99
+ if (parts.length <= 2) {
100
+ // Root level (epic), no parent
101
+ return null;
102
+ }
103
+
104
+ // Remove the last segment
105
+ return parts.slice(0, -1).join('-');
106
+ }
107
+
108
+ /**
109
+ * Get all ancestors for a work item (path to root)
110
+ * @param {object} item - Work item
111
+ * @returns {Array<object>} Array of ancestors from immediate parent to root
112
+ */
113
+ getAncestors(item) {
114
+ const ancestors = [];
115
+ let current = item._parent;
116
+
117
+ while (current) {
118
+ ancestors.push(current);
119
+ current = current._parent;
120
+ }
121
+
122
+ return ancestors;
123
+ }
124
+
125
+ /**
126
+ * Get all descendants for a work item (recursive children)
127
+ * @param {object} item - Work item
128
+ * @returns {Array<object>} Array of all descendants
129
+ */
130
+ getDescendants(item) {
131
+ const descendants = [];
132
+
133
+ const traverse = (node) => {
134
+ if (node._children && node._children.length > 0) {
135
+ node._children.forEach((child) => {
136
+ descendants.push(child);
137
+ traverse(child);
138
+ });
139
+ }
140
+ };
141
+
142
+ traverse(item);
143
+ return descendants;
144
+ }
145
+
146
+ /**
147
+ * Get the root epic for a work item
148
+ * @param {object} item - Work item
149
+ * @returns {object|null} Root epic or null if already a root
150
+ */
151
+ getRootEpic(item) {
152
+ if (!item._parent) {
153
+ // Already a root
154
+ return item._type === 'epic' ? item : null;
155
+ }
156
+
157
+ let current = item._parent;
158
+ while (current._parent) {
159
+ current = current._parent;
160
+ }
161
+
162
+ return current._type === 'epic' ? current : null;
163
+ }
164
+
165
+ /**
166
+ * Calculate progress statistics for a work item based on children
167
+ * @param {object} item - Work item
168
+ * @returns {object} Progress statistics
169
+ */
170
+ calculateProgress(item) {
171
+ if (!item._children || item._children.length === 0) {
172
+ // Leaf node - progress based on own status
173
+ return {
174
+ total: 1,
175
+ completed: item.status === 'completed' ? 1 : 0,
176
+ percentage: item.status === 'completed' ? 100 : 0,
177
+ };
178
+ }
179
+
180
+ // Aggregate children's progress
181
+ let total = 0;
182
+ let completed = 0;
183
+
184
+ item._children.forEach((child) => {
185
+ const childProgress = this.calculateProgress(child);
186
+ total += childProgress.total;
187
+ completed += childProgress.completed;
188
+ });
189
+
190
+ return {
191
+ total,
192
+ completed,
193
+ percentage: total > 0 ? Math.round((completed / total) * 100) : 0,
194
+ };
195
+ }
196
+ }
@@ -0,0 +1,122 @@
1
+ import { EventEmitter } from 'events';
2
+ import { randomBytes } from 'crypto';
3
+
4
+ /**
5
+ * ProcessRegistry
6
+ * Tracks forked ceremony/CLI processes: status, lightweight log ring-buffer, IPC control.
7
+ */
8
+ export class ProcessRegistry extends EventEmitter {
9
+ constructor() {
10
+ super();
11
+ this._processes = new Map(); // processId → ProcessRecord
12
+ this.LOG_CAP = 500;
13
+ }
14
+
15
+ /**
16
+ * Allocate a new ProcessRecord (before the child is forked).
17
+ * @returns {object} The new record
18
+ */
19
+ create(type, label) {
20
+ const id = `${type}-${Date.now()}-${randomBytes(3).toString('hex')}`;
21
+ const record = {
22
+ id,
23
+ type,
24
+ label,
25
+ status: 'running',
26
+ startedAt: Date.now(),
27
+ endedAt: null,
28
+ childProcess: null,
29
+ logs: [],
30
+ result: null,
31
+ error: null,
32
+ };
33
+ this._processes.set(id, record);
34
+ this.emit('created', record);
35
+ return record;
36
+ }
37
+
38
+ /** Attach the ChildProcess handle after fork(). */
39
+ attach(processId, child) {
40
+ const r = this._processes.get(processId);
41
+ if (r) r.childProcess = child;
42
+ }
43
+
44
+ /** Append a log entry (capped at LOG_CAP). */
45
+ appendLog(processId, entry) {
46
+ const r = this._processes.get(processId);
47
+ if (!r) return;
48
+ r.logs.push(entry);
49
+ if (r.logs.length > this.LOG_CAP) r.logs = r.logs.slice(-this.LOG_CAP);
50
+ this.emit('log', processId, entry);
51
+ }
52
+
53
+ /** Update status and optional extra fields (result/error). */
54
+ setStatus(processId, status, extra = {}) {
55
+ const r = this._processes.get(processId);
56
+ if (!r) return;
57
+ r.status = status;
58
+ if (['complete', 'error', 'cancelled'].includes(status)) {
59
+ r.endedAt = Date.now();
60
+ r.childProcess = null;
61
+ }
62
+ Object.assign(r, extra);
63
+ this.emit('status', processId, status, r);
64
+ }
65
+
66
+ get(id) { return this._processes.get(id) ?? null; }
67
+
68
+ /** Alias for get() — processId is used as the map key. */
69
+ getByProcessId(processId) { return this.get(processId); }
70
+
71
+ /** All records as plain DTOs (no ChildProcess reference). */
72
+ list() { return [...this._processes.values()].map(r => this._dto(r)); }
73
+
74
+ getDTO(id) {
75
+ const r = this._processes.get(id);
76
+ return r ? this._dto(r) : null;
77
+ }
78
+
79
+ /** Send cancel IPC; force-kill after 5 s if still alive. */
80
+ kill(id) {
81
+ const r = this._processes.get(id);
82
+ if (!r?.childProcess) return false;
83
+ try { r.childProcess.send({ type: 'cancel' }); } catch (_) {}
84
+ setTimeout(() => { try { r.childProcess?.kill('SIGTERM'); } catch (_) {} }, 5000);
85
+ return true;
86
+ }
87
+
88
+ pause(id) {
89
+ const r = this._processes.get(id);
90
+ if (r?.childProcess) { try { r.childProcess.send({ type: 'pause' }); } catch (_) {} return true; }
91
+ return false;
92
+ }
93
+
94
+ resume(id) {
95
+ const r = this._processes.get(id);
96
+ if (r?.childProcess) { try { r.childProcess.send({ type: 'resume' }); } catch (_) {} return true; }
97
+ return false;
98
+ }
99
+
100
+ /** Remove all finished records (complete/error/cancelled). */
101
+ clearCompleted() {
102
+ for (const [id, r] of this._processes) {
103
+ if (['complete', 'error', 'cancelled'].includes(r.status)) {
104
+ this._processes.delete(id);
105
+ }
106
+ }
107
+ }
108
+
109
+ _dto(r) {
110
+ return {
111
+ id: r.id,
112
+ type: r.type,
113
+ label: r.label,
114
+ status: r.status,
115
+ startedAt: r.startedAt,
116
+ endedAt: r.endedAt,
117
+ result: r.result,
118
+ error: r.error,
119
+ // logs are NOT included — ceremony stores accumulate via WS events
120
+ };
121
+ }
122
+ }