@lumenflow/cli 1.0.0 → 1.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/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 +24 -10
- package/dist/init.js +251 -0
- package/dist/initiative-bulk-assign-wus.js +315 -0
- 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 +18 -7
- package/dist/wu-delete.js +241 -0
- package/dist/wu-done.js +102 -10
- package/dist/wu-unlock-lane.js +158 -0
- package/package.json +24 -4
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Flow Report Generator CLI (WU-1018)
|
|
4
|
+
*
|
|
5
|
+
* Generates DORA/SPACE flow reports from telemetry and WU data.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* pnpm flow:report # Default: last 7 days, JSON output
|
|
9
|
+
* pnpm flow:report --days 30 # Last 30 days
|
|
10
|
+
* pnpm flow:report --format table # Table output
|
|
11
|
+
* pnpm flow:report --start 2026-01-01 --end 2026-01-15
|
|
12
|
+
*
|
|
13
|
+
* @module flow-report
|
|
14
|
+
* @see {@link @lumenflow/metrics/flow/generate-flow-report}
|
|
15
|
+
*/
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import fg from 'fast-glob';
|
|
20
|
+
import { parse as parseYaml } from 'yaml';
|
|
21
|
+
import { Command } from 'commander';
|
|
22
|
+
import { generateFlowReport, TELEMETRY_PATHS, } from '@lumenflow/metrics';
|
|
23
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
24
|
+
/** Log prefix for console output */
|
|
25
|
+
const LOG_PREFIX = '[flow:report]';
|
|
26
|
+
/** Default report window in days */
|
|
27
|
+
const DEFAULT_DAYS = 7;
|
|
28
|
+
/** Output format options */
|
|
29
|
+
const OUTPUT_FORMATS = {
|
|
30
|
+
JSON: 'json',
|
|
31
|
+
TABLE: 'table',
|
|
32
|
+
};
|
|
33
|
+
/** WU directory relative to repo root */
|
|
34
|
+
const WU_DIR = 'docs/04-operations/tasks/wu';
|
|
35
|
+
/**
|
|
36
|
+
* Parse command line arguments
|
|
37
|
+
*/
|
|
38
|
+
function parseArgs() {
|
|
39
|
+
const program = new Command()
|
|
40
|
+
.name('flow-report')
|
|
41
|
+
.description('Generate DORA/SPACE flow report from telemetry and WU data')
|
|
42
|
+
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
43
|
+
.option('--end <date>', 'End date (YYYY-MM-DD), defaults to today')
|
|
44
|
+
.option('--days <number>', `Days to report (default: ${DEFAULT_DAYS})`, String(DEFAULT_DAYS))
|
|
45
|
+
.option('--format <type>', `Output format: json, table (default: json)`, OUTPUT_FORMATS.JSON)
|
|
46
|
+
.exitOverride();
|
|
47
|
+
try {
|
|
48
|
+
program.parse(process.argv);
|
|
49
|
+
return program.opts();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const error = err;
|
|
53
|
+
if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Calculate date range from options
|
|
61
|
+
*/
|
|
62
|
+
function calculateDateRange(opts) {
|
|
63
|
+
const end = opts.end ? new Date(opts.end) : new Date();
|
|
64
|
+
end.setHours(23, 59, 59, 999);
|
|
65
|
+
let start;
|
|
66
|
+
if (opts.start) {
|
|
67
|
+
start = new Date(opts.start);
|
|
68
|
+
start.setHours(0, 0, 0, 0);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const days = parseInt(opts.days ?? String(DEFAULT_DAYS), 10);
|
|
72
|
+
start = new Date(end);
|
|
73
|
+
start.setDate(start.getDate() - days);
|
|
74
|
+
start.setHours(0, 0, 0, 0);
|
|
75
|
+
}
|
|
76
|
+
return { start, end };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read NDJSON file and parse events
|
|
80
|
+
*/
|
|
81
|
+
async function readNDJSON(filePath) {
|
|
82
|
+
if (!existsSync(filePath)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const content = await readFile(filePath, { encoding: 'utf-8' });
|
|
86
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
87
|
+
const events = [];
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
try {
|
|
90
|
+
events.push(JSON.parse(line));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Skip invalid JSON lines
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return events;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Transform raw gate event to GateTelemetryEvent type
|
|
100
|
+
*/
|
|
101
|
+
function transformGateEvent(raw) {
|
|
102
|
+
if (typeof raw.timestamp !== 'string' ||
|
|
103
|
+
typeof raw.gate_name !== 'string' ||
|
|
104
|
+
typeof raw.passed !== 'boolean' ||
|
|
105
|
+
typeof raw.duration_ms !== 'number') {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
timestamp: raw.timestamp,
|
|
110
|
+
wuId: raw.wu_id ?? null,
|
|
111
|
+
lane: raw.lane ?? null,
|
|
112
|
+
gateName: raw.gate_name,
|
|
113
|
+
passed: raw.passed,
|
|
114
|
+
durationMs: raw.duration_ms,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Transform raw LLM event to LLMTelemetryEvent type
|
|
119
|
+
*/
|
|
120
|
+
function transformLLMEvent(raw) {
|
|
121
|
+
if (typeof raw.timestamp !== 'string' ||
|
|
122
|
+
typeof raw.event_type !== 'string' ||
|
|
123
|
+
typeof raw.classification_type !== 'string') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const eventType = raw.event_type;
|
|
127
|
+
if (eventType !== 'llm.classification.start' &&
|
|
128
|
+
eventType !== 'llm.classification.complete' &&
|
|
129
|
+
eventType !== 'llm.classification.error') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
timestamp: raw.timestamp,
|
|
134
|
+
eventType,
|
|
135
|
+
classificationType: raw.classification_type,
|
|
136
|
+
durationMs: raw.duration_ms,
|
|
137
|
+
tokensUsed: raw.tokens_used,
|
|
138
|
+
estimatedCostUsd: raw.estimated_cost_usd,
|
|
139
|
+
confidence: raw.confidence,
|
|
140
|
+
fallbackUsed: raw.fallback_used,
|
|
141
|
+
fallbackReason: raw.fallback_reason,
|
|
142
|
+
errorType: raw.error_type,
|
|
143
|
+
errorMessage: raw.error_message,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Load gate telemetry events from file
|
|
148
|
+
*/
|
|
149
|
+
async function loadGateEvents(baseDir) {
|
|
150
|
+
const gatesPath = join(baseDir, TELEMETRY_PATHS.GATES);
|
|
151
|
+
const rawEvents = await readNDJSON(gatesPath);
|
|
152
|
+
return rawEvents.map(transformGateEvent).filter((e) => e !== null);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Load LLM telemetry events from file
|
|
156
|
+
*/
|
|
157
|
+
async function loadLLMEvents(baseDir) {
|
|
158
|
+
const llmPath = join(baseDir, TELEMETRY_PATHS.LLM_CLASSIFICATION);
|
|
159
|
+
const rawEvents = await readNDJSON(llmPath);
|
|
160
|
+
return rawEvents.map(transformLLMEvent).filter((e) => e !== null);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Load completed WUs from YAML files
|
|
164
|
+
*/
|
|
165
|
+
async function loadCompletedWUs(baseDir, start, end) {
|
|
166
|
+
const wuDir = join(baseDir, WU_DIR);
|
|
167
|
+
const wuFiles = await fg('WU-*.yaml', { cwd: wuDir, absolute: true });
|
|
168
|
+
const completedWUs = [];
|
|
169
|
+
for (const file of wuFiles) {
|
|
170
|
+
try {
|
|
171
|
+
const content = await readFile(file, { encoding: 'utf-8' });
|
|
172
|
+
const wu = parseYaml(content);
|
|
173
|
+
if (wu.status !== 'done' || !wu.completed_at) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const completedAt = new Date(wu.completed_at);
|
|
177
|
+
if (completedAt < start || completedAt > end) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
completedWUs.push({
|
|
181
|
+
id: wu.id,
|
|
182
|
+
title: wu.title,
|
|
183
|
+
lane: wu.lane,
|
|
184
|
+
status: 'done',
|
|
185
|
+
claimedAt: wu.claimed_at ? new Date(wu.claimed_at) : undefined,
|
|
186
|
+
completedAt,
|
|
187
|
+
cycleTimeHours: calculateCycleTime(wu),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Skip invalid WU files
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return completedWUs;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Calculate cycle time in hours from WU data
|
|
198
|
+
*/
|
|
199
|
+
function calculateCycleTime(wu) {
|
|
200
|
+
if (!wu.claimed_at || !wu.completed_at) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const claimed = new Date(wu.claimed_at);
|
|
204
|
+
const completed = new Date(wu.completed_at);
|
|
205
|
+
const diffMs = completed.getTime() - claimed.getTime();
|
|
206
|
+
const diffHours = diffMs / (1000 * 60 * 60);
|
|
207
|
+
return Math.round(diffHours * 10) / 10; // Round to 1 decimal
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Filter events by date range
|
|
211
|
+
*/
|
|
212
|
+
function filterByDateRange(events, start, end) {
|
|
213
|
+
return events.filter((event) => {
|
|
214
|
+
const eventDate = new Date(event.timestamp);
|
|
215
|
+
return eventDate >= start && eventDate <= end;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Format report as table output
|
|
220
|
+
*/
|
|
221
|
+
function formatAsTable(report) {
|
|
222
|
+
const lines = [];
|
|
223
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
224
|
+
lines.push(` FLOW REPORT: ${report.range.start} to ${report.range.end}`);
|
|
225
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
226
|
+
lines.push('');
|
|
227
|
+
// Gates section
|
|
228
|
+
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
|
229
|
+
lines.push('│ GATES │');
|
|
230
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
231
|
+
lines.push(`│ Pass Rate: ${report.gates.passRate}% | Total: ${report.gates.total} | P95: ${report.gates.p95}ms │`);
|
|
232
|
+
lines.push(`│ Passed: ${report.gates.passed} | Failed: ${report.gates.failed}`);
|
|
233
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
234
|
+
lines.push('│ By Name: │');
|
|
235
|
+
for (const [name, stats] of Object.entries(report.gates.byName)) {
|
|
236
|
+
lines.push(`│ ${name.padEnd(20)} ${stats.passRate}% (${stats.passed}/${stats.total})`);
|
|
237
|
+
}
|
|
238
|
+
lines.push('└─────────────────────────────────────────────────────────────┘');
|
|
239
|
+
lines.push('');
|
|
240
|
+
// WUs section
|
|
241
|
+
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
|
242
|
+
lines.push('│ WORK UNITS │');
|
|
243
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
244
|
+
lines.push(`│ Completed: ${report.wus.completed}`);
|
|
245
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
246
|
+
for (const wu of report.wus.list.slice(0, 10)) {
|
|
247
|
+
lines.push(`│ ${wu.wuId.padEnd(8)} ${wu.completedDate} ${wu.lane.padEnd(15)} ${wu.title.slice(0, 25)}`);
|
|
248
|
+
}
|
|
249
|
+
if (report.wus.list.length > 10) {
|
|
250
|
+
lines.push(`│ ... and ${report.wus.list.length - 10} more`);
|
|
251
|
+
}
|
|
252
|
+
lines.push('└─────────────────────────────────────────────────────────────┘');
|
|
253
|
+
lines.push('');
|
|
254
|
+
// LLM section
|
|
255
|
+
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
|
256
|
+
lines.push('│ LLM CLASSIFICATION │');
|
|
257
|
+
lines.push('├─────────────────────────────────────────────────────────────┤');
|
|
258
|
+
lines.push(`│ Total: ${report.llm.totalClassifications} | Error Rate: ${report.llm.errorRate}%`);
|
|
259
|
+
lines.push(`│ Avg Latency: ${report.llm.avgLatencyMs}ms | P95: ${report.llm.p95LatencyMs}ms`);
|
|
260
|
+
lines.push(`│ Tokens: ${report.llm.totalTokens} | Cost: $${report.llm.totalCostUsd.toFixed(4)}`);
|
|
261
|
+
lines.push('└─────────────────────────────────────────────────────────────┘');
|
|
262
|
+
return lines.join('\n');
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Main function
|
|
266
|
+
*/
|
|
267
|
+
async function main() {
|
|
268
|
+
const opts = parseArgs();
|
|
269
|
+
const baseDir = process.cwd();
|
|
270
|
+
console.log(`${LOG_PREFIX} Generating flow report...`);
|
|
271
|
+
const { start, end } = calculateDateRange(opts);
|
|
272
|
+
console.log(`${LOG_PREFIX} Date range: ${start.toISOString().split('T')[0]} to ${end.toISOString().split('T')[0]}`);
|
|
273
|
+
// Load telemetry and WU data
|
|
274
|
+
const [gateEvents, llmEvents, completedWUs] = await Promise.all([
|
|
275
|
+
loadGateEvents(baseDir),
|
|
276
|
+
loadLLMEvents(baseDir),
|
|
277
|
+
loadCompletedWUs(baseDir, start, end),
|
|
278
|
+
]);
|
|
279
|
+
// Filter events by date range
|
|
280
|
+
const filteredGateEvents = filterByDateRange(gateEvents, start, end);
|
|
281
|
+
const filteredLLMEvents = filterByDateRange(llmEvents, start, end);
|
|
282
|
+
console.log(`${LOG_PREFIX} Found ${filteredGateEvents.length} gate events`);
|
|
283
|
+
console.log(`${LOG_PREFIX} Found ${filteredLLMEvents.length} LLM events`);
|
|
284
|
+
console.log(`${LOG_PREFIX} Found ${completedWUs.length} completed WUs`);
|
|
285
|
+
// Generate report
|
|
286
|
+
const input = {
|
|
287
|
+
gateEvents: filteredGateEvents,
|
|
288
|
+
llmEvents: filteredLLMEvents,
|
|
289
|
+
completedWUs,
|
|
290
|
+
dateRange: {
|
|
291
|
+
start: start.toISOString().split('T')[0],
|
|
292
|
+
end: end.toISOString().split('T')[0],
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
const report = generateFlowReport(input);
|
|
296
|
+
// Output report
|
|
297
|
+
console.log('');
|
|
298
|
+
if (opts.format === OUTPUT_FORMATS.TABLE) {
|
|
299
|
+
console.log(formatAsTable(report));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.log(JSON.stringify(report, null, 2));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Guard main() for testability (WU-1366)
|
|
306
|
+
import { fileURLToPath } from 'node:url';
|
|
307
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
308
|
+
main().catch((err) => {
|
|
309
|
+
die(`Flow report failed: ${err.message}`);
|
|
310
|
+
});
|
|
311
|
+
}
|
package/dist/gates.js
CHANGED
|
@@ -45,7 +45,7 @@ import path from 'node:path';
|
|
|
45
45
|
import { emitGateEvent, getCurrentWU, getCurrentLane } from '@lumenflow/core/dist/telemetry.js';
|
|
46
46
|
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
47
47
|
import { getChangedLintableFiles, convertToPackageRelativePaths, } from '@lumenflow/core/dist/incremental-lint.js';
|
|
48
|
-
import {
|
|
48
|
+
import { isCodeFilePath } from '@lumenflow/core/dist/incremental-test.js';
|
|
49
49
|
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
50
50
|
import { runCoverageGate, COVERAGE_GATE_MODES } from '@lumenflow/core/dist/coverage-gate.js';
|
|
51
51
|
import { buildGatesLogPath, shouldUseGatesAgentMode, updateGatesLatestSymlink, } from '@lumenflow/core/dist/gates-agent-mode.js';
|
|
@@ -184,6 +184,15 @@ async function runIncrementalLint({ agentLog, } = {}) {
|
|
|
184
184
|
}
|
|
185
185
|
writeSync(agentLog.logFd, `${line}\n`);
|
|
186
186
|
};
|
|
187
|
+
// WU-1006: Skip incremental lint if apps/web doesn't exist (repo-agnostic)
|
|
188
|
+
const webDir = path.join(process.cwd(), DIRECTORIES.APPS_WEB);
|
|
189
|
+
try {
|
|
190
|
+
await access(webDir);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
logLine('\n> ESLint (incremental) skipped (apps/web not present)\n');
|
|
194
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
195
|
+
}
|
|
187
196
|
try {
|
|
188
197
|
// Check if we're on main branch
|
|
189
198
|
const git = getGitForCwd();
|
|
@@ -298,15 +307,11 @@ async function runChangedTests({ agentLog, } = {}) {
|
|
|
298
307
|
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
299
308
|
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
300
309
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
logLine('\n> Vitest (changed: turbo --affected)\n');
|
|
308
|
-
const result = run(pnpmCmd('turbo', 'run', 'test:changed', '--affected'), { agentLog });
|
|
309
|
-
return { ...result, duration: Date.now() - start, isIncremental: true };
|
|
310
|
+
// WU-1006: Use turbo for tests (repo-agnostic)
|
|
311
|
+
// Previously used --project tools and test:changed which don't exist in all repos
|
|
312
|
+
logLine('\n> Running tests (turbo run test)\n');
|
|
313
|
+
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
314
|
+
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
310
315
|
}
|
|
311
316
|
catch (error) {
|
|
312
317
|
console.error('⚠️ Changed tests failed, falling back to full suite:', error.message);
|
|
@@ -359,6 +364,15 @@ async function runSafetyCriticalTests({ agentLog, } = {}) {
|
|
|
359
364
|
}
|
|
360
365
|
writeSync(agentLog.logFd, `${line}\n`);
|
|
361
366
|
};
|
|
367
|
+
// WU-1006: Skip safety-critical tests if apps/web doesn't exist (repo-agnostic)
|
|
368
|
+
const webDir = path.join(process.cwd(), DIRECTORIES.APPS_WEB);
|
|
369
|
+
try {
|
|
370
|
+
await access(webDir);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
logLine('\n> Safety-critical tests skipped (apps/web not present)\n');
|
|
374
|
+
return { ok: true, duration: Date.now() - start, testCount: 0 };
|
|
375
|
+
}
|
|
362
376
|
try {
|
|
363
377
|
logLine('\n> Safety-critical tests (always run)\n');
|
|
364
378
|
logLine(`Test files: ${SAFETY_CRITICAL_TEST_FILES.length} files\n`);
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file init.ts
|
|
3
|
+
* LumenFlow project scaffolding command (WU-1005)
|
|
4
|
+
* WU-1006: Library-First - use js-yaml.dump() for YAML generation
|
|
5
|
+
*
|
|
6
|
+
* Scaffolds new projects with:
|
|
7
|
+
* - .lumenflow.yaml configuration
|
|
8
|
+
* - CLAUDE.md development guide
|
|
9
|
+
* - AGENTS.md agent context
|
|
10
|
+
* - .beacon/stamps directory
|
|
11
|
+
* - docs/04-operations/tasks/wu directory
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import yaml from 'js-yaml';
|
|
16
|
+
/**
|
|
17
|
+
* Default LumenFlow configuration object
|
|
18
|
+
* WU-1006: Use structured object + js-yaml instead of string template
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_LUMENFLOW_CONFIG = {
|
|
21
|
+
version: '1.0',
|
|
22
|
+
lanes: [
|
|
23
|
+
{ name: 'Core', paths: ['src/core/**'] },
|
|
24
|
+
{ name: 'API', paths: ['src/api/**'] },
|
|
25
|
+
{ name: 'UI', paths: ['src/ui/**', 'src/components/**'] },
|
|
26
|
+
{ name: 'Infrastructure', paths: ['infra/**', '.github/**'] },
|
|
27
|
+
{ name: 'Documentation', paths: ['docs/**', '*.md'] },
|
|
28
|
+
],
|
|
29
|
+
gates: {
|
|
30
|
+
format: true,
|
|
31
|
+
lint: true,
|
|
32
|
+
typecheck: true,
|
|
33
|
+
test: true,
|
|
34
|
+
},
|
|
35
|
+
memory: {
|
|
36
|
+
checkpoint_interval: 30,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Generate YAML configuration with header comment
|
|
41
|
+
*/
|
|
42
|
+
function generateLumenflowYaml() {
|
|
43
|
+
const header = `# LumenFlow Configuration
|
|
44
|
+
# Generated by: lumenflow init
|
|
45
|
+
# Customize lanes based on your project structure
|
|
46
|
+
|
|
47
|
+
`;
|
|
48
|
+
return header + yaml.dump(DEFAULT_LUMENFLOW_CONFIG, { lineWidth: -1, quotingType: "'" });
|
|
49
|
+
}
|
|
50
|
+
// Template for CLAUDE.md
|
|
51
|
+
const CLAUDE_MD_TEMPLATE = `# Development Guide
|
|
52
|
+
|
|
53
|
+
**Last updated:** ${new Date().toISOString().split('T')[0]}
|
|
54
|
+
|
|
55
|
+
This project uses LumenFlow workflow for all changes.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
\`\`\`bash
|
|
62
|
+
# 1. Create a WU
|
|
63
|
+
pnpm wu:create --id WU-XXXX --lane <Lane> --title "Title"
|
|
64
|
+
|
|
65
|
+
# 2. Edit WU spec with acceptance criteria, then claim:
|
|
66
|
+
pnpm wu:claim --id WU-XXXX --lane <Lane>
|
|
67
|
+
cd worktrees/<lane>-wu-xxxx
|
|
68
|
+
|
|
69
|
+
# 3. Implement in worktree
|
|
70
|
+
|
|
71
|
+
# 4. Run gates
|
|
72
|
+
pnpm gates
|
|
73
|
+
|
|
74
|
+
# 5. Complete (from main)
|
|
75
|
+
cd /path/to/main
|
|
76
|
+
pnpm wu:done --id WU-XXXX
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Core Principles
|
|
82
|
+
|
|
83
|
+
1. **TDD**: Failing test → implementation → passing test (≥90% coverage on new code)
|
|
84
|
+
2. **Library-First**: Search existing libraries before custom code
|
|
85
|
+
3. **DRY/SOLID/KISS/YAGNI**: No magic numbers, no hardcoded strings
|
|
86
|
+
4. **Worktree Discipline**: After \`wu:claim\`, work ONLY in the worktree
|
|
87
|
+
5. **Gates Before Done**: All gates must pass before \`wu:done\`
|
|
88
|
+
6. **Do Not Bypass Hooks**: No \`--no-verify\`, fix issues properly
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Definition of Done
|
|
93
|
+
|
|
94
|
+
- Acceptance criteria satisfied
|
|
95
|
+
- Gates green (\`pnpm gates\`)
|
|
96
|
+
- WU YAML status = \`done\`
|
|
97
|
+
- \`.beacon/stamps/WU-<id>.done\` exists
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Commands Reference
|
|
102
|
+
|
|
103
|
+
| Command | Description |
|
|
104
|
+
| --------------------- | ----------------------------------- |
|
|
105
|
+
| \`pnpm wu:create\` | Create new WU spec |
|
|
106
|
+
| \`pnpm wu:claim\` | Claim WU and create worktree |
|
|
107
|
+
| \`pnpm wu:done\` | Complete WU (merge, stamp, cleanup) |
|
|
108
|
+
| \`pnpm gates\` | Run quality gates |
|
|
109
|
+
| \`pnpm mem:checkpoint\` | Save memory checkpoint |
|
|
110
|
+
`;
|
|
111
|
+
// Template for AGENTS.md
|
|
112
|
+
const AGENTS_MD_TEMPLATE = `# Agent Context
|
|
113
|
+
|
|
114
|
+
This project uses LumenFlow workflow. Before starting any work:
|
|
115
|
+
|
|
116
|
+
## Context Loading Protocol
|
|
117
|
+
|
|
118
|
+
1. Read \`CLAUDE.md\` for workflow fundamentals
|
|
119
|
+
2. Read \`README.md\` for project structure
|
|
120
|
+
3. Read the specific WU YAML file from \`docs/04-operations/tasks/wu/\`
|
|
121
|
+
|
|
122
|
+
## Critical Rules
|
|
123
|
+
|
|
124
|
+
### Worktree Discipline (IMMUTABLE LAW)
|
|
125
|
+
|
|
126
|
+
After claiming a WU, you MUST work in its worktree:
|
|
127
|
+
|
|
128
|
+
\`\`\`bash
|
|
129
|
+
# 1. Claim creates worktree
|
|
130
|
+
pnpm wu:claim --id WU-XXX --lane <lane>
|
|
131
|
+
|
|
132
|
+
# 2. IMMEDIATELY cd to worktree
|
|
133
|
+
cd worktrees/<lane>-wu-xxx
|
|
134
|
+
|
|
135
|
+
# 3. ALL work happens here (edits, git add/commit/push, tests, gates)
|
|
136
|
+
|
|
137
|
+
# 4. Return to main ONLY to complete
|
|
138
|
+
cd ../../
|
|
139
|
+
pnpm wu:done --id WU-XXX
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
Main checkout becomes read-only after claim. Hooks will block WU commits from main.
|
|
143
|
+
|
|
144
|
+
### Never Bypass Hooks
|
|
145
|
+
|
|
146
|
+
If a git hook fails (pre-commit, commit-msg):
|
|
147
|
+
|
|
148
|
+
1. Read the error message (shows which gate failed)
|
|
149
|
+
2. Fix the underlying issue (format/lint/type errors)
|
|
150
|
+
3. Re-run the commit
|
|
151
|
+
|
|
152
|
+
**NEVER use \`--no-verify\` or \`--no-gpg-sign\` to bypass hooks.**
|
|
153
|
+
|
|
154
|
+
### WIP = 1 Per Lane
|
|
155
|
+
|
|
156
|
+
Only ONE work unit can be "in progress" per lane at any time.
|
|
157
|
+
|
|
158
|
+
## Coding Standards
|
|
159
|
+
|
|
160
|
+
- **TDD:** Tests first, ≥90% coverage on new application code
|
|
161
|
+
- **Conventional commits:** \`type: summary\` (e.g., \`feat: add feature\`, \`fix: resolve bug\`)
|
|
162
|
+
- **Documentation:** Concise, clear Markdown
|
|
163
|
+
|
|
164
|
+
## Forbidden Git Commands (NEVER RUN on main)
|
|
165
|
+
|
|
166
|
+
\`\`\`bash
|
|
167
|
+
git reset --hard # Data loss
|
|
168
|
+
git stash # Hides work
|
|
169
|
+
git clean -fd # Deletes files
|
|
170
|
+
git push --force # History rewrite
|
|
171
|
+
--no-verify # Bypasses safety checks
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
**Where allowed:** Inside your worktree on a lane branch (safe, isolated).
|
|
175
|
+
`;
|
|
176
|
+
/**
|
|
177
|
+
* Scaffold a new LumenFlow project
|
|
178
|
+
*/
|
|
179
|
+
export async function scaffoldProject(targetDir, options) {
|
|
180
|
+
const result = {
|
|
181
|
+
created: [],
|
|
182
|
+
skipped: [],
|
|
183
|
+
};
|
|
184
|
+
// Ensure target directory exists
|
|
185
|
+
if (!fs.existsSync(targetDir)) {
|
|
186
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
// Create .lumenflow.yaml (WU-1006: use js-yaml.dump() instead of string template)
|
|
189
|
+
await createFile(path.join(targetDir, '.lumenflow.yaml'), generateLumenflowYaml(), options.force, result);
|
|
190
|
+
// Create CLAUDE.md
|
|
191
|
+
await createFile(path.join(targetDir, 'CLAUDE.md'), CLAUDE_MD_TEMPLATE, options.force, result);
|
|
192
|
+
// Create AGENTS.md
|
|
193
|
+
await createFile(path.join(targetDir, 'AGENTS.md'), AGENTS_MD_TEMPLATE, options.force, result);
|
|
194
|
+
// Create .beacon/stamps directory
|
|
195
|
+
const beaconStampsDir = path.join(targetDir, '.beacon', 'stamps');
|
|
196
|
+
if (!fs.existsSync(beaconStampsDir)) {
|
|
197
|
+
fs.mkdirSync(beaconStampsDir, { recursive: true });
|
|
198
|
+
result.created.push('.beacon/stamps');
|
|
199
|
+
}
|
|
200
|
+
// Create docs/04-operations/tasks/wu directory with .gitkeep
|
|
201
|
+
const wuDir = path.join(targetDir, 'docs', '04-operations', 'tasks', 'wu');
|
|
202
|
+
if (!fs.existsSync(wuDir)) {
|
|
203
|
+
fs.mkdirSync(wuDir, { recursive: true });
|
|
204
|
+
result.created.push('docs/04-operations/tasks/wu');
|
|
205
|
+
}
|
|
206
|
+
// Create .gitkeep in WU directory
|
|
207
|
+
const gitkeepPath = path.join(wuDir, '.gitkeep');
|
|
208
|
+
if (!fs.existsSync(gitkeepPath)) {
|
|
209
|
+
fs.writeFileSync(gitkeepPath, '');
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Create a file, respecting force option
|
|
215
|
+
*/
|
|
216
|
+
async function createFile(filePath, content, force, result) {
|
|
217
|
+
const relativePath = path.basename(filePath);
|
|
218
|
+
if (fs.existsSync(filePath) && !force) {
|
|
219
|
+
result.skipped.push(relativePath);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Ensure parent directory exists
|
|
223
|
+
const parentDir = path.dirname(filePath);
|
|
224
|
+
if (!fs.existsSync(parentDir)) {
|
|
225
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
fs.writeFileSync(filePath, content);
|
|
228
|
+
result.created.push(relativePath);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* CLI entry point
|
|
232
|
+
*/
|
|
233
|
+
export async function main() {
|
|
234
|
+
const args = process.argv.slice(2);
|
|
235
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
236
|
+
const targetDir = process.cwd();
|
|
237
|
+
console.log('[lumenflow init] Scaffolding LumenFlow project...');
|
|
238
|
+
const result = await scaffoldProject(targetDir, { force });
|
|
239
|
+
if (result.created.length > 0) {
|
|
240
|
+
console.log('\nCreated:');
|
|
241
|
+
result.created.forEach((f) => console.log(` ✅ ${f}`));
|
|
242
|
+
}
|
|
243
|
+
if (result.skipped.length > 0) {
|
|
244
|
+
console.log('\nSkipped (already exists, use --force to overwrite):');
|
|
245
|
+
result.skipped.forEach((f) => console.log(` ⏭️ ${f}`));
|
|
246
|
+
}
|
|
247
|
+
console.log('\n[lumenflow init] Done! Next steps:');
|
|
248
|
+
console.log(' 1. Edit .lumenflow.yaml to match your project structure');
|
|
249
|
+
console.log(' 2. Review CLAUDE.md and AGENTS.md templates');
|
|
250
|
+
console.log(' 3. Run: pnpm wu:create --id WU-0001 --lane <lane> --title "First WU"');
|
|
251
|
+
}
|