bigpowers 1.3.2 → 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.
- package/CHANGELOG.md +7 -0
- package/SKILL-INDEX.md +9 -9
- package/audit-code/SKILL.md +12 -0
- package/build-epic/SKILL.md +7 -4
- package/commit-message/SKILL.md +12 -0
- package/compose-workflow/SKILL.md +2 -0
- package/dashboard/bin/dashboard.js +93 -0
- package/dashboard/package-lock.json +1035 -0
- package/dashboard/package.json +18 -0
- package/dashboard/src/data/gate-status.js +32 -0
- package/dashboard/src/data/metrics.js +89 -0
- package/dashboard/src/data/pipeline-map.js +32 -0
- package/dashboard/src/data/reader.js +122 -0
- package/dashboard/src/data/watcher.js +108 -0
- package/dashboard/src/loaders/gate-status.js +32 -0
- package/dashboard/src/loaders/metrics.js +89 -0
- package/dashboard/src/loaders/pipeline-map.js +32 -0
- package/dashboard/src/loaders/reader.js +122 -0
- package/dashboard/src/loaders/watcher.js +108 -0
- package/dashboard/src/tui/epic-queue.js +36 -0
- package/dashboard/src/tui/filesystem.js +95 -0
- package/dashboard/src/tui/index.js +161 -0
- package/dashboard/src/tui/ledger.js +66 -0
- package/dashboard/src/tui/metrics-bar.js +30 -0
- package/dashboard/src/tui/pipeline.js +46 -0
- package/dashboard/src/tui/state-yaml.js +49 -0
- package/dashboard/src/web/client.html +477 -0
- package/dashboard/src/web/server.js +112 -0
- package/dashboard/test/fixtures/state.yaml +2 -0
- package/deepen-architecture/SKILL.md +2 -0
- package/define-language/SKILL.md +2 -0
- package/define-success/SKILL.md +4 -0
- package/delegate-task/SKILL.md +2 -0
- package/design-interface/SKILL.md +2 -0
- package/develop-tdd/SKILL.md +32 -0
- package/dispatch-agents/SKILL.md +2 -0
- package/edit-document/SKILL.md +6 -0
- package/elaborate-spec/SKILL.md +2 -0
- package/enforce-first/SKILL.md +2 -0
- package/grill-me/SKILL.md +2 -0
- package/guard-git/SKILL.md +2 -0
- package/hook-commits/SKILL.md +2 -0
- package/inspect-quality/SKILL.md +2 -0
- package/kickoff-branch/SKILL.md +5 -0
- package/map-codebase/SKILL.md +2 -0
- package/model-domain/SKILL.md +4 -0
- package/orchestrate-project/REFERENCE.md +1 -1
- package/orchestrate-project/SKILL.md +3 -1
- package/organize-workspace/SKILL.md +2 -0
- package/package.json +4 -2
- package/plan-refactor/SKILL.md +2 -0
- package/plan-work/SKILL.md +44 -0
- package/release-branch/SKILL.md +28 -0
- package/respond-review/SKILL.md +2 -0
- package/run-planning/SKILL.md +2 -0
- package/search-skills/SKILL.md +2 -0
- package/seed-conventions/SKILL.md +2 -0
- package/session-state/SKILL.md +16 -0
- package/setup-environment/SKILL.md +2 -0
- package/simulate-agents/SKILL.md +2 -0
- package/spike-prototype/SKILL.md +2 -0
- package/stocktake-skills/SKILL.md +2 -0
- package/survey-context/SKILL.md +23 -0
- package/terse-mode/SKILL.md +2 -0
- package/using-bigpowers/SKILL.md +4 -0
- package/validate-fix/SKILL.md +2 -0
- package/verify-work/SKILL.md +33 -0
- package/visual-dashboard/SKILL.md +2 -0
- package/wire-observability/SKILL.md +2 -0
|
@@ -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,36 @@
|
|
|
1
|
+
const { gateIcon, gateColor } = require('../loaders/gate-status');
|
|
2
|
+
|
|
3
|
+
function renderEpicQueue(box, epics, executionStatus) {
|
|
4
|
+
if (!box || typeof box.setContent !== 'function') {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!epics || epics.length === 0) {
|
|
9
|
+
box.setContent('{dim}no epic shards found{/dim}');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let lines = [];
|
|
14
|
+
let totalBcps = 0;
|
|
15
|
+
|
|
16
|
+
epics.forEach(epic => {
|
|
17
|
+
const epicStatusIcon = gateIcon[epic.status] || '?';
|
|
18
|
+
lines.push(`${epicStatusIcon} ${epic.id} ${epic.title}`);
|
|
19
|
+
|
|
20
|
+
if (epic.stories && epic.stories.length > 0) {
|
|
21
|
+
epic.stories.forEach(story => {
|
|
22
|
+
const storyStatusIcon = gateIcon[story.status] || '?';
|
|
23
|
+
const bcpCount = story.bcps || 0;
|
|
24
|
+
totalBcps += bcpCount;
|
|
25
|
+
lines.push(` ${storyStatusIcon} ${story.id} ${story.title} (${bcpCount} BCPs)`);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(`Total: ${totalBcps} BCPs`);
|
|
32
|
+
|
|
33
|
+
box.setContent(lines.join('\n'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { renderEpicQueue };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
function renderFilesystem(box, projectRoot) {
|
|
6
|
+
if (!box || typeof box.setContent !== 'function') {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const specsPath = path.join(projectRoot, 'specs');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(specsPath)) {
|
|
13
|
+
box.setContent(chalk.dim('{center}specs/ not found{/center}'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const lines = [];
|
|
19
|
+
lines.push('{bold}{cyan}Filesystem{/cyan}{/bold}');
|
|
20
|
+
|
|
21
|
+
let fileCount = 0;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const entries = fs.readdirSync(specsPath, { withFileTypes: true });
|
|
25
|
+
|
|
26
|
+
// Count files for badge
|
|
27
|
+
function countFiles(dir, depth = 0) {
|
|
28
|
+
if (depth > 1) return 0;
|
|
29
|
+
let count = 0;
|
|
30
|
+
try {
|
|
31
|
+
const dirEntries = fs.readdirSync(dir, { withFileTypes: true });
|
|
32
|
+
dirEntries.forEach((entry) => {
|
|
33
|
+
if (entry.isFile()) count++;
|
|
34
|
+
else if (entry.isDirectory() && depth < 1) {
|
|
35
|
+
count += countFiles(path.join(dir, entry.name), depth + 1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
return count;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fileCount = countFiles(specsPath);
|
|
45
|
+
lines.push(`{dim}[${fileCount} files]{/dim}`);
|
|
46
|
+
lines.push('');
|
|
47
|
+
|
|
48
|
+
// Build tree
|
|
49
|
+
function buildTree(dir, prefix = '', depth = 0) {
|
|
50
|
+
if (depth > 1) return;
|
|
51
|
+
try {
|
|
52
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
entries.forEach((entry, index) => {
|
|
54
|
+
const isLast = index === entries.length - 1;
|
|
55
|
+
const connector = isLast ? '└─ ' : '├─ ';
|
|
56
|
+
const fullPath = path.join(dir, entry.name);
|
|
57
|
+
|
|
58
|
+
let displayName = entry.name;
|
|
59
|
+
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
displayName = chalk.blue(displayName);
|
|
62
|
+
lines.push(prefix + connector + displayName);
|
|
63
|
+
|
|
64
|
+
if (depth < 1) {
|
|
65
|
+
const childPrefix =
|
|
66
|
+
prefix + (isLast ? ' ' : '│ ');
|
|
67
|
+
buildTree(fullPath, childPrefix, depth + 1);
|
|
68
|
+
}
|
|
69
|
+
} else if (entry.isFile()) {
|
|
70
|
+
// Check if modified in last 60 seconds
|
|
71
|
+
const stat = fs.statSync(fullPath);
|
|
72
|
+
const mtime = stat.mtimeMs;
|
|
73
|
+
const age = now - mtime;
|
|
74
|
+
|
|
75
|
+
if (age < 60000) {
|
|
76
|
+
displayName = chalk.yellow(displayName + ' ★');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lines.push(prefix + connector + displayName);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
buildTree(specsPath);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
lines.push(chalk.dim('Error reading specs/'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
box.setContent(lines.join('\n'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { renderFilesystem };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { watch } = require('../loaders/watcher');
|
|
3
|
+
const { readStateYaml, readExecutionStatus, readEpicShards, readCycleTimes } = require('../loaders/reader');
|
|
4
|
+
const { getMetrics } = require('../loaders/metrics');
|
|
5
|
+
const { renderPipeline } = require('./pipeline');
|
|
6
|
+
const { renderEpicQueue } = require('./epic-queue');
|
|
7
|
+
const { renderMetricsBar } = require('./metrics-bar');
|
|
8
|
+
const { renderStateYaml } = require('./state-yaml');
|
|
9
|
+
const { renderFilesystem } = require('./filesystem');
|
|
10
|
+
const { renderLedger } = require('./ledger');
|
|
11
|
+
|
|
12
|
+
let blessed;
|
|
13
|
+
try {
|
|
14
|
+
blessed = require('blessed');
|
|
15
|
+
} catch (err) {
|
|
16
|
+
blessed = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function start(projectRoot) {
|
|
20
|
+
if (!blessed) {
|
|
21
|
+
console.error('Error: blessed module is not installed. Please run: npm install blessed');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Create screen
|
|
26
|
+
const screen = blessed.screen({
|
|
27
|
+
smartCSR: true,
|
|
28
|
+
mouse: true,
|
|
29
|
+
title: 'BigPowers Dashboard',
|
|
30
|
+
256: true,
|
|
31
|
+
fullUnicode: true
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Check terminal size
|
|
35
|
+
if (screen.width < 120 || screen.height < 30) {
|
|
36
|
+
const msg = blessed.box({
|
|
37
|
+
parent: screen,
|
|
38
|
+
top: 'center',
|
|
39
|
+
left: 'center',
|
|
40
|
+
width: 60,
|
|
41
|
+
height: 10,
|
|
42
|
+
content: '{center}Terminal must be at least 120x30{/center}\n{center}Please resize and try again{/center}',
|
|
43
|
+
border: 'line',
|
|
44
|
+
style: { border: { fg: 'yellow' } }
|
|
45
|
+
});
|
|
46
|
+
screen.render();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create layout zones
|
|
51
|
+
const metricsBar = blessed.box({
|
|
52
|
+
parent: screen,
|
|
53
|
+
top: 0,
|
|
54
|
+
left: 0,
|
|
55
|
+
width: '100%',
|
|
56
|
+
height: 3,
|
|
57
|
+
border: 'line',
|
|
58
|
+
style: { border: { fg: 'blue' } }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const pipeline = blessed.box({
|
|
62
|
+
parent: screen,
|
|
63
|
+
top: 3,
|
|
64
|
+
left: 0,
|
|
65
|
+
width: '100%',
|
|
66
|
+
height: 5,
|
|
67
|
+
border: 'line',
|
|
68
|
+
style: { border: { fg: 'cyan' } }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const epicQueue = blessed.box({
|
|
72
|
+
parent: screen,
|
|
73
|
+
top: 8,
|
|
74
|
+
left: 0,
|
|
75
|
+
width: '33%',
|
|
76
|
+
height: screen.height - 20,
|
|
77
|
+
border: 'line',
|
|
78
|
+
scrollable: true,
|
|
79
|
+
mouse: true,
|
|
80
|
+
keys: true,
|
|
81
|
+
style: { border: { fg: 'green' } }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const actionLog = blessed.box({
|
|
85
|
+
parent: screen,
|
|
86
|
+
top: 8,
|
|
87
|
+
left: '33%',
|
|
88
|
+
width: '33%',
|
|
89
|
+
height: screen.height - 20,
|
|
90
|
+
border: 'line',
|
|
91
|
+
scrollable: true,
|
|
92
|
+
mouse: true,
|
|
93
|
+
keys: true,
|
|
94
|
+
style: { border: { fg: 'yellow' } }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const fsPanel = blessed.box({
|
|
98
|
+
parent: screen,
|
|
99
|
+
top: 8,
|
|
100
|
+
left: '66%',
|
|
101
|
+
width: '34%',
|
|
102
|
+
height: screen.height - 20,
|
|
103
|
+
border: 'line',
|
|
104
|
+
scrollable: true,
|
|
105
|
+
mouse: true,
|
|
106
|
+
keys: true,
|
|
107
|
+
style: { border: { fg: 'magenta' } }
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const ledger = blessed.box({
|
|
111
|
+
parent: screen,
|
|
112
|
+
top: screen.height - 12,
|
|
113
|
+
left: 0,
|
|
114
|
+
width: '100%',
|
|
115
|
+
height: 12,
|
|
116
|
+
border: 'line',
|
|
117
|
+
scrollable: true,
|
|
118
|
+
mouse: true,
|
|
119
|
+
keys: true,
|
|
120
|
+
style: { border: { fg: 'white' } }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Refresh function
|
|
124
|
+
function refresh() {
|
|
125
|
+
const stateData = readStateYaml(projectRoot);
|
|
126
|
+
const executionStatus = readExecutionStatus(projectRoot);
|
|
127
|
+
const epics = readEpicShards(projectRoot);
|
|
128
|
+
const cycleTimes = readCycleTimes(projectRoot);
|
|
129
|
+
const metrics = getMetrics(cycleTimes);
|
|
130
|
+
|
|
131
|
+
renderMetricsBar(metricsBar, metrics, stateData);
|
|
132
|
+
renderPipeline(pipeline, stateData);
|
|
133
|
+
renderEpicQueue(epicQueue, epics, executionStatus);
|
|
134
|
+
renderStateYaml(actionLog, stateData);
|
|
135
|
+
renderFilesystem(fsPanel, projectRoot);
|
|
136
|
+
renderLedger(ledger, cycleTimes);
|
|
137
|
+
|
|
138
|
+
screen.render();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Set up file watcher
|
|
142
|
+
const watcher = watch(projectRoot);
|
|
143
|
+
watcher.on('change', () => {
|
|
144
|
+
refresh();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Initial refresh
|
|
148
|
+
refresh();
|
|
149
|
+
|
|
150
|
+
// Key bindings
|
|
151
|
+
screen.key(['q', 'C-c'], () => {
|
|
152
|
+
watcher.close();
|
|
153
|
+
process.exit(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
screen.key(['r'], () => {
|
|
157
|
+
refresh();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { start };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
function renderLedger(box, cycleTimes) {
|
|
4
|
+
if (!box || typeof box.setContent !== 'function') {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!cycleTimes || cycleTimes.length === 0) {
|
|
9
|
+
box.setContent(chalk.dim('{center}no completed stories yet{/center}'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push('{bold}{cyan}Ledger{/cyan}{/bold}');
|
|
15
|
+
lines.push('');
|
|
16
|
+
|
|
17
|
+
// Header row
|
|
18
|
+
lines.push(
|
|
19
|
+
'{bold}' +
|
|
20
|
+
'{cyan}Story ID{/cyan} ' +
|
|
21
|
+
'{cyan}Epic{/cyan} ' +
|
|
22
|
+
'{cyan}BCPs{/cyan} ' +
|
|
23
|
+
'{cyan}Minutes{/cyan} ' +
|
|
24
|
+
'{cyan}BCP/hr{/cyan}' +
|
|
25
|
+
'{/bold}'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
let totalBCPs = 0;
|
|
29
|
+
let totalMinutes = 0;
|
|
30
|
+
|
|
31
|
+
// Data rows
|
|
32
|
+
cycleTimes.forEach((cycle) => {
|
|
33
|
+
const storyId = cycle.story_id || '—';
|
|
34
|
+
const epicPrefix = cycle.epic || '—';
|
|
35
|
+
const bcps = cycle.bcps || 0;
|
|
36
|
+
const minutes = cycle.cycle_minutes || 0;
|
|
37
|
+
const bcpPerHour = minutes > 0 ? ((bcps * 60) / minutes).toFixed(1) : '—';
|
|
38
|
+
|
|
39
|
+
totalBCPs += bcps;
|
|
40
|
+
totalMinutes += minutes;
|
|
41
|
+
|
|
42
|
+
lines.push(
|
|
43
|
+
` ${storyId} ` +
|
|
44
|
+
`${epicPrefix} ` +
|
|
45
|
+
`${bcps} ` +
|
|
46
|
+
`${minutes} ` +
|
|
47
|
+
`${bcpPerHour}`
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
lines.push('');
|
|
52
|
+
|
|
53
|
+
// Totals row
|
|
54
|
+
const avgBcpPerHour =
|
|
55
|
+
totalMinutes > 0 ? ((totalBCPs * 60) / totalMinutes).toFixed(1) : '—';
|
|
56
|
+
|
|
57
|
+
lines.push(
|
|
58
|
+
'{bold}{yellow}' +
|
|
59
|
+
`TOTAL — ${totalBCPs} ${totalMinutes} ${avgBcpPerHour}` +
|
|
60
|
+
'{/yellow}{/bold}'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
box.setContent(lines.join('\n'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { renderLedger };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function renderMetricsBar(box, projectMetrics, stateData) {
|
|
2
|
+
if (!box || typeof box.setContent !== 'function') {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const totalBcps = projectMetrics?.totalBcps ?? '-';
|
|
7
|
+
const totalMin = projectMetrics?.totalMin ?? '-';
|
|
8
|
+
const avgBcpPerHour = projectMetrics?.avgBcpPerHour ?? '-';
|
|
9
|
+
const version = stateData?.release?.target_version ?? '-';
|
|
10
|
+
|
|
11
|
+
let bcpHrColor = 'white';
|
|
12
|
+
if (typeof avgBcpPerHour === 'number') {
|
|
13
|
+
if (avgBcpPerHour >= 2.0) {
|
|
14
|
+
bcpHrColor = 'green';
|
|
15
|
+
} else if (avgBcpPerHour >= 1.0) {
|
|
16
|
+
bcpHrColor = 'yellow';
|
|
17
|
+
} else {
|
|
18
|
+
bcpHrColor = 'red';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const avgCycleTime = totalMin === '-' ? '-' : `${totalMin}m`;
|
|
23
|
+
const bcpHrDisplay = typeof avgBcpPerHour === 'number' ? avgBcpPerHour.toFixed(2) : avgBcpPerHour;
|
|
24
|
+
|
|
25
|
+
const line = `BCPs: ${totalBcps} | Cycle: ${avgCycleTime} | {${bcpHrColor}}BCP/hr: ${bcpHrDisplay}{/${bcpHrColor}} | v${version}`;
|
|
26
|
+
|
|
27
|
+
box.setContent(line);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { renderMetricsBar };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { STEPS } = require('../loaders/pipeline-map');
|
|
2
|
+
|
|
3
|
+
function renderPipeline(box, stateData) {
|
|
4
|
+
if (!box || typeof box.setContent !== 'function') {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!stateData) {
|
|
9
|
+
box.setContent('{dim}state.yaml not loaded{/dim}');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const currentStep = stateData.epicCycle?.current_step || null;
|
|
14
|
+
const completedSteps = stateData.epicCycle?.completed_steps || [];
|
|
15
|
+
|
|
16
|
+
// Build pipeline visualization
|
|
17
|
+
const lines = [];
|
|
18
|
+
lines.push('{bold}{cyan}Pipeline{/cyan}{/bold}');
|
|
19
|
+
lines.push('');
|
|
20
|
+
|
|
21
|
+
// Create step display
|
|
22
|
+
const stepDisplay = STEPS.map((step, index) => {
|
|
23
|
+
const isCompleted = completedSteps.includes(step);
|
|
24
|
+
const isCurrent = step === currentStep;
|
|
25
|
+
|
|
26
|
+
let display = ` ${index + 1}. ${step}`;
|
|
27
|
+
|
|
28
|
+
if (isCurrent) {
|
|
29
|
+
display = `{reverse}{bold}${display}{/bold}{/reverse}`;
|
|
30
|
+
} else if (isCompleted) {
|
|
31
|
+
display = `{green-fg}${display}{/green-fg}`;
|
|
32
|
+
} else {
|
|
33
|
+
display = `{gray-fg}${display}{/gray-fg}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return display;
|
|
37
|
+
}).join('\n');
|
|
38
|
+
|
|
39
|
+
lines.push(stepDisplay);
|
|
40
|
+
lines.push('');
|
|
41
|
+
lines.push(`{dim}Current: ${currentStep || '—'}{/dim}`);
|
|
42
|
+
|
|
43
|
+
box.setContent(lines.join('\n'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { renderPipeline };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
function renderStateYaml(box, stateData) {
|
|
4
|
+
if (!box || typeof box.setContent !== 'function') {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!stateData) {
|
|
9
|
+
box.setContent(chalk.dim('{center}state.yaml not found{/center}'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push('{bold}{cyan}state.yaml{/cyan}{/bold}');
|
|
15
|
+
lines.push('');
|
|
16
|
+
|
|
17
|
+
const pairs = [
|
|
18
|
+
{ key: 'active_flow', value: stateData.active_flow },
|
|
19
|
+
{ key: 'active_epic', value: stateData.active_epic },
|
|
20
|
+
{ key: 'active_story', value: stateData.active_story },
|
|
21
|
+
{ key: 'current_step', value: stateData.current_step },
|
|
22
|
+
{ key: 'next_skill', value: stateData.next_skill },
|
|
23
|
+
{ key: 'git.branch', value: stateData.git?.branch },
|
|
24
|
+
{ key: 'metrics.story_start', value: stateData.metrics?.story_start },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
pairs.forEach(({ key, value }) => {
|
|
28
|
+
let displayValue = String(value || '—');
|
|
29
|
+
|
|
30
|
+
// Color rules
|
|
31
|
+
if (key === 'git.branch') {
|
|
32
|
+
if (displayValue === 'main') {
|
|
33
|
+
displayValue = chalk.green(displayValue);
|
|
34
|
+
} else if (displayValue.startsWith('feat/')) {
|
|
35
|
+
displayValue = chalk.yellow(displayValue);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (key === 'next_skill' && value) {
|
|
40
|
+
displayValue = chalk.green(displayValue);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
lines.push(` {dim}${key}:{/dim} ${displayValue}`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
box.setContent(lines.join('\n'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { renderStateYaml };
|