@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/init.js ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * @file init.ts
3
+ * LumenFlow project scaffolding command (WU-1045)
4
+ * WU-1006: Library-First - use core defaults for config generation
5
+ * WU-1028: Vendor-agnostic core + vendor overlays
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import * as yaml from 'yaml';
10
+ import { getDefaultConfig } from '@lumenflow/core';
11
+ const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
12
+ const FRAMEWORK_HINT_FILE = '.lumenflow.framework.yaml';
13
+ const LUMENFLOW_DIR = '.lumenflow';
14
+ const LUMENFLOW_AGENTS_DIR = `${LUMENFLOW_DIR}/agents`;
15
+ const CLAUDE_DIR = '.claude';
16
+ const CLAUDE_AGENTS_DIR = path.join(CLAUDE_DIR, 'agents');
17
+ /**
18
+ * Generate YAML configuration with header comment
19
+ */
20
+ function generateLumenflowConfigYaml() {
21
+ const header = `# LumenFlow Configuration\n# Generated by: lumenflow init\n# Customize paths based on your project structure\n\n`;
22
+ const config = getDefaultConfig();
23
+ config.directories.agentsDir = LUMENFLOW_AGENTS_DIR;
24
+ return header + yaml.stringify(config);
25
+ }
26
+ /**
27
+ * Get current date in YYYY-MM-DD format
28
+ */
29
+ function getCurrentDate() {
30
+ return new Date().toISOString().split('T')[0];
31
+ }
32
+ /**
33
+ * Normalize a framework name into display + slug
34
+ */
35
+ function normalizeFrameworkName(framework) {
36
+ const name = framework.trim();
37
+ const slug = name
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9-_]+/g, '-')
40
+ .replace(/^-+|-+$/g, '');
41
+ if (!slug) {
42
+ throw new Error(`Invalid framework name: "${framework}"`);
43
+ }
44
+ return { name, slug };
45
+ }
46
+ /**
47
+ * Process template content by replacing placeholders
48
+ */
49
+ function processTemplate(content, tokens) {
50
+ let output = content;
51
+ for (const [key, value] of Object.entries(tokens)) {
52
+ output = output.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
53
+ }
54
+ return output;
55
+ }
56
+ function getRelativePath(targetDir, filePath) {
57
+ return path.relative(targetDir, filePath).split(path.sep).join('/');
58
+ }
59
+ // Template for LUMENFLOW.md (main entry point)
60
+ const LUMENFLOW_MD_TEMPLATE = `# LumenFlow Workflow Guide\n\n**Last updated:** {{DATE}}\n\nLumenFlow is a vendor-agnostic workflow framework for AI-native software development.\n\n---\n\n## Critical Rule: ALWAYS Run wu:done\n\n**After completing work on a WU, you MUST run \`pnpm wu:done --id WU-XXXX\` from the main checkout.**\n\nThis is the single most forgotten step. Do NOT:\n- Write "To Complete: pnpm wu:done" and stop\n- Ask if you should run wu:done\n- Forget to run wu:done\n\n**DO**: Run \`pnpm wu:done --id WU-XXXX\` immediately after gates pass.\n\n---\n\n## Quick Start\n\n\`\`\`bash\n# 1. Create a WU\npnpm wu:create --id WU-XXXX --lane <Lane> --title "Title"\n\n# 2. Edit WU spec with acceptance criteria, then claim:\npnpm wu:claim --id WU-XXXX --lane <Lane>\ncd worktrees/<lane>-wu-xxxx\n\n# 3. Implement in worktree\n\n# 4. Run gates\npnpm gates --docs-only # for docs changes\npnpm gates # for code changes\n\n# 5. Complete (from main checkout)\ncd {{PROJECT_ROOT}}\npnpm wu:done --id WU-XXXX\n\`\`\`\n\n---\n\n## Core Principles\n\n1. **TDD**: Failing test -> implementation -> passing test (>=90% coverage on new code)\n2. **Library-First**: Search existing libraries before custom code\n3. **DRY/SOLID/KISS/YAGNI**: No magic numbers, no hardcoded strings\n4. **Worktree Discipline**: After \`wu:claim\`, work ONLY in the worktree\n5. **Gates Before Done**: All gates must pass before \`wu:done\`\n6. **Do Not Bypass Hooks**: No \`--no-verify\`, fix issues properly\n7. **Always wu:done**: Complete every WU by running \`pnpm wu:done\`\n\n---\n\n## Documentation Structure\n\n### Core (Vendor-Agnostic)\n\n- **LUMENFLOW.md** - This file, main entry point\n- **.lumenflow/constraints.md** - Non-negotiable workflow constraints\n- **.lumenflow/agents/** - Agent instructions (vendor-agnostic)\n- **.lumenflow.config.yaml** - Workflow configuration\n\n### Optional Overlays\n\n- **CLAUDE.md + .claude/agents/** - Claude Code overlay (auto if Claude Code detected)\n- **docs/04-operations/tasks/** - Task boards and WU storage (\`lumenflow init --full\`)\n- **docs/04-operations/_frameworks/<framework>/** - Framework overlay docs (\`lumenflow init --framework <name>\`)\n- **.lumenflow.framework.yaml** - Framework hint file (created with \`--framework\`)\n\n---\n\n## Worktree Discipline (IMMUTABLE LAW)\n\nAfter claiming a WU, you MUST work in its worktree:\n\n\`\`\`bash\n# 1. Claim creates worktree\npnpm wu:claim --id WU-XXX --lane <lane>\n\n# 2. IMMEDIATELY cd to worktree\ncd worktrees/<lane>-wu-xxx\n\n# 3. ALL work happens here\n\n# 4. Return to main ONLY to complete\ncd {{PROJECT_ROOT}}\npnpm wu:done --id WU-XXX\n\`\`\`\n\n---\n\n## Definition of Done\n\n- Acceptance criteria satisfied\n- Gates green (\`pnpm gates\` or \`pnpm gates --docs-only\`)\n- WU YAML status = \`done\`\n- \`wu:done\` has been run\n\n---\n\n## Commands Reference\n\n| Command | Description |\n| ----------------- | ----------------------------------- |\n| \`pnpm wu:create\` | Create new WU spec |\n| \`pnpm wu:claim\` | Claim WU and create worktree |\n| \`pnpm wu:done\` | Complete WU (merge, stamp, cleanup) |\n| \`pnpm gates\` | Run quality gates |\n\n---\n\n## Constraints\n\nSee [.lumenflow/constraints.md](.lumenflow/constraints.md) for the 6 non-negotiable rules.\n\n---\n\n## Agent Onboarding\n\n- Start with **CLAUDE.md** if present (Claude Code overlay).\n- Add vendor-agnostic guidance in **.lumenflow/agents/**.\n- Add framework-specific notes in **docs/04-operations/_frameworks/<framework>/**.\n`;
61
+ // Template for .lumenflow/constraints.md
62
+ const CONSTRAINTS_MD_TEMPLATE = `# LumenFlow Constraints Capsule\n\n**Version:** 1.0\n**Last updated:** {{DATE}}\n\n## The 6 Non-Negotiable Constraints\n\n### 1. Worktree Discipline and Git Safety\nWork only in worktrees, treat main as read-only, never run destructive git commands on main.\n\n### 2. WUs Are Specs, Not Code\nRespect code_paths boundaries, no feature creep, no code blocks in WU YAML files.\n\n### 3. Docs-Only vs Code WUs\nDocumentation WUs use \`--docs-only\` gates, code WUs run full gates.\n\n### 4. LLM-First, Zero-Fallback Inference\nUse LLMs for semantic tasks, fall back to safe defaults (never regex/keywords).\n\n### 5. Gates and Skip-Gates\nComplete via \`pnpm wu:done\`; skip-gates only for pre-existing failures with \`--reason\` and \`--fix-wu\`.\n\n### 6. Safety and Governance\nRespect privacy rules, approved sources, security policies; when uncertain, choose safer path.\n\n---\n\n## Mini Audit Checklist\n\nBefore running \`wu:done\`, verify:\n\n- [ ] Working in worktree (not main)\n- [ ] Only modified files in \`code_paths\`\n- [ ] Gates pass\n- [ ] No forbidden git commands used\n- [ ] Acceptance criteria satisfied\n\n---\n\n## Escalation Triggers\n\nStop and ask a human when:\n- Same error repeats 3 times\n- Auth or permissions changes required\n- PII/PHI/safety issues discovered\n- Cloud spend or secrets involved\n`;
63
+ // Template for root CLAUDE.md
64
+ const CLAUDE_MD_TEMPLATE = `# Claude Code Instructions\n\n**Last updated:** {{DATE}}\n\nThis project uses LumenFlow workflow. For workflow documentation, see [LUMENFLOW.md](LUMENFLOW.md).\n\n---\n\n## Quick Start\n\n\`\`\`bash\n# 1. Claim a WU\npnpm wu:claim --id WU-XXXX --lane <Lane>\ncd worktrees/<lane>-wu-xxxx\n\n# 2. Work in worktree, run gates\npnpm gates\n\n# 3. Complete (ALWAYS run this!)\ncd {{PROJECT_ROOT}}\npnpm wu:done --id WU-XXXX\n\`\`\`\n\n---\n\n## Critical: Always wu:done\n\nAfter completing work, ALWAYS run \`pnpm wu:done --id WU-XXXX\`.\n\nSee [LUMENFLOW.md](LUMENFLOW.md) for full workflow documentation.\n`;
65
+ // Template for .claude/settings.json
66
+ const CLAUDE_SETTINGS_TEMPLATE = `{
67
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
68
+ "permissions": {
69
+ "allow": [
70
+ "Bash",
71
+ "Read",
72
+ "Write",
73
+ "Edit",
74
+ "WebFetch",
75
+ "WebSearch"
76
+ ],
77
+ "deny": [
78
+ "Read(./.env)",
79
+ "Read(./.env.*)",
80
+ "Write(./.env*)",
81
+ "Bash(git reset --hard *)",
82
+ "Bash(git stash *)",
83
+ "Bash(git clean -fd *)",
84
+ "Bash(git push --force *)",
85
+ "Bash(git push -f *)",
86
+ "Bash(git commit --no-verify *)",
87
+ "Bash(HUSKY=0 *)",
88
+ "Bash(rm -rf /*)",
89
+ "Bash(sudo *)",
90
+ "Bash(git worktree remove *)",
91
+ "Bash(git worktree prune *)"
92
+ ]
93
+ }
94
+ }
95
+ `;
96
+ // Template for .cursor/rules.md
97
+ const CURSOR_RULES_TEMPLATE = `# Cursor Rules\n\nThis project uses LumenFlow workflow. See [LUMENFLOW.md](../LUMENFLOW.md).\n\n## Critical Rules\n\n1. **Always run wu:done** - After gates pass, run \`pnpm wu:done --id WU-XXX\`\n2. **Work in worktrees** - After \`wu:claim\`, work only in the worktree\n3. **Never bypass hooks** - No \`--no-verify\`\n4. **TDD** - Write tests first\n\n## Forbidden Commands\n\n- \`git reset --hard\`\n- \`git push --force\`\n- \`git stash\` (on main)\n- \`--no-verify\`\n`;
98
+ // Template for .aider.conf.yml
99
+ const AIDER_CONF_TEMPLATE = `# Aider Configuration for LumenFlow Projects\n# See LUMENFLOW.md for workflow documentation\n\nmodel: gpt-4-turbo\nauto-commits: false\ndirty-commits: false\n\nread:\n - LUMENFLOW.md\n - .lumenflow/constraints.md\n`;
100
+ // Template for docs/04-operations/tasks/backlog.md
101
+ const BACKLOG_TEMPLATE = `---\nsections:\n ready:\n heading: '## 🚀 Ready (pull from here)'\n insertion: after_heading_blank_line\n in_progress:\n heading: '## 🔧 In progress'\n insertion: after_heading_blank_line\n blocked:\n heading: '## ⛔ Blocked'\n insertion: after_heading_blank_line\n done:\n heading: '## ✅ Done'\n insertion: after_heading_blank_line\n---\n\n# Backlog (single source of truth)\n\n## 🚀 Ready (pull from here)\n\n(No items ready)\n\n## 🔧 In progress\n\n(No items in progress)\n\n## ⛔ Blocked\n\n(No items blocked)\n\n## ✅ Done\n\n(No items completed yet)\n`;
102
+ // Template for docs/04-operations/tasks/status.md
103
+ const STATUS_TEMPLATE = `# Status (active work)\n\n## In Progress\n\n(No items in progress)\n\n## Blocked\n\n(No items blocked)\n\n## Completed\n\n(No items completed yet)\n`;
104
+ // Template for docs/04-operations/tasks/templates/wu-template.yaml
105
+ const WU_TEMPLATE_YAML = `# Work Unit Template (LumenFlow WU Schema)\n#\n# Copy this template when creating new WUs. Fill in all required fields and\n# remove optional fields if not needed.\n#\n# If you used "lumenflow init --full", this template lives at:\n# docs/04-operations/tasks/templates/wu-template.yaml\n\n# Required: Unique work unit identifier (format: WU-NNN)\nid: WU-XXX\n\n# Required: Short, descriptive title (max 80 chars)\ntitle: 'Your WU title here'\n\n# Required: Lane (Parent: Sublane format)\nlane: 'Framework: CLI'\n\n# Required: Type of work\ntype: 'feature' # feature | bug | documentation | process | tooling | chore | refactor\n\n# Required: Current status\nstatus: 'ready' # ready | in_progress | blocked | done | cancelled\n\n# Required: Priority\npriority: P2 # P0 | P1 | P2 | P3\n\n# Required: Creation date (YYYY-MM-DD)\ncreated: {{DATE}}\n\n# Required: Owner/assignee (email)\nassigned_to: 'unassigned@example.com'\n\n# Required: Description\ndescription: |\n Context: ...\n Problem: ...\n Solution: ...\n\n# Required: Acceptance criteria (testable, binary)\nacceptance:\n - Criterion 1 (specific, measurable, testable)\n - Criterion 2 (binary pass/fail)\n - Documentation updated\n\n# Required: References to plans/specs (required for type: feature)\nspec_refs:\n - docs/04-operations/plans/WU-XXX-plan.md\n\n# Required: Code files changed or created (empty only for docs/process WUs)\ncode_paths:\n - path/to/file.ts\n\n# Required: Test paths (at least one of manual/unit/e2e/integration for non-doc WUs)\ntests:\n manual:\n - Manual test: Verify behavior\n unit:\n - path/to/test.test.ts\n e2e: []\n integration: []\n\n# Required: Exposure level\nexposure: 'backend-only' # ui | api | backend-only | documentation\n\n# Optional: User journey (recommended for ui/api)\n# user_journey: |\n# User navigates to ...\n# User performs ...\n\n# Optional: UI pairing WUs (for api exposure)\n# ui_pairing_wus:\n# - WU-1234\n\n# Optional: Navigation path (required when exposure=ui and no page file)\n# navigation_path: '/settings'\n\n# Required: Deliverable artifacts (stamps, docs, etc.)\nartifacts:\n - .beacon/stamps/WU-XXX.done\n\n# Optional: Dependencies (other WUs that must complete first)\ndependencies: []\n\n# Optional: Risks\nrisks:\n - Risk 1\n\n# Optional: Notes\nnotes: ''\n\n# Optional: Requires human review\nrequires_review: false\n\n# Optional: Claimed mode (worktree or branch-only)\n# Automatically set by wu:claim, usually don't need to specify\n# claimed_mode: worktree\n\n# Optional: Assigned to (email of current claimant)\n# Automatically set by wu:claim\n# assigned_to: engineer@example.com\n\n# Optional: Locked status (prevents concurrent edits)\n# Automatically set by wu:claim and wu:done\n# locked: false\n\n# Optional: Completion date (ISO 8601 format)\n# Automatically set by wu:done\n# completed: 2025-10-23\n\n# Optional: Completion notes (added by wu:done)\n# completion_notes: |\n# Additional notes added during wu:done.\n# Any deviations from original plan.\n# Lessons learned.\n\n# ============================================================================\n# GOVERNANCE BLOCK (WU Schema v2.0)\n# ============================================================================\n# Optional: COS governance rules that apply to this WU\n# Only include if this WU needs specific governance enforcement\n\n# governance:\n# # Rules that apply to this WU (evaluated during cos:gates)\n# rules:\n# - rule_id: UPAIN-01\n# satisfied: false # Initially false, set true when evidence provided\n# evidence:\n# - type: link\n# value: docs/product/voc/feature-user-pain.md\n# description: "Voice of Customer analysis showing user pain"\n# notes: |\n# VOC analysis shows 40% of support tickets request this feature.\n# Average time wasted: 15min/user/week.\n#\n# - rule_id: CASH-03\n# satisfied: false\n# evidence:\n# - type: link\n# value: docs/finance/spend-reviews/2025-10-cloud-infra.md\n# description: "Spend review for £1200/month cloud infrastructure"\n# - type: approval\n# value: owner@example.com\n# description: "Owner approval for spend commitment"\n# notes: |\n# New cloud infrastructure commitment: £1200/month for 12 months.\n# ROI: Reduces latency by 50%, improves user retention.\n#\n# # Gate checks (enforced by cos-gates.mjs)\n# gates:\n# narrative: "pending" # Status: pending, passed, skipped, failed\n# finance: "pending"\n#\n# # Exemptions (only if rule doesn't apply)\n# exemptions:\n# - rule_id: FAIR-01\n# reason: "No user-facing pricing changes in this WU"\n# approved_by: product-owner@example.com\n# approved_at: 2025-10-23\n\n# ============================================================================\n# USAGE NOTES\n# ============================================================================\n#\n# 1. Remove this entire governance block if no COS rules apply to your WU\n# 2. Only include rules that require enforcement (not all rules apply to all WUs)\n# 3. Evidence types: link:, metric:, screenshot:, approval:\n# 4. Gates are checked during wu:done (before merge)\n# 5. Exemptions require approval from rule owner\n#\n# For more details, see:\n# - docs/04-operations/_frameworks/cos/system-prompt-v1.3.md\n# - docs/04-operations/_frameworks/cos/evidence-format.md\n`;
106
+ // Template for .lumenflow.framework.yaml
107
+ const FRAMEWORK_HINT_TEMPLATE = `# LumenFlow Framework Hint\n# Generated by: lumenflow init --framework {{FRAMEWORK_NAME}}\n\nframework: "{{FRAMEWORK_NAME}}"\nslug: "{{FRAMEWORK_SLUG}}"\n`;
108
+ // Template for docs/04-operations/_frameworks/<framework>/README.md
109
+ const FRAMEWORK_OVERLAY_TEMPLATE = `# {{FRAMEWORK_NAME}} Framework Overlay\n\n**Last updated:** {{DATE}}\n\nThis overlay captures framework-specific conventions, constraints, and references for {{FRAMEWORK_NAME}} projects.\n\n## Scope\n\n- Project structure conventions\n- Framework-specific testing guidance\n- Common pitfalls and mitigations\n\n## References\n\n- Add official docs links here\n`;
110
+ /**
111
+ * Detect default client from environment
112
+ */
113
+ function detectDefaultClient() {
114
+ if (process.env.CLAUDE_PROJECT_DIR || process.env.CLAUDE_CODE) {
115
+ return 'claude-code';
116
+ }
117
+ return 'none';
118
+ }
119
+ /**
120
+ * Parse vendor flag from arguments
121
+ */
122
+ function parseVendorArg(args) {
123
+ const vendorIndex = args.findIndex((arg) => arg === '--vendor');
124
+ if (vendorIndex !== -1 && args[vendorIndex + 1]) {
125
+ const vendor = args[vendorIndex + 1].toLowerCase();
126
+ if (['claude', 'cursor', 'aider', 'all', 'none'].includes(vendor)) {
127
+ return vendor;
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ /**
133
+ * Parse framework flag from arguments
134
+ */
135
+ function parseFrameworkArg(args) {
136
+ const frameworkArg = args.find((arg) => arg.startsWith('--framework='));
137
+ if (frameworkArg) {
138
+ const [, value] = frameworkArg.split('=', 2);
139
+ return value?.trim() || undefined;
140
+ }
141
+ const frameworkIndex = args.findIndex((arg) => arg === '--framework');
142
+ if (frameworkIndex !== -1 && args[frameworkIndex + 1]) {
143
+ return args[frameworkIndex + 1];
144
+ }
145
+ return undefined;
146
+ }
147
+ function shouldUseVendor(vendor, defaultClient) {
148
+ if (vendor) {
149
+ return vendor;
150
+ }
151
+ return defaultClient === 'claude-code' ? 'claude' : 'none';
152
+ }
153
+ /**
154
+ * Scaffold a new LumenFlow project
155
+ */
156
+ export async function scaffoldProject(targetDir, options) {
157
+ const result = {
158
+ created: [],
159
+ skipped: [],
160
+ };
161
+ const defaultClient = options.defaultClient ?? detectDefaultClient();
162
+ const vendor = shouldUseVendor(options.vendor, defaultClient);
163
+ // Ensure target directory exists
164
+ if (!fs.existsSync(targetDir)) {
165
+ fs.mkdirSync(targetDir, { recursive: true });
166
+ }
167
+ const tokenDefaults = {
168
+ DATE: getCurrentDate(),
169
+ PROJECT_ROOT: targetDir,
170
+ };
171
+ // Create .lumenflow.config.yaml
172
+ await createFile(path.join(targetDir, CONFIG_FILE_NAME), generateLumenflowConfigYaml(), options.force, result, targetDir);
173
+ // Create LUMENFLOW.md (main entry point)
174
+ await createFile(path.join(targetDir, 'LUMENFLOW.md'), processTemplate(LUMENFLOW_MD_TEMPLATE, tokenDefaults), options.force, result, targetDir);
175
+ // Create .lumenflow/constraints.md
176
+ await createFile(path.join(targetDir, LUMENFLOW_DIR, 'constraints.md'), processTemplate(CONSTRAINTS_MD_TEMPLATE, tokenDefaults), options.force, result, targetDir);
177
+ // Create .lumenflow/agents directory with .gitkeep
178
+ await createDirectory(path.join(targetDir, LUMENFLOW_AGENTS_DIR), result, targetDir);
179
+ await createFile(path.join(targetDir, LUMENFLOW_AGENTS_DIR, '.gitkeep'), '', options.force, result, targetDir);
180
+ // Optional: full docs scaffolding
181
+ if (options.full) {
182
+ await scaffoldFullDocs(targetDir, options, result, tokenDefaults);
183
+ }
184
+ // Optional: framework overlay
185
+ if (options.framework) {
186
+ await scaffoldFrameworkOverlay(targetDir, options, result, tokenDefaults);
187
+ }
188
+ // Scaffold vendor-specific files
189
+ await scaffoldVendorFiles(targetDir, options, result, tokenDefaults, vendor);
190
+ return result;
191
+ }
192
+ async function scaffoldFullDocs(targetDir, options, result, tokens) {
193
+ const tasksDir = path.join(targetDir, 'docs', '04-operations', 'tasks');
194
+ const wuDir = path.join(tasksDir, 'wu');
195
+ const templatesDir = path.join(tasksDir, 'templates');
196
+ await createDirectory(wuDir, result, targetDir);
197
+ await createDirectory(templatesDir, result, targetDir);
198
+ await createFile(path.join(wuDir, '.gitkeep'), '', options.force, result, targetDir);
199
+ await createFile(path.join(tasksDir, 'backlog.md'), BACKLOG_TEMPLATE, options.force, result, targetDir);
200
+ await createFile(path.join(tasksDir, 'status.md'), STATUS_TEMPLATE, options.force, result, targetDir);
201
+ await createFile(path.join(templatesDir, 'wu-template.yaml'), processTemplate(WU_TEMPLATE_YAML, tokens), options.force, result, targetDir);
202
+ }
203
+ async function scaffoldFrameworkOverlay(targetDir, options, result, tokens) {
204
+ if (!options.framework) {
205
+ return;
206
+ }
207
+ const { name, slug } = normalizeFrameworkName(options.framework);
208
+ const frameworkTokens = {
209
+ ...tokens,
210
+ FRAMEWORK_NAME: name,
211
+ FRAMEWORK_SLUG: slug,
212
+ };
213
+ await createFile(path.join(targetDir, FRAMEWORK_HINT_FILE), processTemplate(FRAMEWORK_HINT_TEMPLATE, frameworkTokens), options.force, result, targetDir);
214
+ const overlayDir = path.join(targetDir, 'docs', '04-operations', '_frameworks', slug);
215
+ await createDirectory(overlayDir, result, targetDir);
216
+ await createFile(path.join(overlayDir, 'README.md'), processTemplate(FRAMEWORK_OVERLAY_TEMPLATE, frameworkTokens), options.force, result, targetDir);
217
+ }
218
+ /**
219
+ * Scaffold vendor-specific files based on --vendor option
220
+ */
221
+ async function scaffoldVendorFiles(targetDir, options, result, tokens, vendor) {
222
+ // Claude Code
223
+ if (vendor === 'claude' || vendor === 'all') {
224
+ await createFile(path.join(targetDir, 'CLAUDE.md'), processTemplate(CLAUDE_MD_TEMPLATE, tokens), options.force, result, targetDir);
225
+ await createDirectory(path.join(targetDir, CLAUDE_AGENTS_DIR), result, targetDir);
226
+ await createFile(path.join(targetDir, CLAUDE_AGENTS_DIR, '.gitkeep'), '', options.force, result, targetDir);
227
+ await createFile(path.join(targetDir, CLAUDE_DIR, 'settings.json'), CLAUDE_SETTINGS_TEMPLATE, options.force, result, targetDir);
228
+ }
229
+ // Cursor
230
+ if (vendor === 'cursor' || vendor === 'all') {
231
+ const cursorDir = path.join(targetDir, '.cursor');
232
+ await createDirectory(cursorDir, result, targetDir);
233
+ await createFile(path.join(cursorDir, 'rules.md'), processTemplate(CURSOR_RULES_TEMPLATE, tokens), options.force, result, targetDir);
234
+ }
235
+ // Aider
236
+ if (vendor === 'aider' || vendor === 'all') {
237
+ await createFile(path.join(targetDir, '.aider.conf.yml'), AIDER_CONF_TEMPLATE, options.force, result, targetDir);
238
+ }
239
+ }
240
+ /**
241
+ * Create a directory if missing
242
+ */
243
+ async function createDirectory(dirPath, result, targetDir) {
244
+ if (!fs.existsSync(dirPath)) {
245
+ fs.mkdirSync(dirPath, { recursive: true });
246
+ result.created.push(getRelativePath(targetDir, dirPath));
247
+ }
248
+ }
249
+ /**
250
+ * Create a file, respecting force option
251
+ */
252
+ async function createFile(filePath, content, force, result, targetDir) {
253
+ const relativePath = getRelativePath(targetDir, filePath);
254
+ if (fs.existsSync(filePath) && !force) {
255
+ result.skipped.push(relativePath);
256
+ return;
257
+ }
258
+ const parentDir = path.dirname(filePath);
259
+ if (!fs.existsSync(parentDir)) {
260
+ fs.mkdirSync(parentDir, { recursive: true });
261
+ }
262
+ fs.writeFileSync(filePath, content);
263
+ result.created.push(relativePath);
264
+ }
265
+ /**
266
+ * CLI entry point
267
+ */
268
+ export async function main() {
269
+ const args = process.argv.slice(2);
270
+ const force = args.includes('--force') || args.includes('-f');
271
+ const full = args.includes('--full');
272
+ const vendor = parseVendorArg(args);
273
+ const framework = parseFrameworkArg(args);
274
+ const targetDir = process.cwd();
275
+ console.log('[lumenflow init] Scaffolding LumenFlow project...');
276
+ console.log(` Mode: ${full ? 'full' : 'minimal'}`);
277
+ console.log(` Framework: ${framework ?? 'none'}`);
278
+ console.log(` Vendor overlays: ${vendor ?? 'auto'}`);
279
+ const result = await scaffoldProject(targetDir, {
280
+ force,
281
+ full,
282
+ vendor,
283
+ framework,
284
+ });
285
+ if (result.created.length > 0) {
286
+ console.log('\nCreated:');
287
+ result.created.forEach((f) => console.log(` + ${f}`));
288
+ }
289
+ if (result.skipped.length > 0) {
290
+ console.log('\nSkipped (already exists, use --force to overwrite):');
291
+ result.skipped.forEach((f) => console.log(` - ${f}`));
292
+ }
293
+ console.log('\n[lumenflow init] Done! Next steps:');
294
+ console.log(' 1. Review LUMENFLOW.md for workflow documentation');
295
+ console.log(` 2. Edit ${CONFIG_FILE_NAME} to match your project structure`);
296
+ console.log(' 3. Run: pnpm wu:create --id WU-0001 --lane <lane> --title "First WU"');
297
+ }
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Initiative Bulk Assign WUs CLI (WU-1018)
4
+ *
5
+ * Bulk-assigns orphaned WUs to initiatives based on lane prefix rules.
6
+ * Uses micro-worktree isolation for race-safe commits.
7
+ *
8
+ * Usage:
9
+ * pnpm initiative:bulk-assign # Dry-run (default)
10
+ * LUMENFLOW_ADMIN=1 pnpm initiative:bulk-assign --apply # Apply changes
11
+ * pnpm initiative:bulk-assign --config custom-config.yaml # Custom config
12
+ * pnpm initiative:bulk-assign --reconcile-initiative INIT-001
13
+ *
14
+ * @module initiative-bulk-assign-wus
15
+ */
16
+ import { readFile, writeFile } 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 { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
22
+ import { die } from '@lumenflow/core/dist/error-handler.js';
23
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
24
+ /** Log prefix for console output */
25
+ const LOG_PREFIX = '[initiative:bulk-assign]';
26
+ /** Default lane bucket configuration path */
27
+ const DEFAULT_CONFIG_PATH = 'tools/config/initiative-lane-buckets.yaml';
28
+ /** WU directory relative to repo root */
29
+ const WU_DIR = 'docs/04-operations/tasks/wu';
30
+ /** Initiative directory relative to repo root */
31
+ const INIT_DIR = 'docs/04-operations/tasks/initiatives';
32
+ /** Environment variable required for apply mode */
33
+ const ADMIN_ENV_VAR = 'LUMENFLOW_ADMIN';
34
+ /** Micro-worktree operation name */
35
+ const OPERATION_NAME = 'initiative-bulk-assign';
36
+ /**
37
+ * Load lane bucket configuration
38
+ */
39
+ async function loadConfig(configPath) {
40
+ const fullPath = join(process.cwd(), configPath);
41
+ if (!existsSync(fullPath)) {
42
+ console.log(`${LOG_PREFIX} Config not found: ${configPath}`);
43
+ console.log(`${LOG_PREFIX} Using empty rules (no auto-assignment)`);
44
+ return { rules: [] };
45
+ }
46
+ const content = await readFile(fullPath, { encoding: 'utf-8' });
47
+ return parseYaml(content);
48
+ }
49
+ /**
50
+ * Scan top-level meta from WU YAML content (text-based to preserve formatting)
51
+ */
52
+ function scanTopLevelMeta(text, filePath) {
53
+ const lines = text.split('\n');
54
+ let id;
55
+ let lane;
56
+ let initiative;
57
+ let laneLineIndex = -1;
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const line = lines[i];
60
+ const trimmed = line.trim();
61
+ // Skip comments and empty lines
62
+ if (trimmed.startsWith('#') || trimmed === '' || trimmed === '---') {
63
+ continue;
64
+ }
65
+ // Extract id
66
+ if (trimmed.startsWith('id:')) {
67
+ id = trimmed.replace('id:', '').trim();
68
+ }
69
+ // Extract lane
70
+ if (trimmed.startsWith('lane:')) {
71
+ lane = trimmed.replace('lane:', '').trim();
72
+ laneLineIndex = i;
73
+ }
74
+ // Extract initiative
75
+ if (trimmed.startsWith('initiative:')) {
76
+ initiative = trimmed.replace('initiative:', '').trim();
77
+ }
78
+ }
79
+ if (!id || !lane || laneLineIndex === -1) {
80
+ return null;
81
+ }
82
+ return {
83
+ id,
84
+ lane,
85
+ initiative,
86
+ filePath,
87
+ laneLineIndex,
88
+ rawContent: text,
89
+ };
90
+ }
91
+ /**
92
+ * Insert initiative line after lane line (text-based)
93
+ */
94
+ function insertInitiativeLine(text, laneLineIndex, initiativeId) {
95
+ const lines = text.split('\n');
96
+ const initLine = `initiative: ${initiativeId}`;
97
+ // Insert after lane line
98
+ lines.splice(laneLineIndex + 1, 0, initLine);
99
+ return lines.join('\n');
100
+ }
101
+ /**
102
+ * Match lane against rules to find initiative
103
+ */
104
+ function pickInitiativeForLane(lane, rules) {
105
+ for (const rule of rules) {
106
+ if (lane.toLowerCase().startsWith(rule.lane_prefix.toLowerCase())) {
107
+ return rule.initiative;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ /**
113
+ * List all WU files
114
+ */
115
+ async function listWUFiles() {
116
+ const wuDir = join(process.cwd(), WU_DIR);
117
+ return fg('WU-*.yaml', { cwd: wuDir, absolute: true });
118
+ }
119
+ /**
120
+ * List all initiative files
121
+ */
122
+ async function listInitiativeFiles() {
123
+ const initDir = join(process.cwd(), INIT_DIR);
124
+ if (!existsSync(initDir)) {
125
+ return [];
126
+ }
127
+ return fg('INIT-*.yaml', { cwd: initDir, absolute: true });
128
+ }
129
+ /**
130
+ * Load WU IDs from initiative files
131
+ */
132
+ async function loadInitiativeWUs() {
133
+ const initFiles = await listInitiativeFiles();
134
+ const initWUs = new Map();
135
+ for (const file of initFiles) {
136
+ try {
137
+ const content = await readFile(file, { encoding: 'utf-8' });
138
+ const init = parseYaml(content);
139
+ if (init.id && Array.isArray(init.wus)) {
140
+ initWUs.set(init.id, init.wus);
141
+ }
142
+ }
143
+ catch {
144
+ // Skip invalid initiative files
145
+ }
146
+ }
147
+ return initWUs;
148
+ }
149
+ /**
150
+ * Compute all changes without writing
151
+ */
152
+ async function computeChanges(config) {
153
+ const wuFiles = await listWUFiles();
154
+ const initWUs = await loadInitiativeWUs();
155
+ const changes = [];
156
+ const stats = {
157
+ total: wuFiles.length,
158
+ alreadyAssigned: 0,
159
+ newlyAssigned: 0,
160
+ synced: 0,
161
+ skipped: 0,
162
+ };
163
+ // Build reverse lookup: WU ID -> Initiative ID
164
+ const wuToInit = new Map();
165
+ for (const [initId, wuList] of initWUs.entries()) {
166
+ for (const wuId of wuList) {
167
+ wuToInit.set(wuId, initId);
168
+ }
169
+ }
170
+ for (const file of wuFiles) {
171
+ try {
172
+ const content = await readFile(file, { encoding: 'utf-8' });
173
+ const meta = scanTopLevelMeta(content, file);
174
+ if (!meta) {
175
+ stats.skipped++;
176
+ continue;
177
+ }
178
+ // Check if already assigned
179
+ if (meta.initiative) {
180
+ stats.alreadyAssigned++;
181
+ continue;
182
+ }
183
+ // Check if initiative assigns this WU
184
+ const assignedInit = wuToInit.get(meta.id);
185
+ if (assignedInit) {
186
+ // Sync from initiative
187
+ const newContent = insertInitiativeLine(content, meta.laneLineIndex, assignedInit);
188
+ changes.push({
189
+ wuId: meta.id,
190
+ type: 'sync',
191
+ initiative: assignedInit,
192
+ filePath: file,
193
+ newContent,
194
+ });
195
+ stats.synced++;
196
+ continue;
197
+ }
198
+ // Try to auto-assign by lane prefix
199
+ const matchedInit = pickInitiativeForLane(meta.lane, config.rules);
200
+ if (matchedInit) {
201
+ const newContent = insertInitiativeLine(content, meta.laneLineIndex, matchedInit);
202
+ changes.push({
203
+ wuId: meta.id,
204
+ type: 'assign',
205
+ initiative: matchedInit,
206
+ filePath: file,
207
+ newContent,
208
+ });
209
+ stats.newlyAssigned++;
210
+ }
211
+ else {
212
+ stats.skipped++;
213
+ }
214
+ }
215
+ catch {
216
+ stats.skipped++;
217
+ }
218
+ }
219
+ return { changes, stats };
220
+ }
221
+ /**
222
+ * Print summary of changes
223
+ */
224
+ function printSummary(stats) {
225
+ console.log('');
226
+ console.log('═══════════════════════════════════════════════════════════════');
227
+ console.log(' BULK ASSIGNMENT SUMMARY');
228
+ console.log('═══════════════════════════════════════════════════════════════');
229
+ console.log(` Total WUs scanned: ${stats.total}`);
230
+ console.log(` Already assigned: ${stats.alreadyAssigned}`);
231
+ console.log(` Synced from initiatives: ${stats.synced}`);
232
+ console.log(` Newly assigned by lane: ${stats.newlyAssigned}`);
233
+ console.log(` Skipped (no match): ${stats.skipped}`);
234
+ console.log('═══════════════════════════════════════════════════════════════');
235
+ console.log('');
236
+ }
237
+ /**
238
+ * Main function
239
+ */
240
+ async function main() {
241
+ const args = createWUParser({
242
+ name: 'initiative-bulk-assign-wus',
243
+ description: 'Bulk-assign orphaned WUs to initiatives based on lane prefix rules',
244
+ options: [WU_OPTIONS.config, WU_OPTIONS.apply, WU_OPTIONS.syncFromInitiative],
245
+ required: [],
246
+ });
247
+ const configPath = args.config || DEFAULT_CONFIG_PATH;
248
+ const applyMode = args.apply === true;
249
+ console.log(`${LOG_PREFIX} Bulk assign WUs to initiatives`);
250
+ console.log(`${LOG_PREFIX} Config: ${configPath}`);
251
+ console.log(`${LOG_PREFIX} Mode: ${applyMode ? 'APPLY' : 'dry-run'}`);
252
+ // Check admin mode for apply
253
+ if (applyMode && process.env[ADMIN_ENV_VAR] !== '1') {
254
+ die(`Apply mode requires ${ADMIN_ENV_VAR}=1 environment variable.\n\n` +
255
+ `This prevents accidental use by agents.\n\n` +
256
+ `Usage: ${ADMIN_ENV_VAR}=1 pnpm initiative:bulk-assign --apply`);
257
+ }
258
+ // Load configuration
259
+ const config = await loadConfig(configPath);
260
+ console.log(`${LOG_PREFIX} Loaded ${config.rules.length} lane assignment rules`);
261
+ // Compute changes
262
+ console.log(`${LOG_PREFIX} Scanning WUs...`);
263
+ const { changes, stats } = await computeChanges(config);
264
+ // Print summary
265
+ printSummary(stats);
266
+ if (changes.length === 0) {
267
+ console.log(`${LOG_PREFIX} No changes to apply.`);
268
+ return;
269
+ }
270
+ // Show changes
271
+ console.log(`${LOG_PREFIX} Changes to apply (${changes.length}):`);
272
+ for (const change of changes) {
273
+ const icon = change.type === 'sync' ? '↻' : '→';
274
+ console.log(` ${icon} ${change.wuId} ${change.type} ${change.initiative}`);
275
+ }
276
+ if (!applyMode) {
277
+ console.log('');
278
+ console.log(`${LOG_PREFIX} Dry-run complete. Use --apply to write changes.`);
279
+ console.log(`${LOG_PREFIX} ${ADMIN_ENV_VAR}=1 pnpm initiative:bulk-assign --apply`);
280
+ return;
281
+ }
282
+ // Apply changes via micro-worktree
283
+ console.log('');
284
+ console.log(`${LOG_PREFIX} Applying changes via micro-worktree...`);
285
+ await withMicroWorktree({
286
+ operation: OPERATION_NAME,
287
+ id: `bulk-${Date.now()}`,
288
+ logPrefix: LOG_PREFIX,
289
+ execute: async ({ worktreePath }) => {
290
+ const filesChanged = [];
291
+ for (const change of changes) {
292
+ if (!change.newContent)
293
+ continue;
294
+ // Calculate relative path from repo root
295
+ const relativePath = change.filePath.replace(process.cwd() + '/', '');
296
+ const worktreeFilePath = join(worktreePath, relativePath);
297
+ await writeFile(worktreeFilePath, change.newContent, { encoding: 'utf-8' });
298
+ filesChanged.push(relativePath);
299
+ }
300
+ const commitMessage = `chore: bulk-assign ${changes.length} WUs to initiatives\n\nAuto-assigned by initiative-bulk-assign-wus`;
301
+ return {
302
+ commitMessage,
303
+ files: filesChanged,
304
+ };
305
+ },
306
+ });
307
+ console.log(`${LOG_PREFIX} ✅ Successfully applied ${changes.length} changes`);
308
+ }
309
+ // Guard main() for testability (WU-1366)
310
+ import { fileURLToPath } from 'node:url';
311
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
312
+ main().catch((err) => {
313
+ die(`Bulk assign failed: ${err.message}`);
314
+ });
315
+ }
@@ -30,11 +30,11 @@ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
30
30
  import { die } from '@lumenflow/core/dist/error-handler.js';
31
31
  import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
32
32
  import { join } from 'node:path';
33
- import yaml from 'js-yaml';
33
+ import { stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
34
34
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
35
35
  import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
36
36
  import { INIT_PATTERNS, INIT_COMMIT_FORMATS, INIT_DEFAULTS, } from '@lumenflow/initiatives/dist/initiative-constants.js';
37
- import { FILE_SYSTEM, YAML_OPTIONS } from '@lumenflow/core/dist/wu-constants.js';
37
+ import { FILE_SYSTEM } from '@lumenflow/core/dist/wu-constants.js';
38
38
  import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
39
39
  import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
40
40
  // WU-1428: Use date-utils for consistent YYYY-MM-DD format (library-first)
@@ -97,11 +97,7 @@ function createInitiativeYamlInWorktree(worktreePath, id, slug, title, options =
97
97
  success_metrics: [],
98
98
  labels: [],
99
99
  };
100
- const yamlContent = yaml.dump(initContent, {
101
- lineWidth: YAML_OPTIONS.LINE_WIDTH,
102
- quotingType: '"',
103
- forceQuotes: false,
104
- });
100
+ const yamlContent = stringifyYAML(initContent);
105
101
  writeFileSync(initAbsolutePath, yamlContent, { encoding: FILE_SYSTEM.UTF8 });
106
102
  console.log(`${LOG_PREFIX} ✅ Created ${id}.yaml in micro-worktree`);
107
103
  return initRelativePath;