bigpowers 2.0.1 → 2.1.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.
- package/CHANGELOG.md +7 -0
- package/dashboard/src/tui/epic-queue.js +58 -19
- package/dashboard/src/tui/filesystem.js +0 -2
- package/dashboard/src/tui/index.js +22 -20
- package/dashboard/src/tui/ledger.js +23 -28
- package/dashboard/src/tui/metrics-bar.js +66 -31
- package/dashboard/src/tui/pipeline.js +19 -23
- package/dashboard/src/tui/state-yaml.js +45 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [2.1.0](https://github.com/danielvm-git/bigpowers/compare/v2.0.1...v2.1.0) (2026-06-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **dashboard/tui:** render blessed tags and improve layout ([fba0e49](https://github.com/danielvm-git/bigpowers/commit/fba0e4918a4d99c94c30e595378565263d72734d))
|
|
7
|
+
|
|
1
8
|
## [2.0.1](https://github.com/danielvm-git/bigpowers/compare/v2.0.0...v2.0.1) (2026-06-11)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -1,5 +1,3 @@
|
|
|
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;
|
|
@@ -10,27 +8,68 @@ function renderEpicQueue(box, epics, executionStatus) {
|
|
|
10
8
|
return;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 '{dim}·{/dim}';
|
|
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 '{dim}·{/dim}';
|
|
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} {dim}·{/dim} ${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
|
+
: `{dim}${bcp} BCP{/dim}`;
|
|
55
|
+
lines.push(` ${dot(story.id)} {dim}${story.id}{/dim} ${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}{dim}/${epicBcpTotal} BCP{/dim}`
|
|
62
|
+
: `{dim}0/${epicBcpTotal} BCP{/dim}`;
|
|
63
|
+
lines.push(` {dim}${epicStoriesDone}/${stories.length} done · ${bcpProgressStr}`);
|
|
64
|
+
lines.push('');
|
|
30
65
|
});
|
|
31
66
|
|
|
32
|
-
|
|
33
|
-
lines.push(
|
|
67
|
+
// Release baseline footer
|
|
68
|
+
lines.push('{dim}─────────────────────────{/dim}');
|
|
69
|
+
lines.push(`{dim}release baseline{/dim}`);
|
|
70
|
+
lines.push(`{dim}total: {/dim}{yellow-fg}${grandTotalBcps} BCPs{/yellow-fg}`);
|
|
71
|
+
lines.push(perEpicBcp.map(e => `{dim}${e.id}: ${e.bcps} BCP{/dim}`).join(' '));
|
|
72
|
+
lines.push(`{dim}target: {/dim}{bold}v2.0.0{/bold}`);
|
|
34
73
|
|
|
35
74
|
box.setContent(lines.join('\n'));
|
|
36
75
|
}
|
|
@@ -35,14 +35,14 @@ function start(projectRoot) {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
// Check terminal size
|
|
38
|
-
if (screen.width < 120 || screen.height <
|
|
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
|
|
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:
|
|
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:
|
|
78
|
+
top: 6,
|
|
80
79
|
left: 0,
|
|
81
80
|
width: '100%',
|
|
82
81
|
height: 5,
|
|
83
82
|
border: 'line',
|
|
84
83
|
tags: true,
|
|
85
|
-
|
|
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:
|
|
90
|
+
top: 11,
|
|
91
91
|
left: 0,
|
|
92
92
|
width: '33%',
|
|
93
|
-
height: screen.height -
|
|
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:
|
|
105
|
+
top: 11,
|
|
105
106
|
left: '33%',
|
|
106
107
|
width: '33%',
|
|
107
|
-
height: screen.height -
|
|
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
|
-
|
|
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:
|
|
120
|
+
top: 11,
|
|
119
121
|
left: '66%',
|
|
120
122
|
width: '34%',
|
|
121
|
-
height: screen.height -
|
|
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
|
-
|
|
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
|
-
//
|
|
153
|
-
|
|
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} {dim}v2 — seed {cyan-fg}→{/cyan-fg} epics {cyan-fg}→{/cyan-fg} mvp{/dim}');
|
|
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('{
|
|
12
|
+
box.setContent('{dim}stories complete here as they land{/dim}');
|
|
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
|
-
//
|
|
18
|
+
// Column widths
|
|
19
|
+
const W = { sid: 12, epic: 6, bcps: 5, min: 8, bhr: 7 };
|
|
20
|
+
|
|
21
|
+
// Header
|
|
16
22
|
lines.push(
|
|
17
|
-
|
|
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('{dim}' + '─'.repeat(W.sid + W.epic + W.bcps + W.min + W.bhr) + '{/dim}');
|
|
25
26
|
|
|
26
27
|
let totalBCPs = 0;
|
|
27
28
|
let totalMinutes = 0;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
cycleTimes.forEach((cycle) => {
|
|
30
|
+
cycleTimes.forEach(cycle => {
|
|
31
31
|
const storyId = cycle.id || '—';
|
|
32
|
-
const
|
|
32
|
+
const epicId = cycle.epic || '—';
|
|
33
33
|
const bcps = cycle.bcps || 0;
|
|
34
34
|
const minutes = cycle.cycleMin || 0;
|
|
35
|
-
const
|
|
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
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
`{white-fg}${pad(storyId, W.sid)}{/white-fg}` +
|
|
42
|
+
`{dim}${pad(epicId, W.epic)}{/dim}` +
|
|
43
|
+
`{yellow-fg}${pad(bcps, W.bcps)}{/yellow-fg}` +
|
|
44
|
+
`{dim}${pad(minutes, W.min)}{/dim}` +
|
|
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('{dim}' + '─'.repeat(W.sid + W.epic + W.bcps + W.min + W.bhr) + '{/dim}');
|
|
54
50
|
|
|
51
|
+
const avgBph = totalMinutes > 0 ? ((totalBCPs * 60) / totalMinutes).toFixed(1) : '—';
|
|
55
52
|
lines.push(
|
|
56
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
32
|
+
// BCP/hr color threshold
|
|
33
|
+
let bphColor = 'white';
|
|
29
34
|
if (typeof avgBcpPerHour === 'number') {
|
|
30
|
-
if (avgBcpPerHour >= 2.0)
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
`{dim}BCPs:{/dim} {yellow-fg}${deliveredBcps}{/yellow-fg}{dim}/${targetBcps}{/dim}` +
|
|
45
|
+
` {dim}│{/dim} {dim}Cycle:{/dim} ${cycleDisplay}` +
|
|
46
|
+
` {dim}│{/dim} {dim}BCP/hr:{/dim} {${bphColor}-fg}${bphDisplay}{/${bphColor}-fg}` +
|
|
47
|
+
` {dim}│{/dim} {dim}v{/dim}{green-fg}${version}{/green-fg}` +
|
|
48
|
+
`{|} {dim}branch:{/dim} {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
|
|
40
|
-
const
|
|
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
|
+
? `{dim}${activeEpic}{/dim} {dim}›{/dim} {cyan-fg}${activeStory}{/cyan-fg} {dim}—{/dim} ${stepName}`
|
|
64
|
+
: `{dim}flow:{/dim} ${activeFlow} {dim}next:{/dim} {cyan-fg}${stateData?.epicCycle?.next_skill ?? 'survey-context'}{/cyan-fg}`;
|
|
65
|
+
const stepLine =
|
|
66
|
+
`{dim}step {/dim}{cyan-fg}${doneStories}{/cyan-fg}{dim}/${totalStories}{/dim}` +
|
|
67
|
+
` {dim}—{/dim} ${statusWord}` +
|
|
68
|
+
` {dim}—{/dim} ${storyDesc}`;
|
|
41
69
|
|
|
42
|
-
|
|
70
|
+
// ── Line 3: stats row ──
|
|
71
|
+
const statsLine =
|
|
72
|
+
`{dim}[{/dim} epics {green-fg}${doneEpics}${doneEpics > 0 ? '' : ''}{/green-fg}{dim}/${totalEpics} ]{/dim}` +
|
|
73
|
+
` {dim}[{/dim} stories {cyan-fg}${doneStories}{/cyan-fg}{dim}/${totalStories} ]{/dim}` +
|
|
74
|
+
` {dim}[{/dim} BCPs {yellow-fg}${deliveredBcps}{/yellow-fg}{dim}/${targetBcps} ]{/dim}` +
|
|
75
|
+
` {dim}[{/dim} cycle ${cycleDisplay}{dim} ]{/dim}` +
|
|
76
|
+
` {dim}[{/dim} BCP/hr {${bphColor}-fg}${bphDisplay}{/${bphColor}-fg}{dim} ]{/dim}` +
|
|
77
|
+
` {dim}[{/dim} v{green-fg}${version}{/green-fg}{dim} ]{/dim}`;
|
|
43
78
|
|
|
44
|
-
box.setContent(
|
|
79
|
+
box.setContent([statusLine, stepLine, statsLine].join('\n'));
|
|
45
80
|
}
|
|
46
81
|
|
|
47
82
|
module.exports = { renderMetricsBar };
|
|
@@ -1,5 +1,7 @@
|
|
|
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;
|
|
@@ -10,33 +12,27 @@ function renderPipeline(box, stateData) {
|
|
|
10
12
|
return;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
+
return `{dim}${i + 1} ${step}{/dim}`;
|
|
29
28
|
}
|
|
29
|
+
}).join(' {dim}›{/dim} ');
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
? `{dim}step{/dim} {cyan-fg}${currentIdx + 1}{/cyan-fg}{dim}/${STEPS.length}{/dim} {dim}—{/dim} ${STEPS[currentIdx]}`
|
|
33
|
+
: '{dim}no active step{/dim}';
|
|
38
34
|
|
|
39
|
-
box.setContent(
|
|
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}{dim}state.yaml not found{/dim}{/center}');
|
|
8
14
|
return;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
16
|
-
{ key: 'active_flow',
|
|
17
|
-
{ key: 'active_epic',
|
|
18
|
-
{ key: 'active_story',
|
|
19
|
-
{ key: 'current_step',
|
|
20
|
-
{ key: 'next_skill',
|
|
21
|
-
{ key: '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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
32
|
+
const lines = fields.map(({ key, value }) => {
|
|
33
|
+
const v = value ?? '—';
|
|
34
|
+
const isDash = v === '—';
|
|
35
|
+
let colored;
|
|
36
|
+
if (isDash) {
|
|
37
|
+
colored = `{dim}${v}{/dim}`;
|
|
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 = `{dim}${v}{/dim}`;
|
|
46
|
+
} else {
|
|
47
|
+
colored = v;
|
|
35
48
|
}
|
|
49
|
+
return `{dim}${key}:{/dim} ${colored}`;
|
|
50
|
+
});
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
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 `{dim}${abbr}{/dim}`;
|
|
57
|
+
}).join(' {dim}·{/dim} ');
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push(tracker);
|
|
43
61
|
|
|
44
62
|
box.setContent(lines.join('\n'));
|
|
45
63
|
}
|