@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.
- package/dist/__tests__/flow-report.test.js +24 -0
- package/dist/__tests__/metrics-snapshot.test.js +24 -0
- package/dist/agent-issues-query.js +251 -0
- package/dist/agent-log-issue.js +67 -0
- package/dist/agent-session-end.js +36 -0
- package/dist/agent-session.js +46 -0
- package/dist/flow-bottlenecks.js +183 -0
- package/dist/flow-report.js +311 -0
- package/dist/gates.js +126 -49
- package/dist/init.js +297 -0
- package/dist/initiative-bulk-assign-wus.js +315 -0
- package/dist/initiative-create.js +3 -7
- package/dist/initiative-edit.js +3 -3
- package/dist/metrics-snapshot.js +314 -0
- package/dist/orchestrate-init-status.js +64 -0
- package/dist/orchestrate-initiative.js +100 -0
- package/dist/orchestrate-monitor.js +90 -0
- package/dist/wu-claim.js +313 -116
- package/dist/wu-cleanup.js +49 -3
- package/dist/wu-create.js +195 -121
- package/dist/wu-delete.js +241 -0
- package/dist/wu-done.js +146 -23
- package/dist/wu-edit.js +152 -61
- package/dist/wu-infer-lane.js +2 -2
- package/dist/wu-spawn.js +77 -158
- package/dist/wu-unlock-lane.js +158 -0
- package/package.json +30 -10
package/dist/initiative-edit.js
CHANGED
|
@@ -35,7 +35,7 @@ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
|
35
35
|
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
36
36
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
37
37
|
import { join } from 'node:path';
|
|
38
|
-
import
|
|
38
|
+
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
39
39
|
import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
|
|
40
40
|
import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
|
|
41
41
|
import { INIT_STATUSES, PHASE_STATUSES, INIT_PATTERNS, INIT_LOG_PREFIX, INIT_COMMIT_FORMATS, } from '@lumenflow/initiatives/dist/initiative-constants.js';
|
|
@@ -230,7 +230,7 @@ function loadInitiative(id) {
|
|
|
230
230
|
}
|
|
231
231
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates init files
|
|
232
232
|
const content = readFileSync(initPath, { encoding: FILE_SYSTEM.ENCODING });
|
|
233
|
-
return
|
|
233
|
+
return parseYAML(content);
|
|
234
234
|
}
|
|
235
235
|
/**
|
|
236
236
|
* Ensure working tree is clean
|
|
@@ -421,7 +421,7 @@ async function main() {
|
|
|
421
421
|
execute: async ({ worktreePath }) => {
|
|
422
422
|
// Write updated Initiative to micro-worktree
|
|
423
423
|
const initPath = join(worktreePath, INIT_PATHS.INITIATIVE(id));
|
|
424
|
-
const yamlContent =
|
|
424
|
+
const yamlContent = stringifyYAML(updatedInit);
|
|
425
425
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes init files
|
|
426
426
|
writeFileSync(initPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
|
|
427
427
|
console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Metrics Snapshot Capture CLI (WU-1018)
|
|
4
|
+
*
|
|
5
|
+
* Captures DORA metrics, lane health, and flow state snapshots.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* pnpm metrics:snapshot # All metrics, JSON output
|
|
9
|
+
* pnpm metrics:snapshot --type dora # DORA metrics only
|
|
10
|
+
* pnpm metrics:snapshot --type lanes # Lane health only
|
|
11
|
+
* pnpm metrics:snapshot --type flow # Flow state only
|
|
12
|
+
* pnpm metrics:snapshot --dry-run # Preview without writing
|
|
13
|
+
* pnpm metrics:snapshot --output metrics.json
|
|
14
|
+
*
|
|
15
|
+
* @module metrics-snapshot
|
|
16
|
+
* @see {@link @lumenflow/metrics/flow/capture-metrics-snapshot}
|
|
17
|
+
*/
|
|
18
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { join, dirname } from 'node:path';
|
|
21
|
+
import fg from 'fast-glob';
|
|
22
|
+
import { parse as parseYaml } from 'yaml';
|
|
23
|
+
import { Command } from 'commander';
|
|
24
|
+
import { captureMetricsSnapshot, } from '@lumenflow/metrics';
|
|
25
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
26
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
27
|
+
/** Log prefix for console output */
|
|
28
|
+
const LOG_PREFIX = '[metrics:snapshot]';
|
|
29
|
+
/** Default snapshot output path */
|
|
30
|
+
const DEFAULT_OUTPUT = '.beacon/snapshots/metrics-latest.json';
|
|
31
|
+
/** WU directory relative to repo root */
|
|
32
|
+
const WU_DIR = 'docs/04-operations/tasks/wu';
|
|
33
|
+
/** Skip-gates audit file path */
|
|
34
|
+
const SKIP_GATES_PATH = '.beacon/skip-gates-audit.ndjson';
|
|
35
|
+
/** Snapshot type options */
|
|
36
|
+
const SNAPSHOT_TYPES = ['all', 'dora', 'lanes', 'flow'];
|
|
37
|
+
/**
|
|
38
|
+
* Parse command line arguments
|
|
39
|
+
*/
|
|
40
|
+
function parseArgs() {
|
|
41
|
+
const program = new Command()
|
|
42
|
+
.name('metrics-snapshot')
|
|
43
|
+
.description('Capture DORA metrics, lane health, and flow state snapshot')
|
|
44
|
+
.option('--type <type>', `Snapshot type: ${SNAPSHOT_TYPES.join(', ')} (default: all)`, 'all')
|
|
45
|
+
.option('--days <number>', 'Days to analyze for DORA metrics (default: 7)', '7')
|
|
46
|
+
.option('--output <path>', `Output file path (default: ${DEFAULT_OUTPUT})`, DEFAULT_OUTPUT)
|
|
47
|
+
.option('--dry-run', 'Preview snapshot without writing to file')
|
|
48
|
+
.exitOverride();
|
|
49
|
+
try {
|
|
50
|
+
program.parse(process.argv);
|
|
51
|
+
return program.opts();
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const error = err;
|
|
55
|
+
if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') {
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Calculate week date range
|
|
63
|
+
*/
|
|
64
|
+
function calculateWeekRange(days) {
|
|
65
|
+
const weekEnd = new Date();
|
|
66
|
+
weekEnd.setHours(23, 59, 59, 999);
|
|
67
|
+
const weekStart = new Date(weekEnd);
|
|
68
|
+
weekStart.setDate(weekStart.getDate() - days);
|
|
69
|
+
weekStart.setHours(0, 0, 0, 0);
|
|
70
|
+
return { weekStart, weekEnd };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Load WU metrics from YAML files
|
|
74
|
+
*/
|
|
75
|
+
async function loadWUMetrics(baseDir) {
|
|
76
|
+
const wuDir = join(baseDir, WU_DIR);
|
|
77
|
+
const wuFiles = await fg('WU-*.yaml', { cwd: wuDir, absolute: true });
|
|
78
|
+
const wuMetrics = [];
|
|
79
|
+
for (const file of wuFiles) {
|
|
80
|
+
try {
|
|
81
|
+
const content = await readFile(file, { encoding: 'utf-8' });
|
|
82
|
+
const wu = parseYaml(content);
|
|
83
|
+
// Map WU status to valid WUMetrics status
|
|
84
|
+
const rawStatus = wu.status;
|
|
85
|
+
let status = 'ready';
|
|
86
|
+
if (rawStatus === 'in_progress')
|
|
87
|
+
status = 'in_progress';
|
|
88
|
+
else if (rawStatus === 'blocked')
|
|
89
|
+
status = 'blocked';
|
|
90
|
+
else if (rawStatus === 'waiting')
|
|
91
|
+
status = 'waiting';
|
|
92
|
+
else if (rawStatus === 'done')
|
|
93
|
+
status = 'done';
|
|
94
|
+
else if (rawStatus === 'ready')
|
|
95
|
+
status = 'ready';
|
|
96
|
+
wuMetrics.push({
|
|
97
|
+
id: wu.id,
|
|
98
|
+
title: wu.title,
|
|
99
|
+
lane: wu.lane,
|
|
100
|
+
status,
|
|
101
|
+
priority: wu.priority,
|
|
102
|
+
claimedAt: wu.claimed_at ? new Date(wu.claimed_at) : undefined,
|
|
103
|
+
completedAt: wu.completed_at ? new Date(wu.completed_at) : undefined,
|
|
104
|
+
cycleTimeHours: calculateCycleTime(wu),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Skip invalid WU files
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return wuMetrics;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculate cycle time in hours from WU data
|
|
115
|
+
*/
|
|
116
|
+
function calculateCycleTime(wu) {
|
|
117
|
+
if (!wu.claimed_at || !wu.completed_at) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const claimed = new Date(wu.claimed_at);
|
|
121
|
+
const completed = new Date(wu.completed_at);
|
|
122
|
+
const diffMs = completed.getTime() - claimed.getTime();
|
|
123
|
+
const diffHours = diffMs / (1000 * 60 * 60);
|
|
124
|
+
return Math.round(diffHours * 10) / 10;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Load git commits from repository
|
|
128
|
+
*/
|
|
129
|
+
async function loadGitCommits(weekStart, weekEnd) {
|
|
130
|
+
try {
|
|
131
|
+
const git = getGitForCwd();
|
|
132
|
+
// simple-git log() returns an object with 'all' array containing commits
|
|
133
|
+
// The actual simple-git result includes date but the adapter type is narrowed
|
|
134
|
+
const logResult = await git.log({ maxCount: 500 });
|
|
135
|
+
const commits = [];
|
|
136
|
+
for (const entry of [...logResult.all]) {
|
|
137
|
+
// Filter by date range
|
|
138
|
+
const commitDate = new Date(entry.date);
|
|
139
|
+
if (commitDate < weekStart || commitDate > weekEnd) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const message = entry.message;
|
|
143
|
+
// Extract WU ID from commit message if present
|
|
144
|
+
const wuIdMatch = message.match(/\b(WU-\d+)\b/i);
|
|
145
|
+
const wuId = wuIdMatch ? wuIdMatch[1].toUpperCase() : undefined;
|
|
146
|
+
// Determine commit type from conventional commit prefix
|
|
147
|
+
const typeMatch = message.match(/^(feat|fix|docs|chore|refactor|test|style|perf|ci)[\(:]?/i);
|
|
148
|
+
const type = typeMatch ? typeMatch[1].toLowerCase() : undefined;
|
|
149
|
+
commits.push({
|
|
150
|
+
hash: entry.hash,
|
|
151
|
+
timestamp: commitDate,
|
|
152
|
+
message,
|
|
153
|
+
type,
|
|
154
|
+
wuId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return commits;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.warn(`${LOG_PREFIX} ⚠️ Could not load git commits: ${err.message}`);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Load skip-gates audit entries
|
|
166
|
+
*/
|
|
167
|
+
async function loadSkipGatesEntries(baseDir) {
|
|
168
|
+
const auditPath = join(baseDir, SKIP_GATES_PATH);
|
|
169
|
+
if (!existsSync(auditPath)) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const content = await readFile(auditPath, { encoding: 'utf-8' });
|
|
174
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
175
|
+
const entries = [];
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
try {
|
|
178
|
+
const raw = JSON.parse(line);
|
|
179
|
+
if (raw.timestamp && raw.wu_id && raw.reason && raw.gate) {
|
|
180
|
+
entries.push({
|
|
181
|
+
timestamp: new Date(raw.timestamp),
|
|
182
|
+
wuId: raw.wu_id,
|
|
183
|
+
reason: raw.reason,
|
|
184
|
+
gate: raw.gate,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Skip invalid JSON lines
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return entries;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Format snapshot for display
|
|
200
|
+
*/
|
|
201
|
+
function formatSnapshot(snapshot, type) {
|
|
202
|
+
const lines = [];
|
|
203
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
204
|
+
lines.push(` METRICS SNAPSHOT (type: ${type})`);
|
|
205
|
+
lines.push(` Generated: ${new Date().toISOString()}`);
|
|
206
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
207
|
+
lines.push('');
|
|
208
|
+
if (snapshot.dora) {
|
|
209
|
+
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
|
210
|
+
lines.push('│ DORA METRICS │');
|
|
211
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
212
|
+
const { deploymentFrequency, leadTimeForChanges, changeFailureRate, meanTimeToRecovery } = snapshot.dora;
|
|
213
|
+
lines.push(`│ Deployment Frequency: ${deploymentFrequency.deploysPerWeek}/week (${deploymentFrequency.status})`);
|
|
214
|
+
lines.push(`│ Lead Time: ${leadTimeForChanges.medianHours}h median (${leadTimeForChanges.status})`);
|
|
215
|
+
lines.push(`│ Change Failure Rate: ${changeFailureRate.failurePercentage}% (${changeFailureRate.status})`);
|
|
216
|
+
lines.push(`│ MTTR: ${meanTimeToRecovery.averageHours}h (${meanTimeToRecovery.status})`);
|
|
217
|
+
lines.push('└─────────────────────────────────────────────────────────────┘');
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
if (snapshot.lanes) {
|
|
221
|
+
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
|
222
|
+
lines.push('│ LANE HEALTH │');
|
|
223
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
224
|
+
lines.push(`│ Total Active: ${snapshot.lanes.totalActive} | Blocked: ${snapshot.lanes.totalBlocked} | Completed: ${snapshot.lanes.totalCompleted}`);
|
|
225
|
+
lines.push('│');
|
|
226
|
+
for (const lane of snapshot.lanes.lanes) {
|
|
227
|
+
const statusIcon = lane.status === 'healthy' ? '✓' : lane.status === 'at-risk' ? '⚠' : '✗';
|
|
228
|
+
lines.push(`│ ${statusIcon} ${lane.lane.padEnd(20)} ${lane.wusCompleted} done, ${lane.wusInProgress} active, ${lane.wusBlocked} blocked`);
|
|
229
|
+
}
|
|
230
|
+
lines.push('└─────────────────────────────────────────────────────────────┘');
|
|
231
|
+
lines.push('');
|
|
232
|
+
}
|
|
233
|
+
if (snapshot.flow) {
|
|
234
|
+
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
|
235
|
+
lines.push('│ FLOW STATE │');
|
|
236
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
237
|
+
lines.push(`│ Ready: ${snapshot.flow.ready} | In Progress: ${snapshot.flow.inProgress}`);
|
|
238
|
+
lines.push(`│ Blocked: ${snapshot.flow.blocked} | Waiting: ${snapshot.flow.waiting}`);
|
|
239
|
+
lines.push(`│ Done: ${snapshot.flow.done} | Total Active: ${snapshot.flow.totalActive}`);
|
|
240
|
+
lines.push('└─────────────────────────────────────────────────────────────┘');
|
|
241
|
+
}
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Main function
|
|
246
|
+
*/
|
|
247
|
+
async function main() {
|
|
248
|
+
const opts = parseArgs();
|
|
249
|
+
const baseDir = process.cwd();
|
|
250
|
+
const type = opts.type;
|
|
251
|
+
const days = parseInt(opts.days, 10);
|
|
252
|
+
// Validate snapshot type
|
|
253
|
+
if (!SNAPSHOT_TYPES.includes(type)) {
|
|
254
|
+
die(`Invalid snapshot type: ${type}\n\nValid types: ${SNAPSHOT_TYPES.join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
console.log(`${LOG_PREFIX} Capturing ${type} metrics snapshot...`);
|
|
257
|
+
const { weekStart, weekEnd } = calculateWeekRange(days);
|
|
258
|
+
console.log(`${LOG_PREFIX} Date range: ${weekStart.toISOString().split('T')[0]} to ${weekEnd.toISOString().split('T')[0]}`);
|
|
259
|
+
// Load data
|
|
260
|
+
console.log(`${LOG_PREFIX} Loading WU data...`);
|
|
261
|
+
const wuMetrics = await loadWUMetrics(baseDir);
|
|
262
|
+
console.log(`${LOG_PREFIX} Found ${wuMetrics.length} WUs`);
|
|
263
|
+
console.log(`${LOG_PREFIX} Loading git commits...`);
|
|
264
|
+
const commits = await loadGitCommits(weekStart, weekEnd);
|
|
265
|
+
console.log(`${LOG_PREFIX} Found ${commits.length} commits`);
|
|
266
|
+
console.log(`${LOG_PREFIX} Loading skip-gates audit entries...`);
|
|
267
|
+
const skipGatesEntries = await loadSkipGatesEntries(baseDir);
|
|
268
|
+
console.log(`${LOG_PREFIX} Found ${skipGatesEntries.length} skip-gates entries`);
|
|
269
|
+
// Capture snapshot
|
|
270
|
+
const input = {
|
|
271
|
+
commits,
|
|
272
|
+
wuMetrics,
|
|
273
|
+
skipGatesEntries,
|
|
274
|
+
weekStart,
|
|
275
|
+
weekEnd,
|
|
276
|
+
type,
|
|
277
|
+
};
|
|
278
|
+
const snapshot = captureMetricsSnapshot(input);
|
|
279
|
+
// Output
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log(formatSnapshot(snapshot, type));
|
|
282
|
+
console.log('');
|
|
283
|
+
// Write to file (unless dry-run)
|
|
284
|
+
if (opts.dryRun) {
|
|
285
|
+
console.log(`${LOG_PREFIX} Dry run - not writing to file.`);
|
|
286
|
+
console.log(`${LOG_PREFIX} Would write to: ${opts.output}`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const outputPath = join(baseDir, opts.output);
|
|
290
|
+
const outputDir = dirname(outputPath);
|
|
291
|
+
// Ensure directory exists
|
|
292
|
+
if (!existsSync(outputDir)) {
|
|
293
|
+
await mkdir(outputDir, { recursive: true });
|
|
294
|
+
}
|
|
295
|
+
const outputData = {
|
|
296
|
+
capturedAt: new Date().toISOString(),
|
|
297
|
+
type,
|
|
298
|
+
dateRange: {
|
|
299
|
+
start: weekStart.toISOString(),
|
|
300
|
+
end: weekEnd.toISOString(),
|
|
301
|
+
},
|
|
302
|
+
snapshot,
|
|
303
|
+
};
|
|
304
|
+
await writeFile(outputPath, JSON.stringify(outputData, null, 2), { encoding: 'utf-8' });
|
|
305
|
+
console.log(`${LOG_PREFIX} ✅ Snapshot written to: ${outputPath}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Guard main() for testability (WU-1366)
|
|
309
|
+
import { fileURLToPath } from 'node:url';
|
|
310
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
311
|
+
main().catch((err) => {
|
|
312
|
+
die(`Metrics snapshot failed: ${err.message}`);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrate Initiative Status CLI
|
|
4
|
+
*
|
|
5
|
+
* Compact status view for initiative orchestration.
|
|
6
|
+
* Shows progress of WUs in an initiative.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm orchestrate:init-status --initiative INIT-001
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { loadInitiativeWUs, calculateProgress, formatProgress } from '@lumenflow/initiatives';
|
|
14
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
const LOG_PREFIX = '[orchestrate:init-status]';
|
|
17
|
+
const STAMPS_DIR = '.beacon/stamps';
|
|
18
|
+
function getCompletedWUs(wuIds) {
|
|
19
|
+
const completed = new Set();
|
|
20
|
+
if (!existsSync(STAMPS_DIR)) {
|
|
21
|
+
return completed;
|
|
22
|
+
}
|
|
23
|
+
const files = readdirSync(STAMPS_DIR);
|
|
24
|
+
for (const wuId of wuIds) {
|
|
25
|
+
if (files.includes(`${wuId}.done`)) {
|
|
26
|
+
completed.add(wuId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return completed;
|
|
30
|
+
}
|
|
31
|
+
const program = new Command()
|
|
32
|
+
.name('orchestrate:init-status')
|
|
33
|
+
.description('Show initiative progress status')
|
|
34
|
+
.requiredOption('-i, --initiative <id>', 'Initiative ID (e.g., INIT-001)')
|
|
35
|
+
.action(async (opts) => {
|
|
36
|
+
try {
|
|
37
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Loading initiative ${opts.initiative}...`));
|
|
38
|
+
const { initiative, wus } = loadInitiativeWUs(opts.initiative);
|
|
39
|
+
console.log(chalk.bold(`\nInitiative: ${initiative.id} - ${initiative.title}`));
|
|
40
|
+
console.log('');
|
|
41
|
+
const progress = calculateProgress(wus);
|
|
42
|
+
console.log(chalk.bold('Progress:'));
|
|
43
|
+
console.log(formatProgress(progress));
|
|
44
|
+
console.log('');
|
|
45
|
+
// Show WU status breakdown
|
|
46
|
+
const completed = getCompletedWUs(wus.map((w) => w.id));
|
|
47
|
+
console.log(chalk.bold('WUs:'));
|
|
48
|
+
for (const wu of wus) {
|
|
49
|
+
const status = completed.has(wu.id)
|
|
50
|
+
? chalk.green('✓ done')
|
|
51
|
+
: wu.doc.status === 'in_progress'
|
|
52
|
+
? chalk.yellow('⟳ in_progress')
|
|
53
|
+
: wu.doc.status === 'blocked'
|
|
54
|
+
? chalk.red('⛔ blocked')
|
|
55
|
+
: chalk.gray('○ ready');
|
|
56
|
+
console.log(` ${wu.id}: ${wu.doc.title} [${status}]`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${err.message}`));
|
|
61
|
+
process.exit(EXIT_CODES.ERROR);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
program.parse();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrate Initiative CLI
|
|
4
|
+
*
|
|
5
|
+
* Orchestrate initiative execution with parallel agent spawning.
|
|
6
|
+
* Builds execution plan based on WU dependencies and manages wave-based execution.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm orchestrate:initiative --initiative INIT-001
|
|
10
|
+
* pnpm orchestrate:initiative --initiative INIT-001 --dry-run
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { loadInitiativeWUs, loadMultipleInitiatives, buildExecutionPlan, formatExecutionPlan, calculateProgress, formatProgress, buildCheckpointWave, formatCheckpointOutput, validateCheckpointFlags, resolveCheckpointMode, LOG_PREFIX, } from '@lumenflow/initiatives';
|
|
15
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
16
|
+
const program = new Command()
|
|
17
|
+
.name('orchestrate-initiative')
|
|
18
|
+
.description('Orchestrate initiative execution with parallel agent spawning')
|
|
19
|
+
.option('-i, --initiative <ids...>', 'Initiative ID(s) to orchestrate')
|
|
20
|
+
.option('-d, --dry-run', 'Show execution plan without spawning agents')
|
|
21
|
+
.option('-p, --progress', 'Show current progress only')
|
|
22
|
+
.option('-c, --checkpoint-per-wave', 'Spawn next wave then exit (no polling)')
|
|
23
|
+
.option('--no-checkpoint', 'Force polling mode')
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
const { initiative: initIds, dryRun, progress: progressOnly, checkpointPerWave, checkpoint, } = options;
|
|
26
|
+
const noCheckpoint = checkpoint === false;
|
|
27
|
+
try {
|
|
28
|
+
validateCheckpointFlags({ checkpointPerWave, dryRun, noCheckpoint });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${error.message}`));
|
|
32
|
+
process.exit(EXIT_CODES.ERROR);
|
|
33
|
+
}
|
|
34
|
+
if (!initIds || initIds.length === 0) {
|
|
35
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: --initiative is required`));
|
|
36
|
+
console.error('');
|
|
37
|
+
console.error('Usage:');
|
|
38
|
+
console.error(' pnpm orchestrate:initiative --initiative INIT-001');
|
|
39
|
+
console.error(' pnpm orchestrate:initiative --initiative INIT-001 --dry-run');
|
|
40
|
+
process.exit(EXIT_CODES.ERROR);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Loading initiative(s): ${initIds.join(', ')}`));
|
|
44
|
+
let wus;
|
|
45
|
+
let initiative;
|
|
46
|
+
if (initIds.length === 1) {
|
|
47
|
+
const result = loadInitiativeWUs(initIds[0]);
|
|
48
|
+
initiative = result.initiative;
|
|
49
|
+
wus = result.wus;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
wus = loadMultipleInitiatives(initIds);
|
|
53
|
+
initiative = { id: 'MULTI', title: `Combined: ${initIds.join(', ')}` };
|
|
54
|
+
}
|
|
55
|
+
console.log(chalk.green(`${LOG_PREFIX} Loaded ${wus.length} WU(s)`));
|
|
56
|
+
console.log('');
|
|
57
|
+
const progress = calculateProgress(wus);
|
|
58
|
+
console.log(chalk.bold('Progress:'));
|
|
59
|
+
console.log(formatProgress(progress));
|
|
60
|
+
console.log('');
|
|
61
|
+
if (progressOnly) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const checkpointDecision = resolveCheckpointMode({ checkpointPerWave, noCheckpoint, dryRun }, wus);
|
|
65
|
+
if (checkpointDecision.enabled) {
|
|
66
|
+
if (initIds.length > 1) {
|
|
67
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: Checkpoint mode only supports single initiative`));
|
|
68
|
+
process.exit(EXIT_CODES.ERROR);
|
|
69
|
+
}
|
|
70
|
+
const waveData = buildCheckpointWave(initIds[0], { dryRun });
|
|
71
|
+
if (!waveData) {
|
|
72
|
+
console.log(chalk.green(`${LOG_PREFIX} All WUs are complete! Nothing to spawn.`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log(formatCheckpointOutput({ ...waveData, dryRun }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Building execution plan...`));
|
|
79
|
+
const plan = buildExecutionPlan(wus);
|
|
80
|
+
if (plan.waves.length === 0) {
|
|
81
|
+
console.log(chalk.green(`${LOG_PREFIX} All WUs are complete! Nothing to execute.`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.bold('Execution Plan:'));
|
|
86
|
+
console.log(formatExecutionPlan(initiative, plan));
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
console.log(chalk.yellow(`${LOG_PREFIX} Dry run mode - no agents spawned`));
|
|
89
|
+
console.log(chalk.cyan('To execute this plan, remove the --dry-run flag.'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(chalk.green(`${LOG_PREFIX} Execution plan output complete.`));
|
|
93
|
+
console.log(chalk.cyan('Copy the spawn commands above to execute.'));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${error.message}`));
|
|
97
|
+
process.exit(EXIT_CODES.ERROR);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
program.parse();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrate Monitor CLI
|
|
4
|
+
*
|
|
5
|
+
* Monitors spawned agent progress using mem:inbox signals.
|
|
6
|
+
* Designed to prevent context exhaustion by using compact signal output.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm orchestrate:monitor --since 30m
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import ms from 'ms';
|
|
17
|
+
const LOG_PREFIX = '[orchestrate:monitor]';
|
|
18
|
+
const MEMORY_DIR = '.beacon/memory';
|
|
19
|
+
function parseTimeString(timeStr) {
|
|
20
|
+
const msValue = ms(timeStr);
|
|
21
|
+
if (typeof msValue === 'number') {
|
|
22
|
+
return new Date(Date.now() - msValue);
|
|
23
|
+
}
|
|
24
|
+
const date = new Date(timeStr);
|
|
25
|
+
if (isNaN(date.getTime())) {
|
|
26
|
+
throw new Error(`Invalid time format: ${timeStr}`);
|
|
27
|
+
}
|
|
28
|
+
return date;
|
|
29
|
+
}
|
|
30
|
+
function loadRecentSignals(since) {
|
|
31
|
+
const signals = [];
|
|
32
|
+
if (!existsSync(MEMORY_DIR)) {
|
|
33
|
+
return signals;
|
|
34
|
+
}
|
|
35
|
+
const files = readdirSync(MEMORY_DIR).filter((f) => f.endsWith('.ndjson'));
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const filePath = join(MEMORY_DIR, file);
|
|
38
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
39
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
try {
|
|
42
|
+
const signal = JSON.parse(line);
|
|
43
|
+
const signalTime = new Date(signal.timestamp);
|
|
44
|
+
if (signalTime >= since) {
|
|
45
|
+
signals.push(signal);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Skip malformed lines
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return signals.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
54
|
+
}
|
|
55
|
+
const program = new Command()
|
|
56
|
+
.name('orchestrate:monitor')
|
|
57
|
+
.description('Monitor spawned agent progress')
|
|
58
|
+
.option('--since <time>', 'Show signals since (e.g., 30m, 1h)', '30m')
|
|
59
|
+
.option('--wu <id>', 'Filter by WU ID')
|
|
60
|
+
.action((opts) => {
|
|
61
|
+
try {
|
|
62
|
+
const since = parseTimeString(opts.since);
|
|
63
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Loading signals since ${since.toISOString()}...`));
|
|
64
|
+
const signals = loadRecentSignals(since);
|
|
65
|
+
if (signals.length === 0) {
|
|
66
|
+
console.log(chalk.yellow(`${LOG_PREFIX} No signals found.`));
|
|
67
|
+
console.log(chalk.gray('Agents may still be starting up, or memory layer not initialized.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const filtered = opts.wu ? signals.filter((s) => s.wuId === opts.wu) : signals;
|
|
71
|
+
console.log(chalk.bold(`\nRecent Signals (${filtered.length}):\n`));
|
|
72
|
+
for (const signal of filtered) {
|
|
73
|
+
const time = new Date(signal.timestamp).toLocaleTimeString();
|
|
74
|
+
const wu = signal.wuId ? chalk.cyan(signal.wuId) : chalk.gray('system');
|
|
75
|
+
const type = signal.type === 'complete'
|
|
76
|
+
? chalk.green(signal.type)
|
|
77
|
+
: signal.type === 'error'
|
|
78
|
+
? chalk.red(signal.type)
|
|
79
|
+
: chalk.yellow(signal.type);
|
|
80
|
+
console.log(` ${chalk.gray(time)} [${wu}] ${type}: ${signal.message || ''}`);
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(chalk.gray(`Use: pnpm mem:inbox --since ${opts.since} for more details`));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${err.message}`));
|
|
87
|
+
process.exit(EXIT_CODES.ERROR);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
program.parse();
|