@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.
@@ -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 { buildVitestChangedArgs, isCodeFilePath } from '@lumenflow/core/dist/incremental-test.js';
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
- logLine('\n> Vitest (changed: tools project)\n');
302
- const toolsArgs = ['--project', 'tools', ...buildVitestChangedArgs()];
303
- const toolsResult = run(pnpmCmd('vitest', ...toolsArgs), { agentLog });
304
- if (!toolsResult.ok) {
305
- return { ...toolsResult, duration: Date.now() - start, isIncremental: true };
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
+ }