@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/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
|
|
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
|
|
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 =
|
|
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;
|