bigpowers 1.3.1 → 1.4.0

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 (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/SKILL-INDEX.md +9 -9
  3. package/audit-code/SKILL.md +12 -0
  4. package/build-epic/SKILL.md +7 -4
  5. package/commit-message/SKILL.md +12 -0
  6. package/compose-workflow/SKILL.md +2 -0
  7. package/dashboard/bin/dashboard.js +93 -0
  8. package/dashboard/package-lock.json +1035 -0
  9. package/dashboard/package.json +18 -0
  10. package/dashboard/src/data/gate-status.js +32 -0
  11. package/dashboard/src/data/metrics.js +89 -0
  12. package/dashboard/src/data/pipeline-map.js +32 -0
  13. package/dashboard/src/data/reader.js +122 -0
  14. package/dashboard/src/data/watcher.js +108 -0
  15. package/dashboard/src/loaders/gate-status.js +32 -0
  16. package/dashboard/src/loaders/metrics.js +89 -0
  17. package/dashboard/src/loaders/pipeline-map.js +32 -0
  18. package/dashboard/src/loaders/reader.js +122 -0
  19. package/dashboard/src/loaders/watcher.js +108 -0
  20. package/dashboard/src/tui/epic-queue.js +36 -0
  21. package/dashboard/src/tui/filesystem.js +95 -0
  22. package/dashboard/src/tui/index.js +161 -0
  23. package/dashboard/src/tui/ledger.js +66 -0
  24. package/dashboard/src/tui/metrics-bar.js +30 -0
  25. package/dashboard/src/tui/pipeline.js +46 -0
  26. package/dashboard/src/tui/state-yaml.js +49 -0
  27. package/dashboard/src/web/client.html +477 -0
  28. package/dashboard/src/web/server.js +112 -0
  29. package/dashboard/test/fixtures/state.yaml +2 -0
  30. package/deepen-architecture/SKILL.md +2 -0
  31. package/define-language/SKILL.md +2 -0
  32. package/define-success/SKILL.md +4 -0
  33. package/delegate-task/SKILL.md +2 -0
  34. package/design-interface/SKILL.md +2 -0
  35. package/develop-tdd/SKILL.md +32 -0
  36. package/dispatch-agents/SKILL.md +2 -0
  37. package/edit-document/SKILL.md +6 -0
  38. package/elaborate-spec/SKILL.md +2 -0
  39. package/enforce-first/SKILL.md +2 -0
  40. package/grill-me/SKILL.md +2 -0
  41. package/guard-git/SKILL.md +2 -0
  42. package/hook-commits/SKILL.md +2 -0
  43. package/inspect-quality/SKILL.md +2 -0
  44. package/kickoff-branch/SKILL.md +5 -0
  45. package/map-codebase/SKILL.md +2 -0
  46. package/model-domain/SKILL.md +4 -0
  47. package/orchestrate-project/REFERENCE.md +1 -1
  48. package/orchestrate-project/SKILL.md +3 -1
  49. package/organize-workspace/SKILL.md +2 -0
  50. package/package.json +4 -2
  51. package/plan-refactor/SKILL.md +2 -0
  52. package/plan-work/SKILL.md +44 -0
  53. package/release-branch/SKILL.md +28 -0
  54. package/respond-review/SKILL.md +2 -0
  55. package/run-planning/SKILL.md +2 -0
  56. package/scripts/sync-bugs-registry.sh +16 -10
  57. package/scripts/sync-skills.sh +19 -0
  58. package/search-skills/SKILL.md +2 -0
  59. package/seed-conventions/SKILL.md +2 -0
  60. package/session-state/SKILL.md +16 -0
  61. package/setup-environment/SKILL.md +2 -0
  62. package/simulate-agents/SKILL.md +2 -0
  63. package/spike-prototype/SKILL.md +2 -0
  64. package/stocktake-skills/SKILL.md +2 -0
  65. package/survey-context/SKILL.md +23 -0
  66. package/terse-mode/SKILL.md +2 -0
  67. package/using-bigpowers/SKILL.md +4 -0
  68. package/validate-fix/SKILL.md +2 -0
  69. package/verify-work/SKILL.md +33 -0
  70. package/visual-dashboard/SKILL.md +2 -0
  71. package/wire-observability/SKILL.md +2 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "bigpowers-dashboard",
3
+ "version": "1.0.0",
4
+ "description": "Factory dashboard for bigpowers projects",
5
+ "private": true,
6
+ "main": "src/tui/index.js",
7
+ "scripts": {
8
+ "start": "node bin/dashboard.js",
9
+ "web": "node bin/dashboard.js --web",
10
+ "test": "node test/watcher.test.js && node test/smoke.test.js"
11
+ },
12
+ "dependencies": {
13
+ "blessed": "0.1.81",
14
+ "chokidar": "3.6.0",
15
+ "express": "4.19.0",
16
+ "js-yaml": "4.1.0"
17
+ }
18
+ }
@@ -0,0 +1,32 @@
1
+ const COLOR_MAP = {
2
+ ready: 'green',
3
+ blocked: 'red',
4
+ in_progress: 'yellow',
5
+ done: 'green',
6
+ pending: 'dim',
7
+ active: 'cyan'
8
+ };
9
+
10
+ const ICON_MAP = {
11
+ ready: '●',
12
+ blocked: '✕',
13
+ in_progress: '◐',
14
+ done: '✓',
15
+ pending: '○',
16
+ active: '◐'
17
+ };
18
+
19
+ function gateColor(gateStatus) {
20
+ return COLOR_MAP[gateStatus] || 'dim';
21
+ }
22
+
23
+ function gateIcon(gateStatus) {
24
+ return ICON_MAP[gateStatus] || '-';
25
+ }
26
+
27
+ module.exports = {
28
+ gateColor,
29
+ gateIcon,
30
+ COLOR_MAP,
31
+ ICON_MAP
32
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * computeEpicMetrics(cycleTimes)
3
+ * Groups cycle times by epic prefix (e.g. e01 from e01s01)
4
+ * Returns Map of epicId to { avgCycleMin, totalBcps, avgBcpPerHour }
5
+ */
6
+ function computeEpicMetrics(cycleTimes) {
7
+ if (!cycleTimes || cycleTimes.length === 0) {
8
+ return new Map();
9
+ }
10
+
11
+ const epicMap = new Map();
12
+ for (const story of cycleTimes) {
13
+ // Extract epic prefix: e01s01 -> e01
14
+ const epicId = story.id ? story.id.replace(/s\d+$/, '') : null;
15
+ if (!epicId) continue;
16
+
17
+ if (!epicMap.has(epicId)) {
18
+ epicMap.set(epicId, { stories: [] });
19
+ }
20
+ epicMap.get(epicId).stories.push(story);
21
+ }
22
+
23
+ const result = new Map();
24
+ for (const [epicId, data] of epicMap) {
25
+ const stories = data.stories;
26
+ const totalBcps = stories.reduce((sum, s) => sum + (s.bcps || 0), 0);
27
+ const totalMin = stories.reduce((sum, s) => sum + (s.cycleMin || 0), 0);
28
+ const avgCycleMin = stories.length > 0 ? totalMin / stories.length : 0;
29
+ const avgBcpPerHour = totalMin > 0 ? (totalBcps * 60) / totalMin : 0;
30
+
31
+ result.set(epicId, {
32
+ avgCycleMin,
33
+ totalBcps,
34
+ avgBcpPerHour
35
+ });
36
+ }
37
+
38
+ return result;
39
+ }
40
+
41
+ /**
42
+ * computeProjectMetrics(cycleTimes)
43
+ * Returns { totalBcps, totalMin, avgBcpPerHour } or null if no data
44
+ */
45
+ function computeProjectMetrics(cycleTimes) {
46
+ if (!cycleTimes || cycleTimes.length === 0) {
47
+ return null;
48
+ }
49
+
50
+ const totalBcps = cycleTimes.reduce((sum, s) => sum + (s.bcps || 0), 0);
51
+ const totalMin = cycleTimes.reduce((sum, s) => sum + (s.cycleMin || 0), 0);
52
+ const avgBcpPerHour = totalMin > 0 ? (totalBcps * 60) / totalMin : 0;
53
+
54
+ return {
55
+ totalBcps,
56
+ totalMin,
57
+ avgBcpPerHour
58
+ };
59
+ }
60
+
61
+ /**
62
+ * computeCurrentVelocity(cycleTimes, windowStories=3)
63
+ * Rolling average of last N stories
64
+ * Returns { avgBcpPerHour, avgCycleMin } or null if no data
65
+ */
66
+ function computeCurrentVelocity(cycleTimes, windowStories = 3) {
67
+ if (!cycleTimes || cycleTimes.length === 0) {
68
+ return null;
69
+ }
70
+
71
+ const window = Math.min(windowStories, cycleTimes.length);
72
+ const recentStories = cycleTimes.slice(-window);
73
+
74
+ const totalBcps = recentStories.reduce((sum, s) => sum + (s.bcps || 0), 0);
75
+ const totalMin = recentStories.reduce((sum, s) => sum + (s.cycleMin || 0), 0);
76
+ const avgCycleMin = recentStories.length > 0 ? totalMin / recentStories.length : 0;
77
+ const avgBcpPerHour = totalMin > 0 ? (totalBcps * 60) / totalMin : 0;
78
+
79
+ return {
80
+ avgBcpPerHour,
81
+ avgCycleMin
82
+ };
83
+ }
84
+
85
+ module.exports = {
86
+ computeEpicMetrics,
87
+ computeProjectMetrics,
88
+ computeCurrentVelocity
89
+ };
@@ -0,0 +1,32 @@
1
+ const STEPS = [
2
+ 'survey-context',
3
+ 'plan-work',
4
+ 'kickoff-branch',
5
+ 'develop-tdd',
6
+ 'verify-work',
7
+ 'audit-code',
8
+ 'commit-message',
9
+ 'release-branch'
10
+ ];
11
+
12
+ function stepIndex(currentStep) {
13
+ return STEPS.indexOf(currentStep);
14
+ }
15
+
16
+ function stepLabel(index) {
17
+ if (index < 0 || index >= STEPS.length) {
18
+ return '-';
19
+ }
20
+ return STEPS[index];
21
+ }
22
+
23
+ function allSteps() {
24
+ return [...STEPS];
25
+ }
26
+
27
+ module.exports = {
28
+ stepIndex,
29
+ stepLabel,
30
+ allSteps,
31
+ STEPS
32
+ };
@@ -0,0 +1,122 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+
5
+ /**
6
+ * readStateYaml(projectRoot)
7
+ * Reads specs/state.yaml and returns an object with mapped fields
8
+ * Returns null if file doesn't exist
9
+ */
10
+ function readStateYaml(projectRoot) {
11
+ try {
12
+ const filePath = path.join(projectRoot, 'specs', 'state.yaml');
13
+ if (!fs.existsSync(filePath)) {
14
+ return null;
15
+ }
16
+ const content = fs.readFileSync(filePath, 'utf8');
17
+ const data = yaml.load(content);
18
+ return {
19
+ activeFlow: data.active_flow || null,
20
+ activeEpic: data.active_epic_id || null,
21
+ activeStory: data.active_story_id || null,
22
+ epicCycle: data.epic_cycle || {},
23
+ gitBranch: data.git?.branch || null,
24
+ metrics: data.metrics || null,
25
+ release: data.release || {},
26
+ handoff: data.handoff || {}
27
+ };
28
+ } catch (err) {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * readExecutionStatus(projectRoot)
35
+ * Reads specs/execution-status.yaml and returns { epics: Map }
36
+ * Map keys are story/epic IDs, values are status strings
37
+ * Returns null if file doesn't exist
38
+ */
39
+ function readExecutionStatus(projectRoot) {
40
+ try {
41
+ const filePath = path.join(projectRoot, 'specs', 'execution-status.yaml');
42
+ if (!fs.existsSync(filePath)) {
43
+ return null;
44
+ }
45
+ const content = fs.readFileSync(filePath, 'utf8');
46
+ const data = yaml.load(content);
47
+ const statusMap = new Map();
48
+ const devStatus = data.development_status || {};
49
+ for (const [key, value] of Object.entries(devStatus)) {
50
+ statusMap.set(key, value);
51
+ }
52
+ return { epics: statusMap };
53
+ } catch (err) {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * readEpicShards(projectRoot)
60
+ * Reads all files in specs/epics/*.yaml
61
+ * Returns array of { id, title, stories } objects or null
62
+ */
63
+ function readEpicShards(projectRoot) {
64
+ try {
65
+ const epicsDir = path.join(projectRoot, 'specs', 'epics');
66
+ if (!fs.existsSync(epicsDir)) {
67
+ return null;
68
+ }
69
+ const files = fs.readdirSync(epicsDir)
70
+ .filter(f => f.endsWith('.yaml'))
71
+ .sort();
72
+
73
+ const epics = [];
74
+ for (const file of files) {
75
+ const filePath = path.join(epicsDir, file);
76
+ const content = fs.readFileSync(filePath, 'utf8');
77
+ const data = yaml.load(content);
78
+ epics.push({
79
+ id: data.id || null,
80
+ title: data.title || null,
81
+ stories: data.stories || []
82
+ });
83
+ }
84
+ return epics.length > 0 ? epics : null;
85
+ } catch (err) {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * readCycleTimes(projectRoot)
92
+ * Reads specs/metrics/cycle-times.yaml stories array
93
+ * Returns array of { id, bcps, start, end, cycleMin, bcpPerHour } or null
94
+ */
95
+ function readCycleTimes(projectRoot) {
96
+ try {
97
+ const filePath = path.join(projectRoot, 'specs', 'metrics', 'cycle-times.yaml');
98
+ if (!fs.existsSync(filePath)) {
99
+ return null;
100
+ }
101
+ const content = fs.readFileSync(filePath, 'utf8');
102
+ const data = yaml.load(content);
103
+ const stories = data.stories || [];
104
+ return stories.map(s => ({
105
+ id: s.id || null,
106
+ bcps: s.bcps || 0,
107
+ start: s.start || null,
108
+ end: s.end || null,
109
+ cycleMin: s.cycle_min || 0,
110
+ bcpPerHour: s.bcp_per_hour || 0
111
+ }));
112
+ } catch (err) {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ module.exports = {
118
+ readStateYaml,
119
+ readExecutionStatus,
120
+ readEpicShards,
121
+ readCycleTimes
122
+ };
@@ -0,0 +1,108 @@
1
+ const EventEmitter = require('events');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ let chokidar;
6
+ try {
7
+ chokidar = require('chokidar');
8
+ } catch (err) {
9
+ chokidar = null;
10
+ }
11
+
12
+ function watch(projectRoot) {
13
+ const emitter = new EventEmitter();
14
+ const debounceTimers = {};
15
+
16
+ function notifyChange(file) {
17
+ const key = file;
18
+ if (debounceTimers[key]) {
19
+ clearTimeout(debounceTimers[key]);
20
+ }
21
+ debounceTimers[key] = setTimeout(() => {
22
+ emitter.emit('change', { file, data: null });
23
+ delete debounceTimers[key];
24
+ }, 300);
25
+ }
26
+
27
+ if (chokidar) {
28
+ const filesToWatch = [
29
+ path.join(projectRoot, 'specs/state.yaml'),
30
+ path.join(projectRoot, 'specs/execution-status.yaml'),
31
+ path.join(projectRoot, 'specs/metrics/cycle-times.yaml'),
32
+ path.join(projectRoot, 'specs/epics')
33
+ ];
34
+
35
+ const watcher = chokidar.watch(filesToWatch, {
36
+ persistent: true,
37
+ awaitWriteFinish: {
38
+ stabilityThreshold: 100,
39
+ pollInterval: 100
40
+ }
41
+ });
42
+
43
+ watcher.on('change', (file) => {
44
+ notifyChange(file);
45
+ });
46
+
47
+ watcher.on('add', (file) => {
48
+ notifyChange(file);
49
+ });
50
+
51
+ emitter.close = () => watcher.close();
52
+ } else {
53
+ const fileStats = {};
54
+ const filesToWatch = [
55
+ path.join(projectRoot, 'specs/state.yaml'),
56
+ path.join(projectRoot, 'specs/execution-status.yaml'),
57
+ path.join(projectRoot, 'specs/metrics/cycle-times.yaml')
58
+ ];
59
+
60
+ const pollInterval = setInterval(() => {
61
+ filesToWatch.forEach((file) => {
62
+ try {
63
+ const stat = fs.statSync(file);
64
+ const mtime = stat.mtime.getTime();
65
+
66
+ if (!fileStats[file]) {
67
+ fileStats[file] = mtime;
68
+ } else if (fileStats[file] !== mtime) {
69
+ fileStats[file] = mtime;
70
+ notifyChange(file);
71
+ }
72
+ } catch (err) {
73
+ // File doesn't exist yet or was deleted
74
+ if (fileStats[file]) {
75
+ delete fileStats[file];
76
+ notifyChange(file);
77
+ }
78
+ }
79
+ });
80
+
81
+ // Check specs/epics directory
82
+ const epicsDir = path.join(projectRoot, 'specs/epics');
83
+ try {
84
+ const entries = fs.readdirSync(epicsDir);
85
+ entries.forEach((entry) => {
86
+ const fullPath = path.join(epicsDir, entry);
87
+ const stat = fs.statSync(fullPath);
88
+ const mtime = stat.mtime.getTime();
89
+
90
+ if (!fileStats[fullPath]) {
91
+ fileStats[fullPath] = mtime;
92
+ } else if (fileStats[fullPath] !== mtime) {
93
+ fileStats[fullPath] = mtime;
94
+ notifyChange(fullPath);
95
+ }
96
+ });
97
+ } catch (err) {
98
+ // epics directory doesn't exist yet
99
+ }
100
+ }, 200);
101
+
102
+ emitter.close = () => clearInterval(pollInterval);
103
+ }
104
+
105
+ return emitter;
106
+ }
107
+
108
+ module.exports = { watch };
@@ -0,0 +1,32 @@
1
+ const COLOR_MAP = {
2
+ ready: 'green',
3
+ blocked: 'red',
4
+ in_progress: 'yellow',
5
+ done: 'green',
6
+ pending: 'dim',
7
+ active: 'cyan'
8
+ };
9
+
10
+ const ICON_MAP = {
11
+ ready: '●',
12
+ blocked: '✕',
13
+ in_progress: '◐',
14
+ done: '✓',
15
+ pending: '○',
16
+ active: '◐'
17
+ };
18
+
19
+ function gateColor(gateStatus) {
20
+ return COLOR_MAP[gateStatus] || 'dim';
21
+ }
22
+
23
+ function gateIcon(gateStatus) {
24
+ return ICON_MAP[gateStatus] || '-';
25
+ }
26
+
27
+ module.exports = {
28
+ gateColor,
29
+ gateIcon,
30
+ COLOR_MAP,
31
+ ICON_MAP
32
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * computeEpicMetrics(cycleTimes)
3
+ * Groups cycle times by epic prefix (e.g. e01 from e01s01)
4
+ * Returns Map of epicId to { avgCycleMin, totalBcps, avgBcpPerHour }
5
+ */
6
+ function computeEpicMetrics(cycleTimes) {
7
+ if (!cycleTimes || cycleTimes.length === 0) {
8
+ return new Map();
9
+ }
10
+
11
+ const epicMap = new Map();
12
+ for (const story of cycleTimes) {
13
+ // Extract epic prefix: e01s01 -> e01
14
+ const epicId = story.id ? story.id.replace(/s\d+$/, '') : null;
15
+ if (!epicId) continue;
16
+
17
+ if (!epicMap.has(epicId)) {
18
+ epicMap.set(epicId, { stories: [] });
19
+ }
20
+ epicMap.get(epicId).stories.push(story);
21
+ }
22
+
23
+ const result = new Map();
24
+ for (const [epicId, data] of epicMap) {
25
+ const stories = data.stories;
26
+ const totalBcps = stories.reduce((sum, s) => sum + (s.bcps || 0), 0);
27
+ const totalMin = stories.reduce((sum, s) => sum + (s.cycleMin || 0), 0);
28
+ const avgCycleMin = stories.length > 0 ? totalMin / stories.length : 0;
29
+ const avgBcpPerHour = totalMin > 0 ? (totalBcps * 60) / totalMin : 0;
30
+
31
+ result.set(epicId, {
32
+ avgCycleMin,
33
+ totalBcps,
34
+ avgBcpPerHour
35
+ });
36
+ }
37
+
38
+ return result;
39
+ }
40
+
41
+ /**
42
+ * computeProjectMetrics(cycleTimes)
43
+ * Returns { totalBcps, totalMin, avgBcpPerHour } or null if no data
44
+ */
45
+ function computeProjectMetrics(cycleTimes) {
46
+ if (!cycleTimes || cycleTimes.length === 0) {
47
+ return null;
48
+ }
49
+
50
+ const totalBcps = cycleTimes.reduce((sum, s) => sum + (s.bcps || 0), 0);
51
+ const totalMin = cycleTimes.reduce((sum, s) => sum + (s.cycleMin || 0), 0);
52
+ const avgBcpPerHour = totalMin > 0 ? (totalBcps * 60) / totalMin : 0;
53
+
54
+ return {
55
+ totalBcps,
56
+ totalMin,
57
+ avgBcpPerHour
58
+ };
59
+ }
60
+
61
+ /**
62
+ * computeCurrentVelocity(cycleTimes, windowStories=3)
63
+ * Rolling average of last N stories
64
+ * Returns { avgBcpPerHour, avgCycleMin } or null if no data
65
+ */
66
+ function computeCurrentVelocity(cycleTimes, windowStories = 3) {
67
+ if (!cycleTimes || cycleTimes.length === 0) {
68
+ return null;
69
+ }
70
+
71
+ const window = Math.min(windowStories, cycleTimes.length);
72
+ const recentStories = cycleTimes.slice(-window);
73
+
74
+ const totalBcps = recentStories.reduce((sum, s) => sum + (s.bcps || 0), 0);
75
+ const totalMin = recentStories.reduce((sum, s) => sum + (s.cycleMin || 0), 0);
76
+ const avgCycleMin = recentStories.length > 0 ? totalMin / recentStories.length : 0;
77
+ const avgBcpPerHour = totalMin > 0 ? (totalBcps * 60) / totalMin : 0;
78
+
79
+ return {
80
+ avgBcpPerHour,
81
+ avgCycleMin
82
+ };
83
+ }
84
+
85
+ module.exports = {
86
+ computeEpicMetrics,
87
+ computeProjectMetrics,
88
+ computeCurrentVelocity
89
+ };
@@ -0,0 +1,32 @@
1
+ const STEPS = [
2
+ 'survey-context',
3
+ 'plan-work',
4
+ 'kickoff-branch',
5
+ 'develop-tdd',
6
+ 'verify-work',
7
+ 'audit-code',
8
+ 'commit-message',
9
+ 'release-branch'
10
+ ];
11
+
12
+ function stepIndex(currentStep) {
13
+ return STEPS.indexOf(currentStep);
14
+ }
15
+
16
+ function stepLabel(index) {
17
+ if (index < 0 || index >= STEPS.length) {
18
+ return '-';
19
+ }
20
+ return STEPS[index];
21
+ }
22
+
23
+ function allSteps() {
24
+ return [...STEPS];
25
+ }
26
+
27
+ module.exports = {
28
+ stepIndex,
29
+ stepLabel,
30
+ allSteps,
31
+ STEPS
32
+ };
@@ -0,0 +1,122 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+
5
+ /**
6
+ * readStateYaml(projectRoot)
7
+ * Reads specs/state.yaml and returns an object with mapped fields
8
+ * Returns null if file doesn't exist
9
+ */
10
+ function readStateYaml(projectRoot) {
11
+ try {
12
+ const filePath = path.join(projectRoot, 'specs', 'state.yaml');
13
+ if (!fs.existsSync(filePath)) {
14
+ return null;
15
+ }
16
+ const content = fs.readFileSync(filePath, 'utf8');
17
+ const data = yaml.load(content);
18
+ return {
19
+ activeFlow: data.active_flow || null,
20
+ activeEpic: data.active_epic_id || null,
21
+ activeStory: data.active_story_id || null,
22
+ epicCycle: data.epic_cycle || {},
23
+ gitBranch: data.git?.branch || null,
24
+ metrics: data.metrics || null,
25
+ release: data.release || {},
26
+ handoff: data.handoff || {}
27
+ };
28
+ } catch (err) {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * readExecutionStatus(projectRoot)
35
+ * Reads specs/execution-status.yaml and returns { epics: Map }
36
+ * Map keys are story/epic IDs, values are status strings
37
+ * Returns null if file doesn't exist
38
+ */
39
+ function readExecutionStatus(projectRoot) {
40
+ try {
41
+ const filePath = path.join(projectRoot, 'specs', 'execution-status.yaml');
42
+ if (!fs.existsSync(filePath)) {
43
+ return null;
44
+ }
45
+ const content = fs.readFileSync(filePath, 'utf8');
46
+ const data = yaml.load(content);
47
+ const statusMap = new Map();
48
+ const devStatus = data.development_status || {};
49
+ for (const [key, value] of Object.entries(devStatus)) {
50
+ statusMap.set(key, value);
51
+ }
52
+ return { epics: statusMap };
53
+ } catch (err) {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * readEpicShards(projectRoot)
60
+ * Reads all files in specs/epics/*.yaml
61
+ * Returns array of { id, title, stories } objects or null
62
+ */
63
+ function readEpicShards(projectRoot) {
64
+ try {
65
+ const epicsDir = path.join(projectRoot, 'specs', 'epics');
66
+ if (!fs.existsSync(epicsDir)) {
67
+ return null;
68
+ }
69
+ const files = fs.readdirSync(epicsDir)
70
+ .filter(f => f.endsWith('.yaml'))
71
+ .sort();
72
+
73
+ const epics = [];
74
+ for (const file of files) {
75
+ const filePath = path.join(epicsDir, file);
76
+ const content = fs.readFileSync(filePath, 'utf8');
77
+ const data = yaml.load(content);
78
+ epics.push({
79
+ id: data.id || null,
80
+ title: data.title || null,
81
+ stories: data.stories || []
82
+ });
83
+ }
84
+ return epics.length > 0 ? epics : null;
85
+ } catch (err) {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * readCycleTimes(projectRoot)
92
+ * Reads specs/metrics/cycle-times.yaml stories array
93
+ * Returns array of { id, bcps, start, end, cycleMin, bcpPerHour } or null
94
+ */
95
+ function readCycleTimes(projectRoot) {
96
+ try {
97
+ const filePath = path.join(projectRoot, 'specs', 'metrics', 'cycle-times.yaml');
98
+ if (!fs.existsSync(filePath)) {
99
+ return null;
100
+ }
101
+ const content = fs.readFileSync(filePath, 'utf8');
102
+ const data = yaml.load(content);
103
+ const stories = data.stories || [];
104
+ return stories.map(s => ({
105
+ id: s.id || null,
106
+ bcps: s.bcps || 0,
107
+ start: s.start || null,
108
+ end: s.end || null,
109
+ cycleMin: s.cycle_min || 0,
110
+ bcpPerHour: s.bcp_per_hour || 0
111
+ }));
112
+ } catch (err) {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ module.exports = {
118
+ readStateYaml,
119
+ readExecutionStatus,
120
+ readEpicShards,
121
+ readCycleTimes
122
+ };