bigpowers 2.0.1 → 2.1.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.1.1](https://github.com/danielvm-git/bigpowers/compare/v2.1.0...v2.1.1) (2026-06-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **dashboard/tui:** replace invalid {dim} tags with {gray-fg} across all render modules ([f029360](https://github.com/danielvm-git/bigpowers/commit/f02936068b024107e7685accb62fe2bb1b0917b7))
7
+
8
+ # [2.1.0](https://github.com/danielvm-git/bigpowers/compare/v2.0.1...v2.1.0) (2026-06-11)
9
+
10
+
11
+ ### Features
12
+
13
+ * **dashboard/tui:** render blessed tags and improve layout ([fba0e49](https://github.com/danielvm-git/bigpowers/commit/fba0e4918a4d99c94c30e595378565263d72734d))
14
+
1
15
  ## [2.0.1](https://github.com/danielvm-git/bigpowers/compare/v2.0.0...v2.0.1) (2026-06-11)
2
16
 
3
17
 
@@ -1,36 +1,75 @@
1
- const { gateIcon, gateColor } = require('../loaders/gate-status');
2
-
3
1
  function renderEpicQueue(box, epics, executionStatus) {
4
2
  if (!box || typeof box.setContent !== 'function') {
5
3
  return;
6
4
  }
7
5
 
8
6
  if (!epics || epics.length === 0) {
9
- box.setContent('{dim}no epic shards found{/dim}');
7
+ box.setContent('{gray-fg}no epic shards found{/gray-fg}');
10
8
  return;
11
9
  }
12
10
 
13
- let lines = [];
14
- lines.push('{bold}EPIC QUEUE{/bold}');
15
- lines.push('');
16
- let totalBcps = 0;
11
+ const statusMap = executionStatus?.epics || new Map();
12
+ const doneStatuses = new Set(['done', 'complete', 'completed', 'released']);
13
+ const activeStatuses = new Set(['in_progress', 'running', 'active']);
17
14
 
18
- epics.forEach(epic => {
19
- const epicStatusIcon = gateIcon[epic.status] || '?';
20
- lines.push(`${epicStatusIcon} ${epic.id} ${epic.title}`);
21
-
22
- if (epic.stories && epic.stories.length > 0) {
23
- epic.stories.forEach(story => {
24
- const storyStatusIcon = gateIcon[story.status] || '?';
25
- const bcpCount = story.bcps || 0;
26
- totalBcps += bcpCount;
27
- lines.push(` ${storyStatusIcon} ${story.id} ${story.title} (${bcpCount} BCPs)`);
28
- });
15
+ function dot(id) {
16
+ const s = statusMap.get(id) || '';
17
+ if (doneStatuses.has(s)) return '{green-fg}{/green-fg}';
18
+ if (activeStatuses.has(s)) return '{yellow-fg}●{/yellow-fg}';
19
+ return '{gray-fg}·{/gray-fg}';
20
+ }
21
+
22
+ function epicDot(epic) {
23
+ const s = statusMap.get(epic.id) || '';
24
+ if (doneStatuses.has(s)) return '{green-fg}{/green-fg}';
25
+ if (activeStatuses.has(s)) return '{yellow-fg}●{/yellow-fg}';
26
+ const stories = epic.stories || [];
27
+ if (stories.length > 0 && stories.every(st => doneStatuses.has(statusMap.get(st.id) || ''))) {
28
+ return '{green-fg}●{/green-fg}';
29
29
  }
30
+ return '{gray-fg}·{/gray-fg}';
31
+ }
32
+
33
+ const lines = [];
34
+ let grandTotalBcps = 0;
35
+ const perEpicBcp = [];
36
+
37
+ epics.forEach(epic => {
38
+ lines.push(`${epicDot(epic)} {bold}${epic.id}{/bold} {gray-fg}·{/gray-fg} ${epic.title || '—'}`);
39
+
40
+ const stories = epic.stories || [];
41
+ let epicBcpDone = 0;
42
+ let epicBcpTotal = 0;
43
+ let epicStoriesDone = 0;
44
+
45
+ stories.forEach(story => {
46
+ const bcp = story.bcps || 0;
47
+ epicBcpTotal += bcp;
48
+ grandTotalBcps += bcp;
49
+ const isDone = doneStatuses.has(statusMap.get(story.id) || '');
50
+ if (isDone) { epicBcpDone += bcp; epicStoriesDone++; }
51
+
52
+ const bcpStr = isDone
53
+ ? `{green-fg}${bcp} BCP{/green-fg}`
54
+ : `{gray-fg}${bcp} BCP{/gray-fg}`;
55
+ lines.push(` ${dot(story.id)} {gray-fg}${story.id}{/gray-fg} ${story.title || '—'} ${bcpStr}`);
56
+ });
57
+
58
+ perEpicBcp.push({ id: epic.id, bcps: epicBcpTotal });
59
+
60
+ const bcpProgressStr = epicBcpDone > 0
61
+ ? `{green-fg}${epicBcpDone}{/green-fg}{gray-fg}/${epicBcpTotal} BCP{/gray-fg}`
62
+ : `{gray-fg}0/${epicBcpTotal} BCP{/gray-fg}`;
63
+ lines.push(` {gray-fg}${epicStoriesDone}/${stories.length} done · ${bcpProgressStr}`);
64
+ lines.push('');
30
65
  });
31
66
 
32
- lines.push('');
33
- lines.push(`Total: ${totalBcps} BCPs`);
67
+ // Release baseline footer
68
+ lines.push('{gray-fg}─────────────────────────{/gray-fg}');
69
+ lines.push(`{gray-fg}release baseline{/gray-fg}`);
70
+ lines.push(`{gray-fg}total: {/gray-fg}{yellow-fg}${grandTotalBcps} BCPs{/yellow-fg}`);
71
+ lines.push(perEpicBcp.map(e => `{gray-fg}${e.id}: ${e.bcps} BCP{/gray-fg}`).join(' '));
72
+ lines.push(`{gray-fg}target: {/gray-fg}{bold}v2.0.0{/bold}`);
34
73
 
35
74
  box.setContent(lines.join('\n'));
36
75
  }
@@ -9,14 +9,12 @@ function renderFilesystem(box, projectRoot) {
9
9
  const specsPath = path.join(projectRoot, 'specs');
10
10
 
11
11
  if (!fs.existsSync(specsPath)) {
12
- box.setContent('{center}{dim}specs/ not found{/dim}{/center}');
12
+ box.setContent('{center}{gray-fg}specs/ not found{/gray-fg}{/center}');
13
13
  return;
14
14
  }
15
15
 
16
16
  const now = Date.now();
17
17
  const lines = [];
18
- lines.push('{bold}FILE SYSTEM{/bold}');
19
-
20
18
  let fileCount = 0;
21
19
 
22
20
  try {
@@ -41,7 +39,7 @@ function renderFilesystem(box, projectRoot) {
41
39
  }
42
40
 
43
41
  fileCount = countFiles(specsPath);
44
- lines.push(`{dim}[${fileCount} files]{/dim}`);
42
+ lines.push(`{gray-fg}[${fileCount} files]{/gray-fg}`);
45
43
  lines.push('');
46
44
 
47
45
  // Build tree
@@ -85,7 +83,7 @@ function renderFilesystem(box, projectRoot) {
85
83
 
86
84
  buildTree(specsPath);
87
85
  } catch (err) {
88
- lines.push('{dim}Error reading specs/{/dim}');
86
+ lines.push('{gray-fg}Error reading specs/{/gray-fg}');
89
87
  }
90
88
 
91
89
  box.setContent(lines.join('\n'));
@@ -35,14 +35,14 @@ function start(projectRoot) {
35
35
  });
36
36
 
37
37
  // Check terminal size
38
- if (screen.width < 120 || screen.height < 30) {
38
+ if (screen.width < 120 || screen.height < 34) {
39
39
  const msg = blessed.box({
40
40
  parent: screen,
41
41
  top: 'center',
42
42
  left: 'center',
43
43
  width: 60,
44
44
  height: 10,
45
- content: '{center}Terminal must be at least 120x30{/center}\n{center}Please resize and try again{/center}',
45
+ content: '{center}Terminal must be at least 120x34{/center}\n{center}Please resize and try again{/center}',
46
46
  border: 'line',
47
47
  tags: true,
48
48
  style: { border: { fg: 'yellow' } }
@@ -58,17 +58,16 @@ function start(projectRoot) {
58
58
  left: 0,
59
59
  width: '100%',
60
60
  height: 1,
61
- border: 'line',
62
61
  tags: true,
63
- style: { border: { fg: 'magenta' } }
64
62
  });
65
63
 
64
+ // height 5 = 3 content lines + top/bottom borders: status bar, step info, stats row
66
65
  const metricsBar = blessed.box({
67
66
  parent: screen,
68
67
  top: 1,
69
68
  left: 0,
70
69
  width: '100%',
71
- height: 3,
70
+ height: 5,
72
71
  border: 'line',
73
72
  tags: true,
74
73
  style: { border: { fg: 'blue' } }
@@ -76,54 +75,58 @@ function start(projectRoot) {
76
75
 
77
76
  const pipeline = blessed.box({
78
77
  parent: screen,
79
- top: 4,
78
+ top: 6,
80
79
  left: 0,
81
80
  width: '100%',
82
81
  height: 5,
83
82
  border: 'line',
84
83
  tags: true,
85
- style: { border: { fg: 'cyan' } }
84
+ label: ' {green-fg}≡ PIPELINE{/green-fg} ',
85
+ style: { border: { fg: 'green' } }
86
86
  });
87
87
 
88
88
  const epicQueue = blessed.box({
89
89
  parent: screen,
90
- top: 9,
90
+ top: 11,
91
91
  left: 0,
92
92
  width: '33%',
93
- height: screen.height - 21,
93
+ height: screen.height - 23,
94
94
  border: 'line',
95
95
  scrollable: true,
96
96
  mouse: true,
97
97
  keys: true,
98
98
  tags: true,
99
+ label: ' {green-fg}≡ EPIC QUEUE{/green-fg} ',
99
100
  style: { border: { fg: 'green' } }
100
101
  });
101
102
 
102
103
  const actionLog = blessed.box({
103
104
  parent: screen,
104
- top: 9,
105
+ top: 11,
105
106
  left: '33%',
106
107
  width: '33%',
107
- height: screen.height - 21,
108
+ height: screen.height - 23,
108
109
  border: 'line',
109
110
  scrollable: true,
110
111
  mouse: true,
111
112
  keys: true,
112
113
  tags: true,
113
- style: { border: { fg: 'yellow' } }
114
+ label: ' {cyan-fg}>_ STATE.YAML{/cyan-fg} ',
115
+ style: { border: { fg: 'cyan' } }
114
116
  });
115
117
 
116
118
  const fsPanel = blessed.box({
117
119
  parent: screen,
118
- top: 9,
120
+ top: 11,
119
121
  left: '66%',
120
122
  width: '34%',
121
- height: screen.height - 21,
123
+ height: screen.height - 23,
122
124
  border: 'line',
123
125
  scrollable: true,
124
126
  mouse: true,
125
127
  keys: true,
126
128
  tags: true,
129
+ label: ' {magenta-fg}□ FILESYSTEM{/magenta-fg} ',
127
130
  style: { border: { fg: 'magenta' } }
128
131
  });
129
132
 
@@ -138,7 +141,8 @@ function start(projectRoot) {
138
141
  mouse: true,
139
142
  keys: true,
140
143
  tags: true,
141
- style: { border: { fg: 'white' } }
144
+ label: ' {cyan-fg}⊙ CYCLE TIME LEDGER BCPS · TIMESTAMPS · THROUGHPUT{/cyan-fg} ',
145
+ style: { border: { fg: 'cyan' } }
142
146
  });
143
147
 
144
148
  // Refresh function
@@ -149,12 +153,10 @@ function start(projectRoot) {
149
153
  const cycleTimes = readCycleTimes(projectRoot);
150
154
  const metrics = computeProjectMetrics(cycleTimes);
151
155
 
152
- // Render title bar
153
- const epicTitle = stateData?.active_epic || '—';
154
- const stepNum = stateData?.epicCycle?.current_step ? (stateData.epicCycle.completed_steps?.length || 0) + 1 : 0;
155
- titleBar.setContent(`{bold}{magenta}bigpowers factory{/magenta}{/bold} — ${epicTitle} — step ${stepNum} / 8`);
156
+ // Title bar: fixed project identity
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}');
156
158
 
157
- renderMetricsBar(metricsBar, metrics, stateData, epics);
159
+ renderMetricsBar(metricsBar, metrics, stateData, epics, cycleTimes);
158
160
  renderPipeline(pipeline, stateData);
159
161
  renderEpicQueue(epicQueue, epics, executionStatus);
160
162
  renderStateYaml(actionLog, stateData);
@@ -1,61 +1,56 @@
1
+ function pad(str, width) {
2
+ const s = String(str);
3
+ return s.length >= width ? s : s + ' '.repeat(width - s.length);
4
+ }
5
+
1
6
  function renderLedger(box, cycleTimes) {
2
7
  if (!box || typeof box.setContent !== 'function') {
3
8
  return;
4
9
  }
5
10
 
6
11
  if (!cycleTimes || cycleTimes.length === 0) {
7
- box.setContent('{center}{dim}no completed stories yet{/dim}{/center}');
12
+ box.setContent('{gray-fg}stories complete here as they land{/gray-fg}');
8
13
  return;
9
14
  }
10
15
 
11
16
  const lines = [];
12
- lines.push('{bold}{cyan}Ledger{/cyan}{/bold}');
13
- lines.push('');
14
17
 
15
- // Header row
18
+ // Column widths
19
+ const W = { sid: 12, epic: 6, bcps: 5, min: 8, bhr: 7 };
20
+
21
+ // Header
16
22
  lines.push(
17
- '{bold}' +
18
- '{cyan}Story ID{/cyan} ' +
19
- '{cyan}Epic{/cyan} ' +
20
- '{cyan}BCPs{/cyan} ' +
21
- '{cyan}Minutes{/cyan} ' +
22
- '{cyan}BCP/hr{/cyan}' +
23
- '{/bold}'
23
+ `{cyan-fg}{bold}${pad('Story ID', W.sid)}${pad('Epic', W.epic)}${pad('BCPs', W.bcps)}${pad('Minutes', W.min)}${pad('BCP/hr', W.bhr)}{/bold}{/cyan-fg}`
24
24
  );
25
+ lines.push('{gray-fg}' + '─'.repeat(W.sid + W.epic + W.bcps + W.min + W.bhr) + '{/gray-fg}');
25
26
 
26
27
  let totalBCPs = 0;
27
28
  let totalMinutes = 0;
28
29
 
29
- // Data rows
30
- cycleTimes.forEach((cycle) => {
30
+ cycleTimes.forEach(cycle => {
31
31
  const storyId = cycle.id || '—';
32
- const epicPrefix = cycle.epic || '—';
32
+ const epicId = cycle.epic || '—';
33
33
  const bcps = cycle.bcps || 0;
34
34
  const minutes = cycle.cycleMin || 0;
35
- const bcpPerHour = minutes > 0 ? ((bcps * 60) / minutes).toFixed(1) : '—';
35
+ const bph = minutes > 0 ? ((bcps * 60) / minutes).toFixed(1) : '—';
36
36
 
37
37
  totalBCPs += bcps;
38
38
  totalMinutes += minutes;
39
39
 
40
40
  lines.push(
41
- ` ${storyId} ` +
42
- `${epicPrefix} ` +
43
- `${bcps} ` +
44
- `${minutes} ` +
45
- `${bcpPerHour}`
41
+ `{white-fg}${pad(storyId, W.sid)}{/white-fg}` +
42
+ `{gray-fg}${pad(epicId, W.epic)}{/gray-fg}` +
43
+ `{yellow-fg}${pad(bcps, W.bcps)}{/yellow-fg}` +
44
+ `{gray-fg}${pad(minutes, W.min)}{/gray-fg}` +
45
+ `{green-fg}${pad(bph, W.bhr)}{/green-fg}`
46
46
  );
47
47
  });
48
48
 
49
- lines.push('');
50
-
51
- // Totals row
52
- const avgBcpPerHour =
53
- totalMinutes > 0 ? ((totalBCPs * 60) / totalMinutes).toFixed(1) : '—';
49
+ lines.push('{gray-fg}' + '─'.repeat(W.sid + W.epic + W.bcps + W.min + W.bhr) + '{/gray-fg}');
54
50
 
51
+ const avgBph = totalMinutes > 0 ? ((totalBCPs * 60) / totalMinutes).toFixed(1) : '—';
55
52
  lines.push(
56
- '{bold}{yellow}' +
57
- `TOTAL — ${totalBCPs} ${totalMinutes} ${avgBcpPerHour}` +
58
- '{/yellow}{/bold}'
53
+ `{bold}{yellow-fg}${pad('TOTAL', W.sid)}${pad('—', W.epic)}${pad(totalBCPs, W.bcps)}${pad(totalMinutes, W.min)}${pad(avgBph, W.bhr)}{/yellow-fg}{/bold}`
59
54
  );
60
55
 
61
56
  box.setContent(lines.join('\n'));
@@ -1,47 +1,82 @@
1
- function renderMetricsBar(box, projectMetrics, stateData, epics) {
1
+ function renderMetricsBar(box, projectMetrics, stateData, epics, cycleTimes) {
2
2
  if (!box || typeof box.setContent !== 'function') {
3
3
  return;
4
4
  }
5
5
 
6
- // Count epics and stories
7
- let epicCount = 0;
8
- let storyCount = 0;
6
+ const ct = cycleTimes || [];
7
+ const epicList = epics && Array.isArray(epics) ? epics : [];
8
+
9
+ // Compute totals
10
+ let totalStories = 0;
9
11
  let targetBcps = 0;
12
+ epicList.forEach(epic => {
13
+ if (epic.stories && Array.isArray(epic.stories)) {
14
+ totalStories += epic.stories.length;
15
+ epic.stories.forEach(s => { targetBcps += s.bcps || 0; });
16
+ }
17
+ });
10
18
 
11
- if (epics && Array.isArray(epics)) {
12
- epicCount = epics.length;
13
- epics.forEach(epic => {
14
- if (epic.stories && Array.isArray(epic.stories)) {
15
- storyCount += epic.stories.length;
16
- epic.stories.forEach(story => {
17
- targetBcps += story.bcps || 0;
18
- });
19
- }
20
- });
21
- }
19
+ const doneStories = ct.length;
20
+ 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;
22
25
 
23
- const deliveredBcps = projectMetrics?.totalBcps ?? '-';
24
- const totalMin = projectMetrics?.totalMin ?? '-';
25
- const avgBcpPerHour = projectMetrics?.avgBcpPerHour ?? '-';
26
- const version = stateData?.release?.target_version ?? '-';
26
+ const deliveredBcps = projectMetrics?.totalBcps ?? 0;
27
+ const totalMin = projectMetrics?.totalMin ?? 0;
28
+ const avgBcpPerHour = projectMetrics?.avgBcpPerHour;
29
+ const version = stateData?.release?.target_version ?? '';
30
+ const branch = stateData?.gitBranch ?? 'main';
27
31
 
28
- let bcpHrColor = 'white';
32
+ // BCP/hr color threshold
33
+ let bphColor = 'white';
29
34
  if (typeof avgBcpPerHour === 'number') {
30
- if (avgBcpPerHour >= 2.0) {
31
- bcpHrColor = 'green';
32
- } else if (avgBcpPerHour >= 1.0) {
33
- bcpHrColor = 'yellow';
34
- } else {
35
- bcpHrColor = 'red';
36
- }
35
+ if (avgBcpPerHour >= 2.0) bphColor = 'green';
36
+ else if (avgBcpPerHour >= 1.0) bphColor = 'yellow';
37
+ else bphColor = 'red';
37
38
  }
39
+ const bphDisplay = typeof avgBcpPerHour === 'number' ? avgBcpPerHour.toFixed(2) : '—';
40
+ const cycleDisplay = totalMin > 0 ? `${totalMin}m` : '—';
41
+
42
+ // ── Line 1: status bar ──
43
+ const statusLine =
44
+ `{gray-fg}BCPs:{/gray-fg} {yellow-fg}${deliveredBcps}{/yellow-fg}{gray-fg}/${targetBcps}{/gray-fg}` +
45
+ ` {gray-fg}│{/gray-fg} {gray-fg}Cycle:{/gray-fg} ${cycleDisplay}` +
46
+ ` {gray-fg}│{/gray-fg} {gray-fg}BCP/hr:{/gray-fg} {${bphColor}-fg}${bphDisplay}{/${bphColor}-fg}` +
47
+ ` {gray-fg}│{/gray-fg} {gray-fg}v{/gray-fg}{green-fg}${version}{/green-fg}` +
48
+ ` {gray-fg}branch:{/gray-fg} {green-fg}${branch}{/green-fg}`;
49
+
50
+ // ── Line 2: step info ──
51
+ const currentStep = stateData?.epicCycle?.current_step;
52
+ const activeStory = stateData?.activeStory ?? '—';
53
+ const activeEpic = stateData?.activeEpic ?? '—';
54
+ const activeFlow = stateData?.activeFlow ?? '—';
38
55
 
39
- const avgCycleTime = totalMin === '-' ? '-' : `${totalMin}m`;
40
- const bcpHrDisplay = typeof avgBcpPerHour === 'number' ? avgBcpPerHour.toFixed(2) : avgBcpPerHour;
56
+ const STEP_NAMES = ['survey-context','plan-work','kickoff-branch','develop-tdd','verify-work','audit-code','commit-message','release-branch'];
57
+ const stepName = typeof currentStep === 'number' ? (STEP_NAMES[currentStep] ?? '—') : (currentStep ?? '—');
58
+ const isActive = stepName !== '—' && activeStory !== '—';
59
+ const statusWord = isActive
60
+ ? `{yellow-fg}running{/yellow-fg}`
61
+ : `{green-fg}ready{/green-fg}`;
62
+ const storyDesc = isActive
63
+ ? `{gray-fg}${activeEpic}{/gray-fg} {gray-fg}›{/gray-fg} {cyan-fg}${activeStory}{/cyan-fg} {gray-fg}—{/gray-fg} ${stepName}`
64
+ : `{gray-fg}flow:{/gray-fg} ${activeFlow} {gray-fg}next:{/gray-fg} {cyan-fg}${stateData?.epicCycle?.next_skill ?? 'survey-context'}{/cyan-fg}`;
65
+ const stepLine =
66
+ `{gray-fg}step {/gray-fg}{cyan-fg}${doneStories}{/cyan-fg}{gray-fg}/${totalStories}{/gray-fg}` +
67
+ ` {gray-fg}—{/gray-fg} ${statusWord}` +
68
+ ` {gray-fg}—{/gray-fg} ${storyDesc}`;
41
69
 
42
- const line = `epics: ${epicCount} | stories: ${storyCount} | BCPs: ${deliveredBcps}/${targetBcps} | cycle: ${avgCycleTime} | {${bcpHrColor}}BCP/hr: ${bcpHrDisplay}{/${bcpHrColor}} | v${version}`;
70
+ // ── Line 3: stats row ──
71
+ const statsLine =
72
+ `{gray-fg}[{/gray-fg} epics {green-fg}${doneEpics}${doneEpics > 0 ? '' : ''}{/green-fg}{gray-fg}/${totalEpics} ]{/gray-fg}` +
73
+ ` {gray-fg}[{/gray-fg} stories {cyan-fg}${doneStories}{/cyan-fg}{gray-fg}/${totalStories} ]{/gray-fg}` +
74
+ ` {gray-fg}[{/gray-fg} BCPs {yellow-fg}${deliveredBcps}{/yellow-fg}{gray-fg}/${targetBcps} ]{/gray-fg}` +
75
+ ` {gray-fg}[{/gray-fg} cycle ${cycleDisplay}{gray-fg} ]{/gray-fg}` +
76
+ ` {gray-fg}[{/gray-fg} BCP/hr {${bphColor}-fg}${bphDisplay}{/${bphColor}-fg}{gray-fg} ]{/gray-fg}` +
77
+ ` {gray-fg}[{/gray-fg} v{green-fg}${version}{/green-fg}{gray-fg} ]{/gray-fg}`;
43
78
 
44
- box.setContent(line);
79
+ box.setContent([statusLine, stepLine, statsLine].join('\n'));
45
80
  }
46
81
 
47
82
  module.exports = { renderMetricsBar };
@@ -1,42 +1,38 @@
1
1
  const { STEPS } = require('../loaders/pipeline-map');
2
2
 
3
+ const STEP_ABBR = ['sur','pla','kic','tdd','ver','aud','com','rel'];
4
+
3
5
  function renderPipeline(box, stateData) {
4
6
  if (!box || typeof box.setContent !== 'function') {
5
7
  return;
6
8
  }
7
9
 
8
10
  if (!stateData) {
9
- box.setContent('{dim}state.yaml not loaded{/dim}');
11
+ box.setContent('{gray-fg}state.yaml not loaded{/gray-fg}');
10
12
  return;
11
13
  }
12
14
 
13
- const currentStep = stateData.epicCycle?.current_step || null;
14
- const completedSteps = stateData.epicCycle?.completed_steps || [];
15
-
16
- // Build horizontal pipeline strip
17
- const steps = STEPS.map((step, index) => {
18
- const isCompleted = completedSteps.includes(step);
19
- const isCurrent = step === currentStep;
20
-
21
- let display = `${index + 1} ${step}`;
22
-
23
- if (isCurrent) {
24
- display = `{reverse}${display}{/reverse}`;
25
- } else if (isCompleted) {
26
- display = `{green-fg}${display}{/green-fg}`;
15
+ // current_step is a 0-based index (number) in state.yaml
16
+ const rawStep = stateData.epicCycle?.current_step;
17
+ const currentIdx = typeof rawStep === 'number' ? rawStep : STEPS.indexOf(rawStep);
18
+
19
+ // Build pipeline strip
20
+ const strip = STEPS.map((step, i) => {
21
+ const abbr = STEP_ABBR[i];
22
+ if (i < currentIdx) {
23
+ return `{green-fg}${i + 1} ${step}{/green-fg}`;
24
+ } else if (i === currentIdx) {
25
+ return `{reverse}{cyan-fg}${i + 1} ${step}{/cyan-fg}{/reverse}`;
27
26
  } else {
28
- display = `{dim}${display}{/dim}`;
27
+ return `{gray-fg}${i + 1} ${step}{/gray-fg}`;
29
28
  }
29
+ }).join(' {gray-fg}›{/gray-fg} ');
30
30
 
31
- return display;
32
- }).join(' {bold}{/bold} ');
33
-
34
- const lines = [];
35
- lines.push(`{bold}{cyan}Pipeline{/cyan}{/bold} step ${STEPS.indexOf(currentStep) + 1} / ${STEPS.length}`);
36
- lines.push('');
37
- lines.push(steps);
31
+ const stepLabel = currentIdx >= 0 && currentIdx < STEPS.length
32
+ ? `{gray-fg}step{/gray-fg} {cyan-fg}${currentIdx + 1}{/cyan-fg}{gray-fg}/${STEPS.length}{/gray-fg} {gray-fg}—{/gray-fg} ${STEPS[currentIdx]}`
33
+ : '{gray-fg}no active step{/gray-fg}';
38
34
 
39
- box.setContent(lines.join('\n'));
35
+ box.setContent(stepLabel + '\n\n' + strip);
40
36
  }
41
37
 
42
38
  module.exports = { renderPipeline };
@@ -1,45 +1,63 @@
1
+ const STEP_NAMES = [
2
+ 'survey-context', 'plan-work', 'kickoff-branch', 'develop-tdd',
3
+ 'verify-work', 'audit-code', 'commit-message', 'release-branch'
4
+ ];
5
+ const STEP_ABBR = ['sur', 'pla', 'kic', 'tdd', 'ver', 'aud', 'com', 'rel'];
6
+
1
7
  function renderStateYaml(box, stateData) {
2
8
  if (!box || typeof box.setContent !== 'function') {
3
9
  return;
4
10
  }
5
11
 
6
12
  if (!stateData) {
7
- box.setContent('{center}state.yaml not found{/center}');
13
+ box.setContent('{center}{gray-fg}state.yaml not found{/gray-fg}{/center}');
8
14
  return;
9
15
  }
10
16
 
11
- const lines = [];
12
- lines.push('{bold}CURRENT ACTION{/bold}');
13
- lines.push('');
17
+ const rawStep = stateData.epicCycle?.current_step;
18
+ const currentIdx = typeof rawStep === 'number' ? rawStep : STEP_NAMES.indexOf(rawStep);
19
+ const stepName = currentIdx >= 0 ? STEP_NAMES[currentIdx] : (rawStep ?? '');
20
+ const nextSkill = stateData.epicCycle?.next_skill ?? stateData.handoff?.next_skill ?? '—';
14
21
 
15
- const pairs = [
16
- { key: 'active_flow', value: stateData.active_flow },
17
- { key: 'active_epic', value: stateData.active_epic },
18
- { key: 'active_story', value: stateData.active_story },
19
- { key: 'current_step', value: stateData.current_step },
20
- { key: 'next_skill', value: stateData.next_skill },
21
- { key: 'git.branch', value: stateData.git?.branch },
22
- { key: 'metrics.story_start', value: stateData.metrics?.story_start },
22
+ const fields = [
23
+ { key: 'active_flow', value: stateData.activeFlow, color: 'cyan' },
24
+ { key: 'active_epic', value: stateData.activeEpic, color: 't' },
25
+ { key: 'active_story', value: stateData.activeStory, color: 't' },
26
+ { key: 'current_step', value: stepName, color: currentIdx >= 0 ? 'yellow' : 'dim' },
27
+ { key: 'next_skill', value: nextSkill, color: 'green' },
28
+ { key: 'git.branch', value: stateData.gitBranch, color: 'green' },
29
+ { key: 'metrics.story_start', value: stateData.metrics?.story_start, color: 'dim' },
23
30
  ];
24
31
 
25
- pairs.forEach(({ key, value }) => {
26
- let displayValue = String(value || '—');
27
-
28
- // Color rules: use blessed markup, not chalk
29
- if (key === 'git.branch') {
30
- if (displayValue === 'main') {
31
- displayValue = `{green-fg}${displayValue}{/green-fg}`;
32
- } else if (displayValue.startsWith('feat/')) {
33
- displayValue = `{yellow-fg}${displayValue}{/yellow-fg}`;
34
- }
32
+ const lines = fields.map(({ key, value }) => {
33
+ const v = value ?? '—';
34
+ const isDash = v === '—';
35
+ let colored;
36
+ if (isDash) {
37
+ colored = `{gray-fg}${v}{/gray-fg}`;
38
+ } else if (fields.find(f => f.key === key)?.color === 'cyan') {
39
+ colored = `{cyan-fg}${v}{/cyan-fg}`;
40
+ } else if (fields.find(f => f.key === key)?.color === 'green') {
41
+ colored = `{green-fg}${v}{/green-fg}`;
42
+ } else if (fields.find(f => f.key === key)?.color === 'yellow') {
43
+ colored = `{yellow-fg}${v}{/yellow-fg}`;
44
+ } else if (fields.find(f => f.key === key)?.color === 'dim') {
45
+ colored = `{gray-fg}${v}{/gray-fg}`;
46
+ } else {
47
+ colored = v;
35
48
  }
49
+ return `{gray-fg}${key}:{/gray-fg} ${colored}`;
50
+ });
36
51
 
37
- if (key === 'next_skill' && value) {
38
- displayValue = `{green-fg}${displayValue}{/green-fg}`;
39
- }
52
+ // Stage tracker strip
53
+ const tracker = STEP_ABBR.map((abbr, i) => {
54
+ if (i < currentIdx) return `{green-fg}${abbr}{/green-fg}`;
55
+ if (i === currentIdx) return `{cyan-fg}{bold}${abbr}{/bold}{/cyan-fg}`;
56
+ return `{gray-fg}${abbr}{/gray-fg}`;
57
+ }).join(' {gray-fg}·{/gray-fg} ');
40
58
 
41
- lines.push(` {dim}${key}:{/dim} ${displayValue}`);
42
- });
59
+ lines.push('');
60
+ lines.push(tracker);
43
61
 
44
62
  box.setContent(lines.join('\n'));
45
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "61 agent skills for spec-driven, test-first software development by solo developers",
5
5
  "main": "index.js",
6
6
  "scripts": {