bigpowers 2.1.1 → 2.1.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.1.3](https://github.com/danielvm-git/bigpowers/compare/v2.1.2...v2.1.3) (2026-06-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **dashboard:** resolve wrong folder targeting and update TUI metrics ([bc8d84a](https://github.com/danielvm-git/bigpowers/commit/bc8d84af6221aa59aa131bf3fdd03f25bc67ac33))
7
+
8
+ ## [2.1.2](https://github.com/danielvm-git/bigpowers/compare/v2.1.1...v2.1.2) (2026-06-12)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **dashboard:** support alternative epic id and title keys in yaml parser ([d911e8f](https://github.com/danielvm-git/bigpowers/commit/d911e8f61c608a96d8d3d42cb4a4d19cff71ee3d))
14
+
1
15
  ## [2.1.1](https://github.com/danielvm-git/bigpowers/compare/v2.1.0...v2.1.1) (2026-06-11)
2
16
 
3
17
 
@@ -66,21 +66,48 @@ function readEpicShards(projectRoot) {
66
66
  if (!fs.existsSync(epicsDir)) {
67
67
  return null;
68
68
  }
69
- const files = fs.readdirSync(epicsDir)
70
- .filter(f => f.endsWith('.yaml'))
71
- .sort();
72
-
73
69
  const epics = [];
74
- for (const file of files) {
75
- const filePath = path.join(epicsDir, file);
76
- const content = fs.readFileSync(filePath, 'utf8');
70
+
71
+ function scanDir(dirPath) {
72
+ if (!fs.existsSync(dirPath)) return;
73
+ const entries = fs.readdirSync(dirPath);
74
+ for (const entry of entries) {
75
+ const entryPath = path.join(dirPath, entry);
76
+ const stat = fs.statSync(entryPath);
77
+ if (stat.isDirectory()) {
78
+ if (entry === 'archive') {
79
+ scanDir(entryPath);
80
+ } else {
81
+ const possibleYaml = path.join(entryPath, 'epic.yaml');
82
+ if (fs.existsSync(possibleYaml)) {
83
+ loadYaml(possibleYaml);
84
+ }
85
+ }
86
+ } else if (stat.isFile() && entry.endsWith('.yaml')) {
87
+ loadYaml(entryPath);
88
+ }
89
+ }
90
+ }
91
+
92
+ function loadYaml(yamlPath) {
93
+ const content = fs.readFileSync(yamlPath, 'utf8');
77
94
  const data = yaml.load(content);
78
95
  epics.push({
79
- id: data.id || null,
80
- title: data.title || null,
96
+ id: data.id || data.epic || null,
97
+ title: data.title || data.name || null,
81
98
  stories: data.stories || []
82
99
  });
83
100
  }
101
+
102
+ scanDir(epicsDir);
103
+
104
+ // Sort epics by ID numerically/alphabetically
105
+ epics.sort((a, b) => {
106
+ const idA = a.id || '';
107
+ const idB = b.id || '';
108
+ return idA.localeCompare(idB, undefined, { numeric: true, sensitivity: 'base' });
109
+ });
110
+
84
111
  return epics.length > 0 ? epics : null;
85
112
  } catch (err) {
86
113
  return null;
@@ -156,7 +156,7 @@ function start(projectRoot) {
156
156
  // Title bar: fixed project identity
157
157
  titleBar.setContent(' {bold}{cyan-fg}⚙ bigpowers factory{/cyan-fg}{/bold} {gray-fg}v2 — seed {cyan-fg}→{/cyan-fg} epics {cyan-fg}→{/cyan-fg} mvp{/gray-fg}');
158
158
 
159
- renderMetricsBar(metricsBar, metrics, stateData, epics, cycleTimes);
159
+ renderMetricsBar(metricsBar, metrics, stateData, epics, cycleTimes, executionStatus);
160
160
  renderPipeline(pipeline, stateData);
161
161
  renderEpicQueue(epicQueue, epics, executionStatus);
162
162
  renderStateYaml(actionLog, stateData);
@@ -1,29 +1,40 @@
1
- function renderMetricsBar(box, projectMetrics, stateData, epics, cycleTimes) {
1
+ function renderMetricsBar(box, projectMetrics, stateData, epics, cycleTimes, executionStatus) {
2
2
  if (!box || typeof box.setContent !== 'function') {
3
3
  return;
4
4
  }
5
5
 
6
- const ct = cycleTimes || [];
7
6
  const epicList = epics && Array.isArray(epics) ? epics : [];
7
+ const statusMap = executionStatus?.epics || new Map();
8
8
 
9
- // Compute totals
9
+ // Compute totals and completed counts dynamically from execution-status.yaml
10
10
  let totalStories = 0;
11
11
  let targetBcps = 0;
12
+ let doneStories = 0;
13
+ let deliveredBcps = 0;
14
+ let doneEpics = 0;
15
+
12
16
  epicList.forEach(epic => {
17
+ let epicDone = true;
18
+ let hasStories = false;
13
19
  if (epic.stories && Array.isArray(epic.stories)) {
14
- totalStories += epic.stories.length;
15
- epic.stories.forEach(s => { targetBcps += s.bcps || 0; });
20
+ epic.stories.forEach(s => {
21
+ hasStories = true;
22
+ totalStories++;
23
+ targetBcps += s.bcps || 0;
24
+ if (statusMap.get(s.id) === 'done') {
25
+ doneStories++;
26
+ deliveredBcps += s.bcps || 0;
27
+ } else {
28
+ epicDone = false;
29
+ }
30
+ });
31
+ }
32
+ if (hasStories && epicDone) {
33
+ doneEpics++;
16
34
  }
17
35
  });
18
36
 
19
- const doneStories = ct.length;
20
37
  const totalEpics = epicList.length;
21
- const doneStoryIds = new Set(ct.map(c => c.id));
22
- const doneEpics = epicList.filter(e =>
23
- e.stories && e.stories.length > 0 && e.stories.every(s => doneStoryIds.has(s.id))
24
- ).length;
25
-
26
- const deliveredBcps = projectMetrics?.totalBcps ?? 0;
27
38
  const totalMin = projectMetrics?.totalMin ?? 0;
28
39
  const avgBcpPerHour = projectMetrics?.avgBcpPerHour;
29
40
  const version = stateData?.release?.target_version ?? '—';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "61 agent skills for spec-driven, test-first software development by solo developers",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -41,7 +41,7 @@ function parseEpicsFromReleasePlan(text) {
41
41
  const blocks = text.split(/\n\s*-\s+id:\s+/).slice(1);
42
42
  for (const block of blocks) {
43
43
  const id = block.match(/^(\S+)/)?.[1];
44
- const title = block.match(/title:\s*"?([^"\n]+)"?/)?.[1];
44
+ const title = block.match(/(?:title|name):\s*"?([^"\n]+)"?/)?.[1];
45
45
  const wsjf = parseFloat(block.match(/wsjf:\s*([\d.]+)/)?.[1] || '0');
46
46
  const file = block.match(/file:\s*(\S+)/)?.[1];
47
47
  if (id) epics.push({ id, title: title || id, wsjf, file });
@@ -50,7 +50,7 @@ function parseEpicsFromReleasePlan(text) {
50
50
  }
51
51
 
52
52
  function parseSimpleEpic(text) {
53
- const title = text.match(/^title:\s*"?([^"\n]+)"?/m)?.[1];
53
+ const title = text.match(/^(?:title|name):\s*"?([^"\n]+)"?/m)?.[1];
54
54
  const stories = [];
55
55
  const storyBlocks = text.split(/\n\s*-\s+id:\s+/).slice(1);
56
56
  for (const sb of storyBlocks) {