@lumenflow/cli 1.0.0 → 1.3.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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file flow-report.test.ts
3
+ * @description Tests for flow-report CLI command (WU-1018)
4
+ *
5
+ * These are smoke tests to verify the CLI module can be imported.
6
+ * The CLI commands are wrappers around @lumenflow/metrics library functions
7
+ * which have their own comprehensive tests.
8
+ */
9
+ import { describe, it, expect } from 'vitest';
10
+ import { existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
14
+ describe('flow-report CLI', () => {
15
+ it('should have the CLI source file', () => {
16
+ const srcPath = join(__dirname, '../flow-report.ts');
17
+ expect(existsSync(srcPath)).toBe(true);
18
+ });
19
+ it('should be buildable (dist file exists after build)', () => {
20
+ // This test verifies that tsc compiled the file successfully
21
+ const distPath = join(__dirname, '../../dist/flow-report.js');
22
+ expect(existsSync(distPath)).toBe(true);
23
+ });
24
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file metrics-snapshot.test.ts
3
+ * @description Tests for metrics-snapshot CLI command (WU-1020)
4
+ *
5
+ * These are smoke tests to verify the CLI module can be imported and
6
+ * that the TypeScript compilation succeeds (verifying the readonly array fix).
7
+ */
8
+ import { describe, it, expect } from 'vitest';
9
+ import { existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
13
+ describe('metrics-snapshot CLI', () => {
14
+ it('should have the CLI source file', () => {
15
+ const srcPath = join(__dirname, '../metrics-snapshot.ts');
16
+ expect(existsSync(srcPath)).toBe(true);
17
+ });
18
+ it('should be buildable (dist file exists after build)', () => {
19
+ // This test verifies that tsc compiled the file successfully
20
+ // WU-1020: The readonly array cast fix allows this file to compile
21
+ const distPath = join(__dirname, '../../dist/metrics-snapshot.js');
22
+ expect(existsSync(distPath)).toBe(true);
23
+ });
24
+ });
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Issues Query CLI (WU-1018)
4
+ *
5
+ * Query and display logged agent incidents/issues.
6
+ *
7
+ * Usage:
8
+ * pnpm agent:issues-query summary # Summary of last 7 days
9
+ * pnpm agent:issues-query summary --since 30 # Summary of last 30 days
10
+ * pnpm agent:issues-query summary --category tooling
11
+ * pnpm agent:issues-query summary --severity blocker
12
+ *
13
+ * @module agent-issues-query
14
+ * @see {@link @lumenflow/agent}
15
+ */
16
+ import { readFile } from 'node:fs/promises';
17
+ import { existsSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { Command } from 'commander';
20
+ import chalk from 'chalk';
21
+ import { die } from '@lumenflow/core/dist/error-handler.js';
22
+ /** Log prefix for console output */
23
+ const LOG_PREFIX = '[agent:issues-query]';
24
+ /** Default days to query */
25
+ const DEFAULT_SINCE_DAYS = 7;
26
+ /** Issues log file path */
27
+ const ISSUES_LOG_PATH = '.beacon/agent-issues.ndjson';
28
+ /** Valid severity levels */
29
+ const SEVERITY_LEVELS = ['blocker', 'major', 'minor', 'trivial'];
30
+ /**
31
+ * Parse command line arguments
32
+ */
33
+ function parseArgs() {
34
+ const program = new Command()
35
+ .name('agent-issues-query')
36
+ .description('Query and display logged agent incidents')
37
+ .command('summary', { isDefault: true })
38
+ .description('Show summary of logged issues')
39
+ .option('--since <days>', `Days to include (default: ${DEFAULT_SINCE_DAYS})`, String(DEFAULT_SINCE_DAYS))
40
+ .option('--category <category>', 'Filter by category')
41
+ .option('--severity <severity>', `Filter by severity: ${SEVERITY_LEVELS.join(', ')}`)
42
+ .exitOverride();
43
+ try {
44
+ program.parse(process.argv);
45
+ const opts = program.opts();
46
+ return {
47
+ since: parseInt(opts.since, 10),
48
+ category: opts.category,
49
+ severity: opts.severity,
50
+ };
51
+ }
52
+ catch (err) {
53
+ const error = err;
54
+ if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') {
55
+ process.exit(0);
56
+ }
57
+ throw err;
58
+ }
59
+ }
60
+ /**
61
+ * Read issues from NDJSON log file
62
+ */
63
+ async function readIssues(baseDir, sinceDate, category, severity) {
64
+ const logPath = join(baseDir, ISSUES_LOG_PATH);
65
+ if (!existsSync(logPath)) {
66
+ return [];
67
+ }
68
+ const content = await readFile(logPath, { encoding: 'utf-8' });
69
+ const lines = content.split('\n').filter((line) => line.trim());
70
+ const issues = [];
71
+ for (const line of lines) {
72
+ try {
73
+ const raw = JSON.parse(line);
74
+ // Validate required fields
75
+ if (typeof raw.timestamp !== 'string' ||
76
+ typeof raw.category !== 'string' ||
77
+ typeof raw.severity !== 'string' ||
78
+ typeof raw.title !== 'string') {
79
+ continue;
80
+ }
81
+ const issue = {
82
+ timestamp: raw.timestamp,
83
+ wuId: raw.wu_id,
84
+ sessionId: raw.session_id,
85
+ category: raw.category,
86
+ severity: raw.severity,
87
+ title: raw.title,
88
+ description: raw.description,
89
+ stackTrace: raw.stack_trace,
90
+ metadata: raw.metadata,
91
+ };
92
+ // Filter by date
93
+ const issueDate = new Date(issue.timestamp);
94
+ if (issueDate < sinceDate) {
95
+ continue;
96
+ }
97
+ // Filter by category
98
+ if (category && issue.category.toLowerCase() !== category.toLowerCase()) {
99
+ continue;
100
+ }
101
+ // Filter by severity
102
+ if (severity && issue.severity.toLowerCase() !== severity.toLowerCase()) {
103
+ continue;
104
+ }
105
+ issues.push(issue);
106
+ }
107
+ catch {
108
+ // Skip invalid JSON lines
109
+ }
110
+ }
111
+ return issues;
112
+ }
113
+ /**
114
+ * Group issues by a key
115
+ */
116
+ function groupBy(items, key) {
117
+ const groups = new Map();
118
+ for (const item of items) {
119
+ const groupKey = item[key];
120
+ if (!groups.has(groupKey)) {
121
+ groups.set(groupKey, []);
122
+ }
123
+ groups.get(groupKey).push(item);
124
+ }
125
+ return groups;
126
+ }
127
+ /**
128
+ * Get severity color function
129
+ */
130
+ function getSeverityColor(severity) {
131
+ switch (severity) {
132
+ case 'blocker':
133
+ return chalk.red.bold;
134
+ case 'major':
135
+ return chalk.yellow;
136
+ case 'minor':
137
+ return chalk.blue;
138
+ case 'trivial':
139
+ return chalk.gray;
140
+ default:
141
+ return chalk.white;
142
+ }
143
+ }
144
+ /**
145
+ * Format severity badge
146
+ */
147
+ function formatSeverityBadge(severity) {
148
+ const color = getSeverityColor(severity);
149
+ return color(`[${severity.toUpperCase()}]`);
150
+ }
151
+ /**
152
+ * Display summary of issues
153
+ */
154
+ function displaySummary(issues, sinceDays) {
155
+ console.log('');
156
+ console.log('═══════════════════════════════════════════════════════════════');
157
+ console.log(` AGENT ISSUES SUMMARY (last ${sinceDays} days)`);
158
+ console.log('═══════════════════════════════════════════════════════════════');
159
+ console.log('');
160
+ if (issues.length === 0) {
161
+ console.log(' No issues found in the specified time range.');
162
+ console.log('');
163
+ return;
164
+ }
165
+ console.log(` Total issues: ${issues.length}`);
166
+ console.log('');
167
+ // Group by severity
168
+ const bySeverity = groupBy(issues, 'severity');
169
+ console.log(' By Severity:');
170
+ for (const severity of SEVERITY_LEVELS) {
171
+ const count = bySeverity.get(severity)?.length ?? 0;
172
+ if (count > 0) {
173
+ const color = getSeverityColor(severity);
174
+ console.log(` ${color(severity.toUpperCase().padEnd(10))} ${count}`);
175
+ }
176
+ }
177
+ console.log('');
178
+ // Group by category
179
+ const byCategory = groupBy(issues, 'category');
180
+ console.log(' By Category:');
181
+ for (const [category, categoryIssues] of byCategory.entries()) {
182
+ const severityCounts = SEVERITY_LEVELS.map((s) => {
183
+ const count = categoryIssues.filter((i) => i.severity === s).length;
184
+ return count > 0 ? `${s[0].toUpperCase()}:${count}` : '';
185
+ })
186
+ .filter(Boolean)
187
+ .join(' ');
188
+ console.log(` ${String(category).padEnd(20)} ${categoryIssues.length} issues (${severityCounts})`);
189
+ }
190
+ console.log('');
191
+ // Top 5 most common issues
192
+ const issueCount = new Map();
193
+ for (const issue of issues) {
194
+ const key = `${issue.category}:${issue.title}`;
195
+ issueCount.set(key, (issueCount.get(key) ?? 0) + 1);
196
+ }
197
+ const topIssues = Array.from(issueCount.entries())
198
+ .sort((a, b) => b[1] - a[1])
199
+ .slice(0, 5);
200
+ if (topIssues.length > 0) {
201
+ console.log(' Top 5 Most Common:');
202
+ for (const [key, count] of topIssues) {
203
+ const [category, title] = key.split(':');
204
+ const truncatedTitle = title.length > 40 ? title.slice(0, 37) + '...' : title;
205
+ console.log(` ${count}x [${category}] ${truncatedTitle}`);
206
+ }
207
+ console.log('');
208
+ }
209
+ // Recent issues (last 5)
210
+ const recentIssues = [...issues]
211
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
212
+ .slice(0, 5);
213
+ console.log(' Recent Issues:');
214
+ for (const issue of recentIssues) {
215
+ const date = new Date(issue.timestamp).toISOString().split('T')[0];
216
+ const badge = formatSeverityBadge(issue.severity);
217
+ const wuInfo = issue.wuId ? ` (${issue.wuId})` : '';
218
+ const truncatedTitle = issue.title.length > 40 ? issue.title.slice(0, 37) + '...' : issue.title;
219
+ console.log(` ${date} ${badge} ${truncatedTitle}${wuInfo}`);
220
+ }
221
+ console.log('');
222
+ }
223
+ /**
224
+ * Main function
225
+ */
226
+ async function main() {
227
+ const opts = parseArgs();
228
+ const baseDir = process.cwd();
229
+ const sinceDate = new Date();
230
+ sinceDate.setDate(sinceDate.getDate() - opts.since);
231
+ sinceDate.setHours(0, 0, 0, 0);
232
+ console.log(`${LOG_PREFIX} Querying issues since ${sinceDate.toISOString().split('T')[0]}`);
233
+ if (opts.category) {
234
+ console.log(`${LOG_PREFIX} Category filter: ${opts.category}`);
235
+ }
236
+ if (opts.severity) {
237
+ if (!SEVERITY_LEVELS.includes(opts.severity)) {
238
+ die(`Invalid severity: ${opts.severity}\n\nValid values: ${SEVERITY_LEVELS.join(', ')}`);
239
+ }
240
+ console.log(`${LOG_PREFIX} Severity filter: ${opts.severity}`);
241
+ }
242
+ const issues = await readIssues(baseDir, sinceDate, opts.category, opts.severity);
243
+ displaySummary(issues, opts.since);
244
+ }
245
+ // Guard main() for testability (WU-1366)
246
+ import { fileURLToPath } from 'node:url';
247
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
248
+ main().catch((err) => {
249
+ die(`Issues query failed: ${err.message}`);
250
+ });
251
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Log Issue CLI
4
+ *
5
+ * Logs a workflow issue or incident during agent execution.
6
+ *
7
+ * Usage:
8
+ * pnpm agent:log-issue --category workflow --severity minor --title "..." --description "..."
9
+ */
10
+ import { Command } from 'commander';
11
+ import { logIncident, getCurrentSession } from '@lumenflow/agent';
12
+ import { EXIT_CODES, INCIDENT_SEVERITY } from '@lumenflow/core/dist/wu-constants.js';
13
+ import chalk from 'chalk';
14
+ const program = new Command()
15
+ .name('agent:log-issue')
16
+ .description('Log a workflow issue or incident')
17
+ .requiredOption('--category <cat>', 'Issue category (workflow|tooling|confusion|violation|error)')
18
+ .requiredOption('--severity <sev>', 'Severity level (blocker|major|minor|info)')
19
+ .requiredOption('--title <title>', 'Short description (5-100 chars)')
20
+ .requiredOption('--description <desc>', 'Detailed context (10-2000 chars)')
21
+ .option('--resolution <res>', 'How the issue was resolved')
22
+ .option('--tags <tags>', 'Comma-separated tags (e.g., worktree,gates)')
23
+ .option('--step <step>', 'Current workflow step (e.g., wu:done, gates)')
24
+ .option('--files <files>', 'Comma-separated related files')
25
+ .action(async (opts) => {
26
+ try {
27
+ const session = await getCurrentSession();
28
+ if (!session) {
29
+ console.error(chalk.red('Error: No active session'));
30
+ console.error('Run: pnpm agent:session --wu WU-XXX --tier N');
31
+ process.exit(EXIT_CODES.ERROR);
32
+ }
33
+ const incident = {
34
+ category: opts.category,
35
+ severity: opts.severity,
36
+ title: opts.title,
37
+ description: opts.description,
38
+ resolution: opts.resolution,
39
+ tags: opts.tags ? opts.tags.split(',').map((t) => t.trim()) : [],
40
+ context: {
41
+ current_step: opts.step,
42
+ related_files: opts.files ? opts.files.split(',').map((f) => f.trim()) : [],
43
+ },
44
+ };
45
+ await logIncident(incident);
46
+ console.log(chalk.green(`✓ Issue logged`));
47
+ console.log(` Category: ${chalk.cyan(opts.category)}`);
48
+ console.log(` Severity: ${chalk.cyan(opts.severity)}`);
49
+ console.log(` File: ${chalk.gray(`.beacon/incidents/${opts.category}.ndjson`)}`);
50
+ if (opts.severity === INCIDENT_SEVERITY.MAJOR ||
51
+ opts.severity === INCIDENT_SEVERITY.BLOCKER) {
52
+ console.log();
53
+ console.log(chalk.yellow(' ⚠ Consider documenting this in your WU notes as well.'));
54
+ }
55
+ }
56
+ catch (err) {
57
+ console.error(chalk.red(`Error: ${err.message}`));
58
+ if (err.issues) {
59
+ console.error(chalk.red('Validation errors:'));
60
+ err.issues.forEach((issue) => {
61
+ console.error(chalk.red(` - ${issue.path.join('.')}: ${issue.message}`));
62
+ });
63
+ }
64
+ process.exit(EXIT_CODES.ERROR);
65
+ }
66
+ });
67
+ program.parse();
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Session End CLI
4
+ *
5
+ * Ends the current agent session and returns summary.
6
+ *
7
+ * Usage:
8
+ * pnpm agent:session:end
9
+ */
10
+ import { Command } from 'commander';
11
+ import { endSession, getCurrentSession } from '@lumenflow/agent';
12
+ import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
13
+ import chalk from 'chalk';
14
+ const program = new Command()
15
+ .name('agent:session:end')
16
+ .description('End the current agent session')
17
+ .action(async () => {
18
+ try {
19
+ const session = await getCurrentSession();
20
+ if (!session) {
21
+ console.error(chalk.yellow('No active session to end.'));
22
+ process.exit(EXIT_CODES.SUCCESS);
23
+ }
24
+ const summary = await endSession();
25
+ console.log(chalk.green(`✓ Session ended`));
26
+ console.log(` WU: ${chalk.cyan(summary.wu_id)}`);
27
+ console.log(` Lane: ${chalk.cyan(summary.lane)}`);
28
+ console.log(` Duration: ${chalk.cyan(summary.started)} → ${chalk.cyan(summary.completed)}`);
29
+ console.log(` Incidents: ${summary.incidents_logged} (${summary.incidents_major} major)`);
30
+ }
31
+ catch (err) {
32
+ console.error(chalk.red(`Error: ${err.message}`));
33
+ process.exit(EXIT_CODES.ERROR);
34
+ }
35
+ });
36
+ program.parse();
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Session Start CLI
4
+ *
5
+ * Starts an agent session for tracking WU execution.
6
+ *
7
+ * Usage:
8
+ * pnpm agent:session --wu WU-1234 --tier 2
9
+ */
10
+ import { Command } from 'commander';
11
+ import { startSession, getCurrentSession } from '@lumenflow/agent';
12
+ import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
13
+ import chalk from 'chalk';
14
+ const program = new Command()
15
+ .name('agent:session')
16
+ .description('Start an agent session')
17
+ .requiredOption('--wu <id>', 'WU ID to work on (e.g., WU-1234)')
18
+ .requiredOption('--tier <tier>', 'Context tier (1, 2, or 3)')
19
+ .option('--agent-type <type>', 'Agent type', 'claude-code')
20
+ .action(async (opts) => {
21
+ try {
22
+ // Check for existing session
23
+ const existing = await getCurrentSession();
24
+ if (existing) {
25
+ console.error(chalk.red(`Session already active for ${existing.wu_id}`));
26
+ console.error(`Run: pnpm agent:session:end to close it first.`);
27
+ process.exit(EXIT_CODES.ERROR);
28
+ }
29
+ const tier = parseInt(opts.tier, 10);
30
+ if (![1, 2, 3].includes(tier)) {
31
+ console.error(chalk.red('Invalid tier. Must be 1, 2, or 3.'));
32
+ process.exit(EXIT_CODES.ERROR);
33
+ }
34
+ const sessionId = await startSession(opts.wu, tier, opts.agentType);
35
+ console.log(chalk.green(`✓ Session started`));
36
+ console.log(` Session ID: ${chalk.cyan(sessionId.slice(0, 8))}...`);
37
+ console.log(` WU: ${chalk.cyan(opts.wu)}`);
38
+ console.log(` Tier: ${chalk.cyan(tier)}`);
39
+ console.log(` Agent: ${chalk.cyan(opts.agentType)}`);
40
+ }
41
+ catch (err) {
42
+ console.error(chalk.red(`Error: ${err.message}`));
43
+ process.exit(EXIT_CODES.ERROR);
44
+ }
45
+ });
46
+ program.parse();
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Flow Bottlenecks Analysis CLI (WU-1018)
4
+ *
5
+ * Analyzes WU dependency graph to identify bottlenecks and critical paths.
6
+ *
7
+ * Usage:
8
+ * pnpm flow:bottlenecks # Default: top 10 bottlenecks, JSON output
9
+ * pnpm flow:bottlenecks --limit 5 # Top 5 bottlenecks
10
+ * pnpm flow:bottlenecks --format table # Table output
11
+ * pnpm flow:bottlenecks --format mermaid # Mermaid diagram of critical path
12
+ *
13
+ * @module flow-bottlenecks
14
+ * @see {@link @lumenflow/metrics/flow/analyze-bottlenecks}
15
+ */
16
+ import { Command } from 'commander';
17
+ import { getBottleneckAnalysis, } from '@lumenflow/metrics';
18
+ import { buildDependencyGraph, renderMermaid } from '@lumenflow/core/dist/dependency-graph.js';
19
+ import { die } from '@lumenflow/core/dist/error-handler.js';
20
+ /** Log prefix for console output */
21
+ const LOG_PREFIX = '[flow:bottlenecks]';
22
+ /** Default bottleneck limit */
23
+ const DEFAULT_LIMIT = 10;
24
+ /** Output format options */
25
+ const OUTPUT_FORMATS = {
26
+ JSON: 'json',
27
+ TABLE: 'table',
28
+ MERMAID: 'mermaid',
29
+ };
30
+ /**
31
+ * Parse command line arguments
32
+ */
33
+ function parseArgs() {
34
+ const program = new Command()
35
+ .name('flow-bottlenecks')
36
+ .description('Analyze WU dependency graph for bottlenecks and critical paths')
37
+ .option('--limit <number>', `Number of bottlenecks to show (default: ${DEFAULT_LIMIT})`, String(DEFAULT_LIMIT))
38
+ .option('--format <type>', `Output format: json, table, mermaid (default: json)`, OUTPUT_FORMATS.JSON)
39
+ .exitOverride();
40
+ try {
41
+ program.parse(process.argv);
42
+ return program.opts();
43
+ }
44
+ catch (err) {
45
+ const error = err;
46
+ if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') {
47
+ process.exit(0);
48
+ }
49
+ throw err;
50
+ }
51
+ }
52
+ /**
53
+ * Convert core DependencyGraph to metrics DependencyGraph format
54
+ *
55
+ * The core module uses a slightly different node format, so we need to transform it.
56
+ */
57
+ function convertToMetricsGraph(coreGraph) {
58
+ const metricsGraph = new Map();
59
+ for (const [id, node] of coreGraph.entries()) {
60
+ metricsGraph.set(id, {
61
+ id: node.id,
62
+ title: node.title,
63
+ blocks: node.blocks,
64
+ blockedBy: node.blockedBy,
65
+ status: node.status,
66
+ });
67
+ }
68
+ return metricsGraph;
69
+ }
70
+ /**
71
+ * Format analysis as table output
72
+ */
73
+ function formatAsTable(analysis) {
74
+ const lines = [];
75
+ lines.push('═══════════════════════════════════════════════════════════════');
76
+ lines.push(' BOTTLENECK ANALYSIS');
77
+ lines.push('═══════════════════════════════════════════════════════════════');
78
+ lines.push('');
79
+ // Critical Path section
80
+ lines.push('┌─────────────────────────────────────────────────────────────┐');
81
+ lines.push('│ CRITICAL PATH │');
82
+ lines.push('├─────────────────────────────────────────────────────────────┤');
83
+ if (analysis.criticalPath.warning) {
84
+ lines.push(`│ ⚠️ ${analysis.criticalPath.warning.padEnd(56)} │`);
85
+ if (analysis.criticalPath.cycleNodes && analysis.criticalPath.cycleNodes.length > 0) {
86
+ lines.push(`│ Cycle nodes: ${analysis.criticalPath.cycleNodes.join(', ').slice(0, 46)} │`);
87
+ }
88
+ }
89
+ else if (analysis.criticalPath.path.length === 0) {
90
+ lines.push('│ No critical path (all WUs are independent or completed) │');
91
+ }
92
+ else {
93
+ lines.push(`│ Length: ${analysis.criticalPath.length} WUs`);
94
+ lines.push('│');
95
+ lines.push('│ Path:');
96
+ for (let i = 0; i < analysis.criticalPath.path.length; i++) {
97
+ const wuId = analysis.criticalPath.path[i];
98
+ const arrow = i < analysis.criticalPath.path.length - 1 ? ' → ' : '';
99
+ lines.push(`│ ${i + 1}. ${wuId}${arrow}`);
100
+ }
101
+ }
102
+ lines.push('└─────────────────────────────────────────────────────────────┘');
103
+ lines.push('');
104
+ // Bottlenecks section
105
+ lines.push('┌─────────────────────────────────────────────────────────────┐');
106
+ lines.push('│ TOP BOTTLENECKS (by impact score) │');
107
+ lines.push('├─────────────────────────────────────────────────────────────┤');
108
+ if (analysis.bottlenecks.length === 0) {
109
+ lines.push('│ No bottlenecks found (no active dependencies) │');
110
+ }
111
+ else {
112
+ lines.push('│ # WU ID Score Title │');
113
+ lines.push('│ ─── ───────── ───── ─────────────────────────────────────│');
114
+ for (let i = 0; i < analysis.bottlenecks.length; i++) {
115
+ const b = analysis.bottlenecks[i];
116
+ const rank = String(i + 1).padStart(2);
117
+ const score = String(b.score).padStart(5);
118
+ const title = (b.title ?? 'Unknown').slice(0, 35);
119
+ lines.push(`│ ${rank}. ${b.id.padEnd(9)} ${score} ${title}`);
120
+ }
121
+ }
122
+ lines.push('└─────────────────────────────────────────────────────────────┘');
123
+ lines.push('');
124
+ // Explanation
125
+ lines.push('Impact score = number of downstream WUs blocked (recursively)');
126
+ lines.push('Critical path = longest dependency chain in the graph');
127
+ return lines.join('\n');
128
+ }
129
+ /**
130
+ * Format critical path as Mermaid diagram
131
+ */
132
+ function formatAsMermaid(analysis, coreGraph) {
133
+ if (analysis.criticalPath.path.length === 0) {
134
+ return '%%{ Critical path is empty - no active dependencies }%%\ngraph TD\n empty[No critical path]';
135
+ }
136
+ const rootId = analysis.criticalPath.path[0];
137
+ return renderMermaid(coreGraph, { rootId, direction: 'TD', depth: analysis.criticalPath.length });
138
+ }
139
+ /**
140
+ * Main function
141
+ */
142
+ async function main() {
143
+ const opts = parseArgs();
144
+ const limit = parseInt(opts.limit, 10);
145
+ console.log(`${LOG_PREFIX} Building dependency graph...`);
146
+ // Build dependency graph from WU YAML files
147
+ const coreGraph = buildDependencyGraph();
148
+ if (coreGraph.size === 0) {
149
+ console.log(`${LOG_PREFIX} No WUs found in dependency graph.`);
150
+ console.log(`${LOG_PREFIX} Ensure WU YAML files exist in docs/04-operations/tasks/wu/ with blocked_by/blocks fields.`);
151
+ return;
152
+ }
153
+ console.log(`${LOG_PREFIX} Found ${coreGraph.size} WUs in graph`);
154
+ // Convert to metrics-compatible graph format
155
+ const metricsGraph = convertToMetricsGraph(coreGraph);
156
+ // Perform bottleneck analysis
157
+ console.log(`${LOG_PREFIX} Analyzing bottlenecks (top ${limit})...`);
158
+ const analysis = getBottleneckAnalysis(metricsGraph, limit);
159
+ // Count active WUs
160
+ const activeWUs = Array.from(coreGraph.values()).filter((n) => n.status !== 'done').length;
161
+ console.log(`${LOG_PREFIX} Active WUs: ${activeWUs}`);
162
+ console.log(`${LOG_PREFIX} Bottlenecks found: ${analysis.bottlenecks.length}`);
163
+ console.log(`${LOG_PREFIX} Critical path length: ${analysis.criticalPath.length}`);
164
+ // Output analysis
165
+ console.log('');
166
+ switch (opts.format) {
167
+ case OUTPUT_FORMATS.TABLE:
168
+ console.log(formatAsTable(analysis));
169
+ break;
170
+ case OUTPUT_FORMATS.MERMAID:
171
+ console.log(formatAsMermaid(analysis, coreGraph));
172
+ break;
173
+ default:
174
+ console.log(JSON.stringify(analysis, null, 2));
175
+ }
176
+ }
177
+ // Guard main() for testability (WU-1366)
178
+ import { fileURLToPath } from 'node:url';
179
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
180
+ main().catch((err) => {
181
+ die(`Bottleneck analysis failed: ${err.message}`);
182
+ });
183
+ }