@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,487 @@
1
+ import express from 'express';
2
+ import fs from 'fs/promises';
3
+ import fsSync from 'fs';
4
+ import path from 'path';
5
+ import { renderMarkdown, extractDescriptionFromDoc } from '../utils/markdown.js';
6
+ import { groupItemsByColumn, getColumnStats } from '../utils/status-grouping.js';
7
+
8
+ /**
9
+ * Create work items router
10
+ * @param {object} dataStore - Data store with work items
11
+ * @returns {express.Router}
12
+ */
13
+ export function createWorkItemsRouter(dataStore, refineService, ceremonyService) {
14
+ const router = express.Router();
15
+
16
+ /**
17
+ * GET /api/work-items
18
+ * Get all work items as hierarchical JSON
19
+ */
20
+ router.get('/', (req, res) => {
21
+ try {
22
+ const { items, roots } = dataStore.getHierarchy();
23
+
24
+ // Convert Map to array
25
+ const allItems = Array.from(items.values());
26
+
27
+ // Apply filters if provided
28
+ let filtered = allItems;
29
+
30
+ // Filter by type
31
+ if (req.query.type) {
32
+ const types = req.query.type.split(',');
33
+ filtered = filtered.filter((item) => types.includes(item._type));
34
+ }
35
+
36
+ // Filter by status
37
+ if (req.query.status) {
38
+ const statuses = req.query.status.split(',');
39
+ filtered = filtered.filter((item) => statuses.includes(item.status));
40
+ }
41
+
42
+ // Search query
43
+ if (req.query.search) {
44
+ const search = req.query.search.toLowerCase();
45
+ filtered = filtered.filter(
46
+ (item) =>
47
+ item.name.toLowerCase().includes(search) ||
48
+ item.id.toLowerCase().includes(search) ||
49
+ (item.description && item.description.toLowerCase().includes(search))
50
+ );
51
+ }
52
+
53
+ // Clean items (remove circular references for JSON serialization)
54
+ const cleanItems = filtered.map((item) => cleanWorkItem(item));
55
+
56
+ res.json({
57
+ items: cleanItems,
58
+ total: cleanItems.length,
59
+ roots: roots.map((r) => r.id),
60
+ });
61
+ } catch (error) {
62
+ console.error('Error getting work items:', error);
63
+ res.status(500).json({ error: 'Failed to get work items' });
64
+ }
65
+ });
66
+
67
+ /**
68
+ * GET /api/work-items/grouped
69
+ * Get work items grouped by column
70
+ */
71
+ router.get('/grouped', (req, res) => {
72
+ try {
73
+ const { items } = dataStore.getHierarchy();
74
+ const allItems = Array.from(items.values());
75
+
76
+ // Group by column
77
+ const grouped = groupItemsByColumn(allItems);
78
+
79
+ // Add statistics for each column
80
+ const result = {};
81
+ for (const [column, columnItems] of Object.entries(grouped)) {
82
+ result[column] = {
83
+ items: columnItems.map((item) => cleanWorkItem(item)),
84
+ stats: getColumnStats(columnItems),
85
+ };
86
+ }
87
+
88
+ res.json(result);
89
+ } catch (error) {
90
+ console.error('Error getting grouped work items:', error);
91
+ res.status(500).json({ error: 'Failed to get grouped work items' });
92
+ }
93
+ });
94
+
95
+ /**
96
+ * GET /api/work-items/:id
97
+ * Get single work item with full details
98
+ */
99
+ router.get('/:id', async (req, res) => {
100
+ try {
101
+ const { items } = dataStore.getHierarchy();
102
+ const item = items.get(req.params.id);
103
+
104
+ if (!item) {
105
+ return res.status(404).json({ error: 'Work item not found' });
106
+ }
107
+
108
+ // Get full details (including doc.md and context.md)
109
+ const fullItem = await dataStore.getFullDetails(item);
110
+
111
+ res.json(cleanWorkItem(fullItem, true));
112
+ } catch (error) {
113
+ console.error(`Error getting work item ${req.params.id}:`, error);
114
+ res.status(500).json({ error: 'Failed to get work item' });
115
+ }
116
+ });
117
+
118
+ /**
119
+ * GET /api/work-items/:id/doc
120
+ * Get rendered documentation (doc.md) as HTML
121
+ */
122
+ router.get('/:id/doc', async (req, res) => {
123
+ try {
124
+ const { items } = dataStore.getHierarchy();
125
+ const item = items.get(req.params.id);
126
+
127
+ if (!item) {
128
+ return res.status(404).json({ error: 'Work item not found' });
129
+ }
130
+
131
+ const fullItem = await dataStore.getFullDetails(item);
132
+
133
+ if (!fullItem.documentation) {
134
+ return res.status(404).json({ error: 'Documentation not found' });
135
+ }
136
+
137
+ const html = renderMarkdown(fullItem.documentation);
138
+ res.send(html);
139
+ } catch (error) {
140
+ console.error(`Error getting documentation for ${req.params.id}:`, error);
141
+ res.status(500).json({ error: 'Failed to get documentation' });
142
+ }
143
+ });
144
+
145
+ /**
146
+ * GET /api/work-items/:id/doc/raw
147
+ * Get raw markdown source of doc.md
148
+ */
149
+ router.get('/:id/doc/raw', async (req, res) => {
150
+ try {
151
+ const { items } = dataStore.getHierarchy();
152
+ const item = items.get(req.params.id);
153
+ if (!item) return res.status(404).json({ error: 'Work item not found' });
154
+
155
+ const fullItem = await dataStore.getFullDetails(item);
156
+ if (!fullItem.documentation) return res.status(404).json({ error: 'Documentation not found' });
157
+
158
+ res.type('text/plain').send(fullItem.documentation);
159
+ } catch (error) {
160
+ console.error(`Error getting raw doc for ${req.params.id}:`, error);
161
+ res.status(500).json({ error: 'Failed to get documentation' });
162
+ }
163
+ });
164
+
165
+ /**
166
+ * POST /api/work-items/:id/refine
167
+ * Start an async refinement job for an epic or story.
168
+ * Body: { refinementRequest?, selectedIssues?, modelId, provider, validatorModelId, validatorProvider }
169
+ * Returns: { jobId }
170
+ */
171
+ router.post('/:id/refine', async (req, res) => {
172
+ try {
173
+ if (!refineService) {
174
+ return res.status(503).json({ error: 'Refine service not available' });
175
+ }
176
+ const { items } = dataStore.getHierarchy();
177
+ const item = items.get(req.params.id);
178
+ if (!item) return res.status(404).json({ error: 'Work item not found' });
179
+
180
+ const { refinementRequest, selectedIssues, modelId, provider, validatorModelId, validatorProvider } =
181
+ req.body;
182
+
183
+ if (!modelId || !provider || !validatorModelId || !validatorProvider) {
184
+ return res.status(400).json({
185
+ error: 'modelId, provider, validatorModelId and validatorProvider are required',
186
+ });
187
+ }
188
+
189
+ const fullItem = await dataStore.getFullDetails(item);
190
+ const cleanItem = cleanWorkItem(fullItem, true);
191
+
192
+ const jobId = await refineService.startRefine(req.params.id, cleanItem, {
193
+ refinementRequest: refinementRequest || '',
194
+ selectedIssues: selectedIssues || [],
195
+ modelId,
196
+ provider,
197
+ validatorModelId,
198
+ validatorProvider,
199
+ itemDirPath: item._dirPath,
200
+ });
201
+
202
+ res.json({ jobId });
203
+ } catch (err) {
204
+ console.error(`Error starting refine for ${req.params.id}:`, err);
205
+ res.status(500).json({ error: err.message });
206
+ }
207
+ });
208
+
209
+ /**
210
+ * PUT /api/work-items/:id
211
+ * Apply accepted refinement changes to work.json on disk.
212
+ * Body: { proposedItem, storyChanges? }
213
+ */
214
+ router.put('/:id', async (req, res) => {
215
+ try {
216
+ const cs = ceremonyService?.getStatus();
217
+ if (cs?.status === 'running' && cs?.runningType === 'sprint-planning') {
218
+ return res.status(423).json({ error: 'Work items are read-only while sprint planning is active' });
219
+ }
220
+ if (!refineService) {
221
+ return res.status(503).json({ error: 'Refine service not available' });
222
+ }
223
+ const { proposedItem, storyChanges } = req.body;
224
+ if (!proposedItem) return res.status(400).json({ error: 'proposedItem is required' });
225
+
226
+ // Build a dirPath map from the in-memory data store so applyChanges doesn't
227
+ // need to walk the filesystem (which can silently fail).
228
+ const { items } = dataStore.getHierarchy();
229
+ const item = items.get(req.params.id);
230
+ if (!item) return res.status(404).json({ error: 'Work item not found' });
231
+
232
+ const dirPathMap = new Map([[item.id, item._dirPath]]);
233
+ for (const change of (storyChanges || [])) {
234
+ if (change.storyId) {
235
+ const storyItem = items.get(change.storyId);
236
+ if (storyItem) dirPathMap.set(change.storyId, storyItem._dirPath);
237
+ }
238
+ }
239
+
240
+ await refineService.applyChanges(req.params.id, proposedItem, storyChanges || [], dirPathMap);
241
+ res.json({ status: 'ok' });
242
+ } catch (err) {
243
+ console.error(`Error applying changes for ${req.params.id}:`, err);
244
+ res.status(500).json({ error: err.message });
245
+ }
246
+ });
247
+
248
+ /**
249
+ * PUT /api/work-items/:id/doc
250
+ * Save updated markdown content to doc.md
251
+ */
252
+ router.put('/:id/doc', async (req, res) => {
253
+ try {
254
+ const cs = ceremonyService?.getStatus();
255
+ if (cs?.status === 'running' && cs?.runningType === 'sprint-planning') {
256
+ return res.status(423).json({ error: 'Work items are read-only while sprint planning is active' });
257
+ }
258
+ const { items } = dataStore.getHierarchy();
259
+ const item = items.get(req.params.id);
260
+ if (!item) return res.status(404).json({ error: 'Work item not found' });
261
+
262
+ const markdown = req.body.content;
263
+ if (typeof markdown !== 'string') {
264
+ return res.status(400).json({ error: 'content must be a string' });
265
+ }
266
+
267
+ const docPath = path.join(item._dirPath, 'doc.md');
268
+ await fs.writeFile(docPath, markdown, 'utf8');
269
+
270
+ // Sync work.json description cache so kanban cards stay in sync
271
+ const newDescription = extractDescriptionFromDoc(markdown);
272
+ if (newDescription) {
273
+ const workJsonPath = path.join(item._dirPath, 'work.json');
274
+ if (fsSync.existsSync(workJsonPath)) {
275
+ const workJson = JSON.parse(fsSync.readFileSync(workJsonPath, 'utf8'));
276
+ workJson.description = newDescription;
277
+ fsSync.writeFileSync(workJsonPath, JSON.stringify(workJson, null, 2), 'utf8');
278
+ }
279
+ }
280
+
281
+ res.json({ status: 'ok' });
282
+ } catch (error) {
283
+ console.error(`Error saving doc for ${req.params.id}:`, error);
284
+ res.status(500).json({ error: 'Failed to save documentation' });
285
+ }
286
+ });
287
+
288
+ // ---- Status update ----
289
+ const VALID_TRANSITIONS = {
290
+ 'planned': ['ready', 'implementing'],
291
+ 'ready': ['implementing'],
292
+ 'implementing': ['completed', 'failed'],
293
+ 'failed': ['implementing'],
294
+ 'completed': [],
295
+ };
296
+
297
+ router.put('/:id/status', async (req, res) => {
298
+ try {
299
+ const cs = ceremonyService?.getStatus();
300
+ if (cs?.status === 'running' && cs?.runningType === 'sprint-planning') {
301
+ return res.status(423).json({ error: 'Work items are read-only while sprint planning is active' });
302
+ }
303
+
304
+ const { items } = dataStore.getHierarchy();
305
+ const item = items.get(req.params.id);
306
+ if (!item) return res.status(404).json({ error: 'Work item not found' });
307
+
308
+ const { status } = req.body;
309
+ if (!status || typeof status !== 'string') {
310
+ return res.status(400).json({ error: 'status is required' });
311
+ }
312
+
313
+ const currentStatus = item.status || 'planned';
314
+ const allowed = VALID_TRANSITIONS[currentStatus] || [];
315
+ if (!allowed.includes(status)) {
316
+ return res.status(422).json({
317
+ error: `Invalid status transition: ${currentStatus} → ${status}`,
318
+ allowed,
319
+ });
320
+ }
321
+
322
+ // Write to work.json
323
+ const workJsonPath = path.join(item._dirPath, 'work.json');
324
+ if (fsSync.existsSync(workJsonPath)) {
325
+ const workJson = JSON.parse(fsSync.readFileSync(workJsonPath, 'utf8'));
326
+ workJson.status = status;
327
+ workJson.updated = new Date().toISOString();
328
+ fsSync.writeFileSync(workJsonPath, JSON.stringify(workJson, null, 2), 'utf8');
329
+ }
330
+
331
+ // Reload hierarchy so in-memory state reflects the change
332
+ await dataStore.reload();
333
+
334
+ // When an item is completed, cascade: promote dependents to 'ready' if all their deps are now met
335
+ if (status === 'completed') {
336
+ try {
337
+ const { checkDependenciesReady } = await import('../../../cli/dependency-checker.js');
338
+ const { items: freshItems } = dataStore.getHierarchy();
339
+
340
+ // Build lookup from fresh hierarchy
341
+ const lookup = {};
342
+ for (const [id, wi] of freshItems) {
343
+ lookup[id] = {
344
+ id: wi.id, name: wi.name, type: wi._type || wi.type,
345
+ status: wi.status || 'planned', dependencies: wi.dependencies || [],
346
+ };
347
+ }
348
+
349
+ // Find items that depend on the completed item and are still 'planned'
350
+ let promoted = 0;
351
+ for (const [id, wi] of freshItems) {
352
+ if (wi.status !== 'planned') continue;
353
+ const deps = wi.dependencies || [];
354
+ if (!deps.includes(req.params.id)) continue;
355
+
356
+ // Check if ALL dependencies are now met
357
+ const result = checkDependenciesReady(id, lookup);
358
+ if (result.ready) {
359
+ const depWorkJsonPath = path.join(wi._dirPath, 'work.json');
360
+ if (fsSync.existsSync(depWorkJsonPath)) {
361
+ const wj = JSON.parse(fsSync.readFileSync(depWorkJsonPath, 'utf8'));
362
+ wj.status = 'ready';
363
+ wj.updated = new Date().toISOString();
364
+ fsSync.writeFileSync(depWorkJsonPath, JSON.stringify(wj, null, 2), 'utf8');
365
+ promoted++;
366
+ }
367
+ }
368
+ }
369
+
370
+ if (promoted > 0) {
371
+ await dataStore.reload();
372
+ console.log(`[status-cascade] ${promoted} item(s) promoted to 'ready' after ${req.params.id} completed`);
373
+ }
374
+ } catch (cascadeErr) {
375
+ console.error('Status cascade error (non-critical):', cascadeErr.message);
376
+ }
377
+ }
378
+
379
+ res.json({ status: 'ok', item: cleanWorkItem(item) });
380
+ } catch (error) {
381
+ console.error(`Error updating status for ${req.params.id}:`, error);
382
+ res.status(500).json({ error: 'Failed to update status' });
383
+ }
384
+ });
385
+
386
+ // ---- Dependency status check ----
387
+ router.get('/:id/dependency-status', (req, res) => {
388
+ try {
389
+ const { items } = dataStore.getHierarchy();
390
+ const item = items.get(req.params.id);
391
+ if (!item) return res.status(404).json({ error: 'Work item not found' });
392
+
393
+ // Build a plain lookup object from the items Map
394
+ const lookup = {};
395
+ for (const [id, wi] of items) {
396
+ lookup[id] = {
397
+ id: wi.id,
398
+ name: wi.name,
399
+ type: wi._type || wi.type,
400
+ status: wi.status || 'planned',
401
+ dependencies: wi.dependencies || [],
402
+ };
403
+ }
404
+
405
+ // Dynamic import to keep the route file sync-compatible
406
+ import('../../../cli/dependency-checker.js').then(({ checkDependenciesReady }) => {
407
+ const result = checkDependenciesReady(req.params.id, lookup);
408
+ res.json(result);
409
+ }).catch(err => {
410
+ console.error(`Error checking dependencies for ${req.params.id}:`, err);
411
+ res.status(500).json({ error: 'Failed to check dependencies' });
412
+ });
413
+ } catch (error) {
414
+ console.error(`Error checking dependencies for ${req.params.id}:`, error);
415
+ res.status(500).json({ error: 'Failed to check dependencies' });
416
+ }
417
+ });
418
+
419
+ return router;
420
+ }
421
+
422
+ /**
423
+ * Clean work item for JSON serialization
424
+ * Removes circular references and internal fields
425
+ * @param {object} item - Work item
426
+ * @param {boolean} includeFullDetails - Include documentation and context
427
+ * @returns {object} Cleaned work item
428
+ */
429
+ function cleanWorkItem(item, includeFullDetails = false) {
430
+ const cleaned = {
431
+ id: item.id,
432
+ name: item.name,
433
+ type: item._type,
434
+ status: item.status,
435
+ description: item.description,
436
+ dependencies: item.dependencies || [],
437
+ metadata: item.metadata,
438
+ created: item.created,
439
+ updated: item.updated,
440
+ // Type-specific fields used for diff display in RefineWorkItemPopup
441
+ features: item.features, // epics
442
+ acceptance: item.acceptance, // stories
443
+ userType: item.userType, // stories
444
+ domain: item.domain, // epics
445
+ functions: item.functions, // code traceability registry
446
+ };
447
+
448
+ // Add parent reference (ID only, not full object)
449
+ if (item._parentId) {
450
+ cleaned.parentId = item._parentId;
451
+ cleaned.parentName = item._parent?.name;
452
+ }
453
+
454
+ // Add children references (IDs only)
455
+ if (item._children && item._children.length > 0) {
456
+ cleaned.children = item._children.map((child) => ({
457
+ id: child.id,
458
+ name: child.name,
459
+ type: child._type,
460
+ status: child.status,
461
+ }));
462
+ }
463
+
464
+ // Add epic reference for nested items
465
+ if (item._type !== 'epic' && item._parent) {
466
+ let epic = item._parent;
467
+ while (epic._parent) {
468
+ epic = epic._parent;
469
+ }
470
+ if (epic._type === 'epic') {
471
+ cleaned.epicId = epic.id;
472
+ cleaned.epicName = epic.name;
473
+ }
474
+ }
475
+
476
+ // Include full details if requested
477
+ if (includeFullDetails) {
478
+ if (item.documentation) {
479
+ cleaned.documentation = item.documentation;
480
+ }
481
+ if (item.context) {
482
+ cleaned.context = item.context;
483
+ }
484
+ }
485
+
486
+ return cleaned;
487
+ }