@agile-vibe-coding/avc 0.1.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +129 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/epic-story-decomposer.md +280 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +79 -0
- package/cli/agents/mission-scope-validator.md +112 -0
- package/cli/agents/project-context-extractor.md +107 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/solver-epic-api.json +15 -0
- package/cli/agents/solver-epic-api.md +39 -0
- package/cli/agents/solver-epic-backend.json +15 -0
- package/cli/agents/solver-epic-backend.md +39 -0
- package/cli/agents/solver-epic-cloud.json +15 -0
- package/cli/agents/solver-epic-cloud.md +39 -0
- package/cli/agents/solver-epic-data.json +15 -0
- package/cli/agents/solver-epic-data.md +39 -0
- package/cli/agents/solver-epic-database.json +15 -0
- package/cli/agents/solver-epic-database.md +39 -0
- package/cli/agents/solver-epic-developer.json +15 -0
- package/cli/agents/solver-epic-developer.md +39 -0
- package/cli/agents/solver-epic-devops.json +15 -0
- package/cli/agents/solver-epic-devops.md +39 -0
- package/cli/agents/solver-epic-frontend.json +15 -0
- package/cli/agents/solver-epic-frontend.md +39 -0
- package/cli/agents/solver-epic-mobile.json +15 -0
- package/cli/agents/solver-epic-mobile.md +39 -0
- package/cli/agents/solver-epic-qa.json +15 -0
- package/cli/agents/solver-epic-qa.md +39 -0
- package/cli/agents/solver-epic-security.json +15 -0
- package/cli/agents/solver-epic-security.md +39 -0
- package/cli/agents/solver-epic-solution-architect.json +15 -0
- package/cli/agents/solver-epic-solution-architect.md +39 -0
- package/cli/agents/solver-epic-test-architect.json +15 -0
- package/cli/agents/solver-epic-test-architect.md +39 -0
- package/cli/agents/solver-epic-ui.json +15 -0
- package/cli/agents/solver-epic-ui.md +39 -0
- package/cli/agents/solver-epic-ux.json +15 -0
- package/cli/agents/solver-epic-ux.md +39 -0
- package/cli/agents/solver-story-api.json +15 -0
- package/cli/agents/solver-story-api.md +39 -0
- package/cli/agents/solver-story-backend.json +15 -0
- package/cli/agents/solver-story-backend.md +39 -0
- package/cli/agents/solver-story-cloud.json +15 -0
- package/cli/agents/solver-story-cloud.md +39 -0
- package/cli/agents/solver-story-data.json +15 -0
- package/cli/agents/solver-story-data.md +39 -0
- package/cli/agents/solver-story-database.json +15 -0
- package/cli/agents/solver-story-database.md +39 -0
- package/cli/agents/solver-story-developer.json +15 -0
- package/cli/agents/solver-story-developer.md +39 -0
- package/cli/agents/solver-story-devops.json +15 -0
- package/cli/agents/solver-story-devops.md +39 -0
- package/cli/agents/solver-story-frontend.json +15 -0
- package/cli/agents/solver-story-frontend.md +39 -0
- package/cli/agents/solver-story-mobile.json +15 -0
- package/cli/agents/solver-story-mobile.md +39 -0
- package/cli/agents/solver-story-qa.json +15 -0
- package/cli/agents/solver-story-qa.md +39 -0
- package/cli/agents/solver-story-security.json +15 -0
- package/cli/agents/solver-story-security.md +39 -0
- package/cli/agents/solver-story-solution-architect.json +15 -0
- package/cli/agents/solver-story-solution-architect.md +39 -0
- package/cli/agents/solver-story-test-architect.json +15 -0
- package/cli/agents/solver-story-test-architect.md +39 -0
- package/cli/agents/solver-story-ui.json +15 -0
- package/cli/agents/solver-story-ui.md +39 -0
- package/cli/agents/solver-story-ux.json +15 -0
- package/cli/agents/solver-story-ux.md +39 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +152 -0
- package/cli/agents/validator-documentation.md +453 -0
- package/cli/agents/validator-epic-api.json +93 -0
- package/cli/agents/validator-epic-api.md +137 -0
- package/cli/agents/validator-epic-backend.json +93 -0
- package/cli/agents/validator-epic-backend.md +130 -0
- package/cli/agents/validator-epic-cloud.json +93 -0
- package/cli/agents/validator-epic-cloud.md +137 -0
- package/cli/agents/validator-epic-data.json +93 -0
- package/cli/agents/validator-epic-data.md +130 -0
- package/cli/agents/validator-epic-database.json +93 -0
- package/cli/agents/validator-epic-database.md +137 -0
- package/cli/agents/validator-epic-developer.json +74 -0
- package/cli/agents/validator-epic-developer.md +153 -0
- package/cli/agents/validator-epic-devops.json +74 -0
- package/cli/agents/validator-epic-devops.md +153 -0
- package/cli/agents/validator-epic-frontend.json +74 -0
- package/cli/agents/validator-epic-frontend.md +153 -0
- package/cli/agents/validator-epic-mobile.json +93 -0
- package/cli/agents/validator-epic-mobile.md +130 -0
- package/cli/agents/validator-epic-qa.json +93 -0
- package/cli/agents/validator-epic-qa.md +130 -0
- package/cli/agents/validator-epic-security.json +74 -0
- package/cli/agents/validator-epic-security.md +154 -0
- package/cli/agents/validator-epic-solution-architect.json +74 -0
- package/cli/agents/validator-epic-solution-architect.md +156 -0
- package/cli/agents/validator-epic-test-architect.json +93 -0
- package/cli/agents/validator-epic-test-architect.md +130 -0
- package/cli/agents/validator-epic-ui.json +93 -0
- package/cli/agents/validator-epic-ui.md +130 -0
- package/cli/agents/validator-epic-ux.json +93 -0
- package/cli/agents/validator-epic-ux.md +130 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/agents/validator-story-api.json +104 -0
- package/cli/agents/validator-story-api.md +152 -0
- package/cli/agents/validator-story-backend.json +104 -0
- package/cli/agents/validator-story-backend.md +152 -0
- package/cli/agents/validator-story-cloud.json +104 -0
- package/cli/agents/validator-story-cloud.md +152 -0
- package/cli/agents/validator-story-data.json +104 -0
- package/cli/agents/validator-story-data.md +152 -0
- package/cli/agents/validator-story-database.json +104 -0
- package/cli/agents/validator-story-database.md +152 -0
- package/cli/agents/validator-story-developer.json +104 -0
- package/cli/agents/validator-story-developer.md +152 -0
- package/cli/agents/validator-story-devops.json +104 -0
- package/cli/agents/validator-story-devops.md +152 -0
- package/cli/agents/validator-story-frontend.json +104 -0
- package/cli/agents/validator-story-frontend.md +152 -0
- package/cli/agents/validator-story-mobile.json +104 -0
- package/cli/agents/validator-story-mobile.md +152 -0
- package/cli/agents/validator-story-qa.json +104 -0
- package/cli/agents/validator-story-qa.md +152 -0
- package/cli/agents/validator-story-security.json +104 -0
- package/cli/agents/validator-story-security.md +152 -0
- package/cli/agents/validator-story-solution-architect.json +104 -0
- package/cli/agents/validator-story-solution-architect.md +152 -0
- package/cli/agents/validator-story-test-architect.json +104 -0
- package/cli/agents/validator-story-test-architect.md +152 -0
- package/cli/agents/validator-story-ui.json +104 -0
- package/cli/agents/validator-story-ui.md +152 -0
- package/cli/agents/validator-story-ux.json +104 -0
- package/cli/agents/validator-story-ux.md +152 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/build-docs.js +29 -8
- package/cli/ceremony-history.js +369 -0
- package/cli/command-logger.js +49 -12
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +1174 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/index.js +0 -0
- package/cli/init-model-config.js +697 -0
- package/cli/init.js +1311 -274
- package/cli/kanban-server-manager.js +228 -0
- package/cli/llm-claude.js +83 -1
- package/cli/llm-gemini.js +85 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +233 -0
- package/cli/llm-provider.js +240 -3
- package/cli/llm-token-limits.js +102 -0
- package/cli/llm-verifier.js +454 -0
- package/cli/message-constants.js +58 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +297 -0
- package/cli/model-pricing.js +169 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +269 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +73 -2
- package/cli/repl-ink.js +4988 -1217
- package/cli/repl-old.js +4 -4
- package/cli/seed-processor.js +792 -0
- package/cli/sprint-planning-processor.js +1813 -0
- package/cli/template-processor.js +2102 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- package/cli/token-tracker.js +520 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +605 -0
- package/cli/verification-tracker.js +563 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
- package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +622 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +353 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +118 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +401 -0
- package/kanban/client/src/lib/status-grouping.js +144 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +115 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +516 -0
- package/kanban/server/routes/ceremony.js +305 -0
- package/kanban/server/routes/costs.js +157 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +303 -0
- package/kanban/server/routes/websocket.js +276 -0
- package/kanban/server/routes/work-items.js +347 -0
- package/kanban/server/services/CeremonyService.js +1190 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/sponsor-call-worker.js +84 -0
- package/kanban/server/workers/sprint-planning-worker.js +130 -0
- package/package.json +18 -5
- 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
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { extractDescriptionFromDoc } from '../utils/markdown.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WorkItemReader
|
|
7
|
+
* Parses work.json files and builds hierarchical work item structure
|
|
8
|
+
*/
|
|
9
|
+
export class WorkItemReader {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} projectRoot - Absolute path to project root directory
|
|
12
|
+
*/
|
|
13
|
+
constructor(projectRoot) {
|
|
14
|
+
this.projectRoot = projectRoot;
|
|
15
|
+
this.avcProjectPath = path.join(projectRoot, '.avc', 'project');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read and parse a single work.json file
|
|
20
|
+
* @param {string} filePath - Absolute path to work.json
|
|
21
|
+
* @returns {Promise<object|null>} Parsed work item or null if invalid
|
|
22
|
+
*/
|
|
23
|
+
async readWorkItem(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
26
|
+
const workItem = JSON.parse(content);
|
|
27
|
+
|
|
28
|
+
// Enhance with computed fields
|
|
29
|
+
workItem._filePath = filePath;
|
|
30
|
+
workItem._dirPath = path.dirname(filePath);
|
|
31
|
+
|
|
32
|
+
return workItem;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`Error reading work item from ${filePath}:`, error.message);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read all work items from an array of file paths
|
|
41
|
+
* @param {Array<string>} filePaths - Array of absolute paths to work.json files
|
|
42
|
+
* @returns {Promise<Array<object>>} Array of parsed work items
|
|
43
|
+
*/
|
|
44
|
+
async readAllWorkItems(filePaths) {
|
|
45
|
+
const promises = filePaths.map((filePath) => this.readWorkItem(filePath));
|
|
46
|
+
const results = await Promise.all(promises);
|
|
47
|
+
|
|
48
|
+
// Filter out null results (failed reads)
|
|
49
|
+
return results.filter((item) => item !== null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Determine work item type based on ID depth
|
|
54
|
+
* Example: context-0001 → epic, context-0001-0001 → story, etc.
|
|
55
|
+
* @param {string} id - Work item ID
|
|
56
|
+
* @returns {string} Type: 'epic' | 'story' | 'task' | 'subtask'
|
|
57
|
+
*/
|
|
58
|
+
getWorkItemType(id) {
|
|
59
|
+
const parts = id.split('-');
|
|
60
|
+
const depth = parts.length - 1; // Subtract 1 for 'context' prefix
|
|
61
|
+
|
|
62
|
+
switch (depth) {
|
|
63
|
+
case 1:
|
|
64
|
+
return 'epic';
|
|
65
|
+
case 2:
|
|
66
|
+
return 'story';
|
|
67
|
+
case 3:
|
|
68
|
+
return 'task';
|
|
69
|
+
case 4:
|
|
70
|
+
return 'subtask';
|
|
71
|
+
default:
|
|
72
|
+
return 'unknown';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get parent ID from a work item ID
|
|
78
|
+
* Example: context-0001-0001-0001 → context-0001-0001
|
|
79
|
+
* @param {string} id - Work item ID
|
|
80
|
+
* @returns {string|null} Parent ID or null if root level
|
|
81
|
+
*/
|
|
82
|
+
getParentId(id) {
|
|
83
|
+
const parts = id.split('-');
|
|
84
|
+
|
|
85
|
+
if (parts.length <= 2) {
|
|
86
|
+
// Root level (epic), no parent
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Remove the last segment
|
|
91
|
+
return parts.slice(0, -1).join('-');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read doc.md file for a work item
|
|
96
|
+
* @param {string} workItemPath - Directory path of work item
|
|
97
|
+
* @returns {Promise<string|null>} Markdown content or null if not found
|
|
98
|
+
*/
|
|
99
|
+
async readDocumentation(workItemPath) {
|
|
100
|
+
try {
|
|
101
|
+
const docPath = path.join(workItemPath, 'doc.md');
|
|
102
|
+
return await fs.readFile(docPath, 'utf8');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get full details for a work item (including doc.md)
|
|
110
|
+
* @param {object} workItem - Base work item object
|
|
111
|
+
* @returns {Promise<object>} Enhanced work item with documentation
|
|
112
|
+
*/
|
|
113
|
+
async getFullDetails(workItem) {
|
|
114
|
+
const dirPath = workItem._dirPath;
|
|
115
|
+
const doc = await this.readDocumentation(dirPath);
|
|
116
|
+
return {
|
|
117
|
+
...workItem,
|
|
118
|
+
// doc.md opening paragraph is canonical; fall back to work.json value if no doc.md
|
|
119
|
+
description: doc ? extractDescriptionFromDoc(doc) || workItem.description : workItem.description,
|
|
120
|
+
documentation: doc,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|