@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
@@ -11,15 +11,27 @@
11
11
  import fs from 'fs';
12
12
  import path from 'path';
13
13
 
14
+ /** Format a Date as a local ISO-8601 string (with timezone offset, e.g. 2026-03-04T18:05:16.554+01:00) */
15
+ function localISO(date = new Date()) {
16
+ const p = n => String(n).padStart(2, '0');
17
+ const ms = String(date.getMilliseconds()).padStart(3, '0');
18
+ const tz = -date.getTimezoneOffset();
19
+ const sign = tz >= 0 ? '+' : '-';
20
+ const tzH = p(Math.floor(Math.abs(tz) / 60));
21
+ const tzM = p(Math.abs(tz) % 60);
22
+ return `${date.getFullYear()}-${p(date.getMonth()+1)}-${p(date.getDate())}T${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}.${ms}${sign}${tzH}:${tzM}`;
23
+ }
24
+
14
25
  class CommandLogger {
15
- constructor(commandName, projectRoot = process.cwd()) {
26
+ constructor(commandName, projectRoot = process.cwd(), inkMode = false) {
16
27
  this.commandName = commandName;
17
28
  this.projectRoot = projectRoot;
18
29
  this.logsDir = path.join(projectRoot, '.avc', 'logs');
30
+ this.inkMode = inkMode; // Don't forward to console if in Ink mode
19
31
 
20
32
  // Create timestamp for this command execution
21
33
  const now = new Date();
22
- const timestamp = now.toISOString()
34
+ const timestamp = localISO(now)
23
35
  .replace(/T/, '-')
24
36
  .replace(/:/g, '-')
25
37
  .replace(/\..+/, '');
@@ -54,7 +66,7 @@ class CommandLogger {
54
66
  const header = [
55
67
  '='.repeat(80),
56
68
  `AVC Command Log: ${this.commandName}`,
57
- `Timestamp: ${new Date().toISOString()}`,
69
+ `Timestamp: ${localISO()}`,
58
70
  `Project: ${this.projectRoot}`,
59
71
  `Log File: ${this.logFileName}`,
60
72
  '='.repeat(80),
@@ -75,19 +87,32 @@ class CommandLogger {
75
87
  if (!this.logFilePath) return;
76
88
 
77
89
  try {
78
- const timestamp = new Date().toISOString();
90
+ const timestamp = localISO();
79
91
  const message = args.map(arg => {
92
+ if (arg === undefined) return 'undefined';
93
+ if (arg === null) return 'null';
80
94
  if (typeof arg === 'object') {
81
- return JSON.stringify(arg, null, 2);
95
+ try {
96
+ // Handle circular references and non-serializable objects
97
+ return JSON.stringify(arg, (key, value) => {
98
+ if (typeof value === 'function') return '[Function]';
99
+ if (typeof value === 'symbol') return value.toString();
100
+ return value;
101
+ }, 2);
102
+ } catch (jsonError) {
103
+ return `[Object: ${arg.constructor?.name || 'Unknown'}]`;
104
+ }
82
105
  }
83
106
  return String(arg);
84
107
  }).join(' ');
85
108
 
86
- const logEntry = `[${timestamp}] [${level}] ${message}\n`;
109
+ // Preserve formatting but ensure single newline at end
110
+ const logEntry = `[${timestamp}] [${level}] ${message}${message.endsWith('\n') ? '' : '\n'}`;
87
111
 
88
112
  fs.appendFileSync(this.logFilePath, logEntry, 'utf8');
89
113
  } catch (error) {
90
- // Silently fail if we can't write to log
114
+ // Write error to stderr so it doesn't get captured in loop
115
+ process.stderr.write(`[CommandLogger] Failed to write log: ${error.message}\n`);
91
116
  }
92
117
  }
93
118
 
@@ -98,25 +123,37 @@ class CommandLogger {
98
123
  // Intercept console.log
99
124
  console.log = (...args) => {
100
125
  this.writeLog('INFO', ...args);
101
- this.originalLog(...args); // Still output to console
126
+ // Only forward to console if NOT in Ink mode
127
+ if (!this.inkMode) {
128
+ this.originalLog(...args);
129
+ }
102
130
  };
103
131
 
104
132
  // Intercept console.error
105
133
  console.error = (...args) => {
106
134
  this.writeLog('ERROR', ...args);
107
- this.originalError(...args);
135
+ // Only forward to console if NOT in Ink mode
136
+ if (!this.inkMode) {
137
+ this.originalError(...args);
138
+ }
108
139
  };
109
140
 
110
141
  // Intercept console.warn
111
142
  console.warn = (...args) => {
112
143
  this.writeLog('WARN', ...args);
113
- this.originalWarn(...args);
144
+ // Only forward to console if NOT in Ink mode
145
+ if (!this.inkMode) {
146
+ this.originalWarn(...args);
147
+ }
114
148
  };
115
149
 
116
150
  // Intercept console.info
117
151
  console.info = (...args) => {
118
152
  this.writeLog('INFO', ...args);
119
- this.originalInfo(...args);
153
+ // Only forward to console if NOT in Ink mode
154
+ if (!this.inkMode) {
155
+ this.originalInfo(...args);
156
+ }
120
157
  };
121
158
  }
122
159
 
@@ -136,7 +173,7 @@ class CommandLogger {
136
173
  const footer = [
137
174
  '',
138
175
  '='.repeat(80),
139
- `Command completed: ${new Date().toISOString()}`,
176
+ `Command completed: ${localISO()}`,
140
177
  `Log saved: ${this.logFilePath}`,
141
178
  '='.repeat(80)
142
179
  ].join('\n');
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { Static, Text, Box } from 'ink';
3
+
4
+ /**
5
+ * Badge config for message types (inspired by jest example's backgroundColor badges)
6
+ */
7
+ const BADGE = {
8
+ ERROR: { bg: 'red', fg: 'white', label: ' ERR ' },
9
+ WARNING: { bg: 'yellow', fg: 'black', label: ' WRN ' },
10
+ SUCCESS: { bg: 'green', fg: 'black', label: ' OK ' },
11
+ INFO: { bg: 'blue', fg: 'white', label: ' INF ' },
12
+ };
13
+
14
+ /**
15
+ * StaticOutput - Renders output items using Ink's built-in Static component.
16
+ *
17
+ * Ink's <Static> commits each item to the terminal once and never touches
18
+ * it again. Items are NOT part of Ink's height-tracking calculation, so
19
+ * they never cause ghost renders when the interactive section changes height.
20
+ *
21
+ * Items with a `type` field get a colored background badge prefix
22
+ * (ERR/WRN/OK/INF) inspired by the jest example's test status chips.
23
+ *
24
+ * @param {Object} props
25
+ * @param {{id: number, content: string, type?: string}[]} props.items
26
+ * @returns {React.Element|null}
27
+ */
28
+ export const StaticOutput = ({ items }) => {
29
+ if (!items || items.length === 0) return null;
30
+
31
+ return React.createElement(Static, { items },
32
+ (item) => {
33
+ const badge = item.type ? BADGE[item.type] : null;
34
+
35
+ if (badge) {
36
+ // Strip the "TYPE: " prefix from content — badge already shows the type visually
37
+ const displayContent = item.content.replace(/^(ERROR|WARNING|SUCCESS|INFO): /, '');
38
+ const lines = displayContent.split('\n');
39
+
40
+ if (lines.length === 1) {
41
+ // Single-line: badge + text side by side
42
+ return React.createElement(Box, { key: item.id, flexDirection: 'row', gap: 1 },
43
+ React.createElement(Text, { backgroundColor: badge.bg, color: badge.fg }, badge.label),
44
+ React.createElement(Text, null, displayContent)
45
+ );
46
+ }
47
+
48
+ // Multi-line: badge on first line only, subsequent lines fully left-aligned
49
+ return React.createElement(Box, { key: item.id, flexDirection: 'column' },
50
+ React.createElement(Box, { flexDirection: 'row', gap: 1 },
51
+ React.createElement(Text, { backgroundColor: badge.bg, color: badge.fg }, badge.label),
52
+ React.createElement(Text, null, lines[0])
53
+ ),
54
+ ...lines.slice(1).map((line, i) =>
55
+ React.createElement(Text, { key: i }, line)
56
+ )
57
+ );
58
+ }
59
+
60
+ return React.createElement(Text, { key: item.id }, item.content);
61
+ }
62
+ );
63
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ConsoleOutputManager - Unified console output handling for Ink REPL
3
+ *
4
+ * Provides consistent console interception across all commands:
5
+ * - Filters [DEBUG] messages (file only, not displayed in UI)
6
+ * - Streams output to OutputBuffer for Static component rendering
7
+ * - Works with CommandLogger for file logging (without console duplication)
8
+ *
9
+ * Usage:
10
+ * const manager = new ConsoleOutputManager();
11
+ * manager.start();
12
+ * try {
13
+ * await someOperation(); // console.log calls will be captured
14
+ * } finally {
15
+ * manager.stop();
16
+ * }
17
+ */
18
+
19
+ import { outputBuffer } from './output-buffer.js';
20
+
21
+ class ConsoleOutputManager {
22
+ constructor() {
23
+ this.originalLog = console.log;
24
+ this.originalError = console.error;
25
+ this.originalWarn = console.warn;
26
+ this.isActive = false;
27
+ }
28
+
29
+ /**
30
+ * Start intercepting console output
31
+ */
32
+ start() {
33
+ if (this.isActive) return;
34
+ this.isActive = true;
35
+
36
+ // Intercept console.log
37
+ console.log = (...args) => {
38
+ const message = args.join(' ');
39
+
40
+ // Filter [DEBUG] messages - they go to file only via CommandLogger
41
+ // Don't display them in the UI
42
+ if (message.includes('[DEBUG]')) {
43
+ // Forward to CommandLogger for file logging
44
+ // CommandLogger is already intercepting, so just call its version
45
+ this.originalLog(...args);
46
+ return;
47
+ }
48
+
49
+ // User-facing messages: append directly to buffer
50
+ outputBuffer.append(message + '\n');
51
+
52
+ // Also forward to CommandLogger for file logging
53
+ // CommandLogger is in inkMode, so it won't forward to real console
54
+ this.originalLog(...args);
55
+ };
56
+
57
+ // Intercept console.error
58
+ console.error = (...args) => {
59
+ const message = args.join(' ');
60
+
61
+ // Display errors in UI with error prefix
62
+ outputBuffer.append(`ERROR: ${message}\n`);
63
+
64
+ // Also forward to CommandLogger for file logging
65
+ this.originalError(...args);
66
+ };
67
+
68
+ // Intercept console.warn
69
+ console.warn = (...args) => {
70
+ const message = args.join(' ');
71
+
72
+ // Display warnings in UI with warning prefix
73
+ outputBuffer.append(`WARNING: ${message}\n`);
74
+
75
+ // Also forward to CommandLogger for file logging
76
+ this.originalWarn(...args);
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Stop intercepting and restore original console
82
+ */
83
+ stop() {
84
+ if (!this.isActive) return;
85
+ this.isActive = false;
86
+
87
+ // Restore original console methods
88
+ console.log = this.originalLog;
89
+ console.error = this.originalError;
90
+ console.warn = this.originalWarn;
91
+ }
92
+ }
93
+
94
+ export default ConsoleOutputManager;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * dependency-checker.js — Pure-function module for checking work item dependency readiness.
3
+ *
4
+ * No side effects, no file I/O. All data passed in as arguments.
5
+ */
6
+
7
+ /**
8
+ * Extract the parent ID from a work item ID by stripping the last segment.
9
+ * context-0001-0001-0001 → context-0001-0001
10
+ * context-0001-0001 → context-0001
11
+ * context-0001 → null
12
+ * @param {string} id
13
+ * @returns {string|null}
14
+ */
15
+ export function getParentId(id) {
16
+ const match = id.match(/^(context-\d{4}(?:-\d{4})*)-\d{4}$/);
17
+ return match ? match[1] : null;
18
+ }
19
+
20
+ /**
21
+ * Check if all dependencies of a work item are completed.
22
+ * Walks up the parent chain: task checks parent story deps,
23
+ * story checks parent epic deps, etc.
24
+ *
25
+ * @param {string} itemId - The work item to check
26
+ * @param {Map<string,object>|object} items - Map of id → work item, or plain object
27
+ * @returns {{ ready: boolean, blockers: Array<{id: string, name: string, type: string, status: string}> }}
28
+ */
29
+ export function checkDependenciesReady(itemId, items) {
30
+ const get = (id) => items instanceof Map ? items.get(id) : items[id];
31
+ const blockers = [];
32
+ const seen = new Set();
33
+
34
+ function collectBlockers(id) {
35
+ if (!id || seen.has(id)) return;
36
+ seen.add(id);
37
+
38
+ const item = get(id);
39
+ if (!item) return;
40
+
41
+ // Check direct dependencies
42
+ for (const depId of item.dependencies || []) {
43
+ if (seen.has(depId)) continue;
44
+ const dep = get(depId);
45
+ if (!dep) continue;
46
+ if (dep.status !== 'completed') {
47
+ blockers.push({
48
+ id: depId,
49
+ name: dep.name || depId,
50
+ type: dep.type || 'unknown',
51
+ status: dep.status || 'planned',
52
+ });
53
+ }
54
+ }
55
+
56
+ // Walk up to parent and check its dependencies too
57
+ const parentId = getParentId(id);
58
+ if (parentId) {
59
+ collectBlockers(parentId);
60
+ }
61
+ }
62
+
63
+ collectBlockers(itemId);
64
+
65
+ // Deduplicate blockers by id
66
+ const unique = [...new Map(blockers.map(b => [b.id, b])).values()];
67
+
68
+ return {
69
+ ready: unique.length === 0,
70
+ blockers: unique,
71
+ };
72
+ }
@@ -0,0 +1,306 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chokidar from 'chokidar';
4
+
5
+ const WORK_ITEMS_START = '// @@AVC-WORK-ITEMS-START@@';
6
+ const WORK_ITEMS_END = '// @@AVC-WORK-ITEMS-END@@';
7
+
8
+ /**
9
+ * Syncs .avc/project/ work-item hierarchy (epics + stories + doc.md files)
10
+ * into the VitePress documentation site at .avc/documentation/.
11
+ *
12
+ * Responsibilities:
13
+ * 1. Read .avc/project/ hierarchy via work.json files
14
+ * 2. Copy changed doc.md files into documentation/project/{epic}/{story}/index.md
15
+ * 3. Regenerate the VitePress sidebar between marker comments in config.mts
16
+ */
17
+ export class DocsSyncProcessor {
18
+ constructor(projectRoot = process.cwd()) {
19
+ this.projectRoot = projectRoot;
20
+ this.avcDir = path.join(projectRoot, '.avc');
21
+ this.projectPath = path.join(this.avcDir, 'project');
22
+ this.docsDir = path.join(this.avcDir, 'documentation');
23
+ this.projectDocsDir = path.join(this.docsDir, 'project');
24
+ this.configPath = path.join(this.docsDir, '.vitepress', 'config.mts');
25
+ this.avcConfigPath = path.join(this.avcDir, 'avc.json');
26
+ }
27
+
28
+ /**
29
+ * Read the epic/story hierarchy from .avc/project/ work.json files.
30
+ * Returns array sorted by ID, each entry: { id, name, docPath, stories: [{ id, name, docPath }] }
31
+ */
32
+ readHierarchy() {
33
+ if (!fs.existsSync(this.projectPath)) {
34
+ return [];
35
+ }
36
+
37
+ const entries = fs.readdirSync(this.projectPath, { withFileTypes: true });
38
+ const epics = [];
39
+
40
+ for (const entry of entries) {
41
+ if (!entry.isDirectory()) continue;
42
+
43
+ const epicId = entry.name;
44
+ const workJsonPath = path.join(this.projectPath, epicId, 'work.json');
45
+
46
+ if (!fs.existsSync(workJsonPath)) continue;
47
+
48
+ let workJson;
49
+ try {
50
+ workJson = JSON.parse(fs.readFileSync(workJsonPath, 'utf8'));
51
+ } catch {
52
+ continue;
53
+ }
54
+
55
+ if (workJson.type !== 'epic') continue;
56
+
57
+ const epicDocPath = path.join(this.projectPath, epicId, 'doc.md');
58
+ const stories = [];
59
+
60
+ // Scan subdirectories for stories
61
+ const epicDir = path.join(this.projectPath, epicId);
62
+ const storyEntries = fs.readdirSync(epicDir, { withFileTypes: true });
63
+
64
+ for (const storyEntry of storyEntries) {
65
+ if (!storyEntry.isDirectory()) continue;
66
+
67
+ const storyId = storyEntry.name;
68
+ const storyWorkJsonPath = path.join(epicDir, storyId, 'work.json');
69
+
70
+ if (!fs.existsSync(storyWorkJsonPath)) continue;
71
+
72
+ let storyWorkJson;
73
+ try {
74
+ storyWorkJson = JSON.parse(fs.readFileSync(storyWorkJsonPath, 'utf8'));
75
+ } catch {
76
+ continue;
77
+ }
78
+
79
+ if (storyWorkJson.type !== 'story') continue;
80
+
81
+ const storyDocPath = path.join(epicDir, storyId, 'doc.md');
82
+ stories.push({
83
+ id: storyId,
84
+ name: storyWorkJson.name || storyId,
85
+ docPath: storyDocPath,
86
+ });
87
+ }
88
+
89
+ stories.sort((a, b) => a.id.localeCompare(b.id));
90
+
91
+ epics.push({
92
+ id: epicId,
93
+ name: workJson.name || epicId,
94
+ docPath: epicDocPath,
95
+ stories,
96
+ });
97
+ }
98
+
99
+ epics.sort((a, b) => a.id.localeCompare(b.id));
100
+ return epics;
101
+ }
102
+
103
+ /**
104
+ * Copy src to dest if source is newer than dest (mtime-based incremental skip).
105
+ * Returns 'copied' or 'skipped'.
106
+ */
107
+ syncFile(src, dest) {
108
+ if (!fs.existsSync(src)) {
109
+ return 'skipped';
110
+ }
111
+
112
+ if (fs.existsSync(dest)) {
113
+ const srcStat = fs.statSync(src);
114
+ const destStat = fs.statSync(dest);
115
+ if (destStat.mtimeMs >= srcStat.mtimeMs) {
116
+ return 'skipped';
117
+ }
118
+ }
119
+
120
+ fs.copyFileSync(src, dest);
121
+ return 'copied';
122
+ }
123
+
124
+ /**
125
+ * Write a stub index.md for an epic/story that has no doc.md yet.
126
+ */
127
+ _writeStub(destPath, name) {
128
+ const content = `# ${name}\n\n_Documentation pending._\n`;
129
+ fs.writeFileSync(destPath, content, 'utf8');
130
+ }
131
+
132
+ /**
133
+ * Main sync orchestration. Reads hierarchy, copies files, regenerates sidebar config.
134
+ * @param {Function} [progressCallback] - optional (message) => void
135
+ * @returns {{ epics: number, stories: number, copied: number, skipped: number }}
136
+ */
137
+ async sync(progressCallback = null) {
138
+ if (!fs.existsSync(this.docsDir)) {
139
+ throw new Error('Documentation directory not found. Run /init first to create documentation structure.');
140
+ }
141
+
142
+ const report = (msg) => { if (progressCallback) progressCallback(msg); };
143
+ const hierarchy = this.readHierarchy();
144
+
145
+ let epicCount = 0;
146
+ let storyCount = 0;
147
+ let copiedCount = 0;
148
+ let skippedCount = 0;
149
+
150
+ // Sync project root doc.md → documentation/index.md
151
+ const projectDocSrc = path.join(this.projectPath, 'doc.md');
152
+ const projectDocDest = path.join(this.docsDir, 'index.md');
153
+ if (fs.existsSync(projectDocSrc)) {
154
+ const result = this.syncFile(projectDocSrc, projectDocDest);
155
+ if (result === 'copied') copiedCount++;
156
+ else skippedCount++;
157
+ report(`Project brief: ${result}`);
158
+ }
159
+
160
+ // Ensure project/ subdir exists if we have epics
161
+ if (hierarchy.length > 0) {
162
+ fs.mkdirSync(this.projectDocsDir, { recursive: true });
163
+ }
164
+
165
+ for (const epic of hierarchy) {
166
+ epicCount++;
167
+ const epicDestDir = path.join(this.projectDocsDir, epic.id);
168
+ fs.mkdirSync(epicDestDir, { recursive: true });
169
+
170
+ const epicDestPath = path.join(epicDestDir, 'index.md');
171
+ if (fs.existsSync(epic.docPath)) {
172
+ const result = this.syncFile(epic.docPath, epicDestPath);
173
+ if (result === 'copied') copiedCount++;
174
+ else skippedCount++;
175
+ report(`Epic ${epic.id}: ${result}`);
176
+ } else {
177
+ this._writeStub(epicDestPath, epic.name);
178
+ copiedCount++;
179
+ report(`Epic ${epic.id}: stub written`);
180
+ }
181
+
182
+ for (const story of epic.stories) {
183
+ storyCount++;
184
+ const storyDestDir = path.join(epicDestDir, story.id);
185
+ fs.mkdirSync(storyDestDir, { recursive: true });
186
+
187
+ const storyDestPath = path.join(storyDestDir, 'index.md');
188
+ if (fs.existsSync(story.docPath)) {
189
+ const result = this.syncFile(story.docPath, storyDestPath);
190
+ if (result === 'copied') copiedCount++;
191
+ else skippedCount++;
192
+ report(`Story ${story.id}: ${result}`);
193
+ } else {
194
+ this._writeStub(storyDestPath, story.name);
195
+ copiedCount++;
196
+ report(`Story ${story.id}: stub written`);
197
+ }
198
+ }
199
+ }
200
+
201
+ // Regenerate VitePress sidebar config
202
+ this.generateVitePressConfig(hierarchy);
203
+
204
+ return { epics: epicCount, stories: storyCount, copied: copiedCount, skipped: skippedCount };
205
+ }
206
+
207
+ /**
208
+ * Regenerate the @@AVC-WORK-ITEMS-START@@ … @@AVC-WORK-ITEMS-END@@ block in config.mts.
209
+ * If markers are absent (existing project), appends as second sidebar array element and inserts markers.
210
+ */
211
+ generateVitePressConfig(hierarchy) {
212
+ if (!fs.existsSync(this.configPath)) return;
213
+
214
+ const content = fs.readFileSync(this.configPath, 'utf8');
215
+ const workItemsBlock = this._buildWorkItemsBlock(hierarchy);
216
+
217
+ if (content.includes(WORK_ITEMS_START)) {
218
+ // Replace content between markers (inclusive)
219
+ const startIdx = content.indexOf(WORK_ITEMS_START);
220
+ const endIdx = content.indexOf(WORK_ITEMS_END);
221
+ if (endIdx === -1) return; // malformed markers, skip
222
+
223
+ const before = content.slice(0, startIdx);
224
+ const after = content.slice(endIdx + WORK_ITEMS_END.length);
225
+ const updated = before + workItemsBlock + after;
226
+ fs.writeFileSync(this.configPath, updated, 'utf8');
227
+ } else {
228
+ // Backwards compat: insert markers + work items block before the closing of the sidebar array
229
+ // Find the closing `]` of the sidebar array
230
+ const sidebarMatch = content.match(/sidebar:\s*\[/);
231
+ if (!sidebarMatch) return;
232
+
233
+ // Find the end of the first sidebar item block to insert after it
234
+ const insertionPattern = /sidebar:\s*\[\s*\{[\s\S]*?\}\s*(?=\])/;
235
+ const match = insertionPattern.exec(content);
236
+ if (!match) return;
237
+
238
+ const insertAt = match.index + match[0].length;
239
+ const updated = content.slice(0, insertAt) + '\n ' + workItemsBlock + '\n ' + content.slice(insertAt);
240
+ fs.writeFileSync(this.configPath, updated, 'utf8');
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Build the sidebar work-items block string (with markers).
246
+ */
247
+ _buildWorkItemsBlock(hierarchy) {
248
+ if (hierarchy.length === 0) {
249
+ return `${WORK_ITEMS_START}\n ${WORK_ITEMS_END}`;
250
+ }
251
+
252
+ const epicItems = hierarchy.map(epic => {
253
+ const storyItems = epic.stories.map(story => {
254
+ const link = path.posix.join('/project', epic.id, story.id) + '/';
255
+ return ` { text: ${JSON.stringify(story.name)}, link: ${JSON.stringify(link)} }`;
256
+ });
257
+
258
+ const epicLink = path.posix.join('/project', epic.id) + '/';
259
+ if (storyItems.length > 0) {
260
+ return ` { text: ${JSON.stringify(epic.name)}, link: ${JSON.stringify(epicLink)}, collapsed: false, items: [\n${storyItems.join(',\n')}\n ] }`;
261
+ } else {
262
+ return ` { text: ${JSON.stringify(epic.name)}, link: ${JSON.stringify(epicLink)} }`;
263
+ }
264
+ });
265
+
266
+ const block = `,{
267
+ text: 'Work Items',
268
+ items: [
269
+ ${epicItems.join(',\n')}
270
+ ]
271
+ }`;
272
+
273
+ return `${WORK_ITEMS_START}\n ${block}\n ${WORK_ITEMS_END}`;
274
+ }
275
+
276
+ /**
277
+ * Watch .avc/project/ for md changes and trigger sync with debounce.
278
+ * Returns a chokidar watcher instance.
279
+ */
280
+ watch(onChange = null) {
281
+ let debounceTimer = null;
282
+
283
+ const watcher = chokidar.watch(path.join(this.projectPath, '**', '*.md'), {
284
+ ignoreInitial: true,
285
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
286
+ });
287
+
288
+ const trigger = () => {
289
+ if (debounceTimer) clearTimeout(debounceTimer);
290
+ debounceTimer = setTimeout(async () => {
291
+ try {
292
+ const stats = await this.sync();
293
+ if (onChange) onChange(stats);
294
+ } catch {
295
+ // Ignore errors in watch mode
296
+ }
297
+ }, 500);
298
+ };
299
+
300
+ watcher.on('add', trigger);
301
+ watcher.on('change', trigger);
302
+ watcher.on('unlink', trigger);
303
+
304
+ return watcher;
305
+ }
306
+ }