@lumenflow/cli 2.5.0 → 2.5.1
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/README.md +11 -8
- package/dist/__tests__/init-scripts.test.js +111 -0
- package/dist/__tests__/templates-sync.test.js +219 -0
- package/dist/init.js +90 -0
- package/dist/orchestrate-init-status.js +37 -9
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/sync-templates.js +137 -5
- package/dist/wu-prep.js +131 -8
- package/dist/wu-spawn.js +7 -2
- package/package.json +7 -7
- package/templates/core/.lumenflow/constraints.md.template +61 -3
- package/templates/core/LUMENFLOW.md.template +85 -23
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
- package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
- package/templates/core/ai/onboarding/release-process.md.template +8 -2
- package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
- package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
- package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
package/README.md
CHANGED
|
@@ -16,19 +16,22 @@ npm install @lumenflow/cli
|
|
|
16
16
|
## Quick Start
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
+
# Install the CLI
|
|
20
|
+
pnpm add -D @lumenflow/cli # or: npm install -D @lumenflow/cli
|
|
21
|
+
|
|
19
22
|
# Initialize LumenFlow (works with any AI)
|
|
20
|
-
|
|
23
|
+
pnpm exec lumenflow
|
|
21
24
|
|
|
22
25
|
# Or specify your AI tool for enhanced integration
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
pnpm exec lumenflow --client claude # Claude Code
|
|
27
|
+
pnpm exec lumenflow --client cursor # Cursor
|
|
28
|
+
pnpm exec lumenflow --client windsurf # Windsurf
|
|
29
|
+
pnpm exec lumenflow --client cline # Cline
|
|
30
|
+
pnpm exec lumenflow --client aider # Aider
|
|
31
|
+
pnpm exec lumenflow --client all # All integrations
|
|
29
32
|
```
|
|
30
33
|
|
|
31
|
-
The default `lumenflow
|
|
34
|
+
The default `lumenflow` command creates `AGENTS.md` and `LUMENFLOW.md` which work with **any AI coding assistant**. The `--client` flag adds vendor-specific configuration files for deeper integration.
|
|
32
35
|
|
|
33
36
|
See [AI Integrations](https://lumenflow.dev/guides/ai-integrations) for details on each tool.
|
|
34
37
|
|
|
@@ -93,4 +93,115 @@ describe('init scripts generation (WU-1307)', () => {
|
|
|
93
93
|
expect(packageJson.scripts?.['wu:claim']).toBeDefined();
|
|
94
94
|
expect(packageJson.scripts?.gates).toBeDefined();
|
|
95
95
|
});
|
|
96
|
+
// WU-1342: Test for all 17 essential commands
|
|
97
|
+
it('should include all 17 essential commands (WU-1342)', async () => {
|
|
98
|
+
// Act
|
|
99
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
100
|
+
// Assert
|
|
101
|
+
const packageJson = readPackageJson();
|
|
102
|
+
// All 17 essential commands that must be present per WU-1342 acceptance criteria
|
|
103
|
+
const essentialScripts = [
|
|
104
|
+
// Core WU lifecycle
|
|
105
|
+
'wu:claim',
|
|
106
|
+
'wu:done',
|
|
107
|
+
'wu:create',
|
|
108
|
+
'wu:status',
|
|
109
|
+
'wu:block',
|
|
110
|
+
'wu:unblock',
|
|
111
|
+
// Additional critical commands (WU-1342)
|
|
112
|
+
'wu:prep',
|
|
113
|
+
'wu:recover',
|
|
114
|
+
'wu:spawn',
|
|
115
|
+
'wu:validate',
|
|
116
|
+
'wu:infer-lane',
|
|
117
|
+
// Memory commands
|
|
118
|
+
'mem:init',
|
|
119
|
+
'mem:checkpoint',
|
|
120
|
+
'mem:inbox',
|
|
121
|
+
// Lane commands
|
|
122
|
+
'lane:suggest',
|
|
123
|
+
// Gates
|
|
124
|
+
'gates',
|
|
125
|
+
'gates:docs',
|
|
126
|
+
];
|
|
127
|
+
for (const scriptName of essentialScripts) {
|
|
128
|
+
expect(packageJson.scripts?.[scriptName], `Missing essential script: ${scriptName}`).toBeDefined();
|
|
129
|
+
}
|
|
130
|
+
// Verify count
|
|
131
|
+
const lumenflowScripts = Object.keys(packageJson.scripts ?? {}).filter((key) => key.startsWith('wu:') ||
|
|
132
|
+
key.startsWith('mem:') ||
|
|
133
|
+
key.startsWith('lane:') ||
|
|
134
|
+
key === 'gates' ||
|
|
135
|
+
key === 'gates:docs');
|
|
136
|
+
expect(lumenflowScripts.length).toBeGreaterThanOrEqual(17);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('init .gitignore generation (WU-1342)', () => {
|
|
140
|
+
let tempDir;
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-gitignore-'));
|
|
143
|
+
});
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
146
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
it('should create .gitignore with required exclusions (WU-1342)', async () => {
|
|
150
|
+
// Act
|
|
151
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
152
|
+
// Assert
|
|
153
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
154
|
+
expect(fs.existsSync(gitignorePath)).toBe(true);
|
|
155
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
156
|
+
// Must include node_modules
|
|
157
|
+
expect(content).toContain('node_modules');
|
|
158
|
+
// Must include .lumenflow/state
|
|
159
|
+
expect(content).toContain('.lumenflow/state');
|
|
160
|
+
// Must include worktrees
|
|
161
|
+
expect(content).toContain('worktrees');
|
|
162
|
+
});
|
|
163
|
+
it('should preserve existing .gitignore content in merge mode (WU-1342)', async () => {
|
|
164
|
+
// Arrange
|
|
165
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
166
|
+
const existingContent = '# Custom ignores\n.env\n*.log\n';
|
|
167
|
+
fs.writeFileSync(gitignorePath, existingContent);
|
|
168
|
+
// Act
|
|
169
|
+
await scaffoldProject(tempDir, { force: false, full: true, merge: true });
|
|
170
|
+
// Assert
|
|
171
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
172
|
+
// Should preserve existing content
|
|
173
|
+
expect(content).toContain('.env');
|
|
174
|
+
expect(content).toContain('*.log');
|
|
175
|
+
// Should add LumenFlow exclusions
|
|
176
|
+
expect(content).toContain('node_modules');
|
|
177
|
+
expect(content).toContain('.lumenflow/state');
|
|
178
|
+
expect(content).toContain('worktrees');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('init .claude directory creation (WU-1342)', () => {
|
|
182
|
+
let tempDir;
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-claude-'));
|
|
185
|
+
});
|
|
186
|
+
afterEach(() => {
|
|
187
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
188
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
it('should create .claude directory when --client claude specified (WU-1342)', async () => {
|
|
192
|
+
// Act
|
|
193
|
+
await scaffoldProject(tempDir, { force: true, full: false, client: 'claude' });
|
|
194
|
+
// Assert
|
|
195
|
+
const claudeDir = path.join(tempDir, '.claude');
|
|
196
|
+
expect(fs.existsSync(claudeDir)).toBe(true);
|
|
197
|
+
// Should have agents directory
|
|
198
|
+
const agentsDir = path.join(claudeDir, 'agents');
|
|
199
|
+
expect(fs.existsSync(agentsDir)).toBe(true);
|
|
200
|
+
// Should have settings.json
|
|
201
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
202
|
+
expect(fs.existsSync(settingsPath)).toBe(true);
|
|
203
|
+
// Should have skills directory
|
|
204
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
205
|
+
expect(fs.existsSync(skillsDir)).toBe(true);
|
|
206
|
+
});
|
|
96
207
|
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file templates-sync.test.ts
|
|
3
|
+
* Tests for templates synchronization and drift detection (WU-1353)
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import { syncTemplates, syncOnboardingDocs, syncCoreDocs, convertToTemplate, checkTemplateDrift, } from '../sync-templates.js';
|
|
10
|
+
// Constants for frequently used path segments (sonarjs/no-duplicate-string)
|
|
11
|
+
const PACKAGES_DIR = 'packages';
|
|
12
|
+
const LUMENFLOW_SCOPE = '@lumenflow';
|
|
13
|
+
const CLI_DIR = 'cli';
|
|
14
|
+
const TEMPLATES_DIR = 'templates';
|
|
15
|
+
const CORE_DIR = 'core';
|
|
16
|
+
const LUMENFLOW_DOT_DIR = '.lumenflow';
|
|
17
|
+
const CONSTRAINTS_FILE = 'constraints.md';
|
|
18
|
+
const CONSTRAINTS_TEMPLATE = 'constraints.md.template';
|
|
19
|
+
describe('templates-sync', () => {
|
|
20
|
+
let tempDir;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'templates-sync-test-'));
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
26
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
describe('convertToTemplate', () => {
|
|
30
|
+
it('should replace dates with {{DATE}} placeholder', () => {
|
|
31
|
+
const content = 'Updated: 2026-02-02\nCreated: 2025-01-15';
|
|
32
|
+
const result = convertToTemplate(content, '/home/test/project');
|
|
33
|
+
expect(result).toBe('Updated: {{DATE}}\nCreated: {{DATE}}');
|
|
34
|
+
});
|
|
35
|
+
it('should preserve content without dates', () => {
|
|
36
|
+
const content = '# Title\n\nSome content without dates.';
|
|
37
|
+
const result = convertToTemplate(content, '/home/test/project');
|
|
38
|
+
expect(result).toBe(content);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('syncCoreDocs', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
// Set up directory structure
|
|
44
|
+
const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
|
|
45
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
46
|
+
// Create source constraints.md with v1.1 content
|
|
47
|
+
const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
|
|
48
|
+
fs.mkdirSync(lumenflowDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# LumenFlow Constraints Capsule
|
|
50
|
+
|
|
51
|
+
**Version:** 1.1
|
|
52
|
+
**Last updated:** 2026-02-02
|
|
53
|
+
|
|
54
|
+
This document contains the 7 non-negotiable constraints.
|
|
55
|
+
|
|
56
|
+
### 1. Worktree Discipline and Git Safety
|
|
57
|
+
|
|
58
|
+
**MANDATORY PRE-WRITE CHECK**
|
|
59
|
+
|
|
60
|
+
**NEVER "QUICK FIX" ON MAIN**
|
|
61
|
+
`);
|
|
62
|
+
// Create LUMENFLOW.md
|
|
63
|
+
fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), `# LumenFlow Workflow Guide
|
|
64
|
+
|
|
65
|
+
**Last updated:** 2026-02-02
|
|
66
|
+
|
|
67
|
+
## Critical Rule: Use wu:prep Then wu:done
|
|
68
|
+
`);
|
|
69
|
+
});
|
|
70
|
+
it('should sync constraints.md to template', async () => {
|
|
71
|
+
const result = await syncCoreDocs(tempDir, false);
|
|
72
|
+
expect(result.errors).toHaveLength(0);
|
|
73
|
+
expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/${LUMENFLOW_DOT_DIR}/${CONSTRAINTS_TEMPLATE}`);
|
|
74
|
+
// Verify template content
|
|
75
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
76
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
77
|
+
// Should have {{DATE}} placeholder
|
|
78
|
+
expect(templateContent).toContain('{{DATE}}');
|
|
79
|
+
expect(templateContent).not.toContain('2026-02-02');
|
|
80
|
+
// Should have v1.1 content markers
|
|
81
|
+
expect(templateContent).toContain('Version:** 1.1');
|
|
82
|
+
expect(templateContent).toContain('7 non-negotiable constraints');
|
|
83
|
+
expect(templateContent).toContain('MANDATORY PRE-WRITE CHECK');
|
|
84
|
+
expect(templateContent).toContain('NEVER "QUICK FIX" ON MAIN');
|
|
85
|
+
});
|
|
86
|
+
it('should use dry-run mode without writing files', async () => {
|
|
87
|
+
// First, ensure no template exists
|
|
88
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
89
|
+
// Remove if it exists from beforeEach
|
|
90
|
+
if (fs.existsSync(templatePath)) {
|
|
91
|
+
fs.unlinkSync(templatePath);
|
|
92
|
+
}
|
|
93
|
+
const result = await syncCoreDocs(tempDir, true);
|
|
94
|
+
expect(result.errors).toHaveLength(0);
|
|
95
|
+
expect(result.synced.length).toBeGreaterThan(0);
|
|
96
|
+
expect(fs.existsSync(templatePath)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('syncOnboardingDocs', () => {
|
|
100
|
+
const ONBOARDING_SUBPATH = [
|
|
101
|
+
'docs',
|
|
102
|
+
'04-operations',
|
|
103
|
+
'_frameworks',
|
|
104
|
+
'lumenflow',
|
|
105
|
+
'agent',
|
|
106
|
+
'onboarding',
|
|
107
|
+
];
|
|
108
|
+
const FIRST_WU_MISTAKES_FILE = 'first-wu-mistakes.md';
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
// Set up onboarding source directory
|
|
111
|
+
const onboardingDir = path.join(tempDir, ...ONBOARDING_SUBPATH);
|
|
112
|
+
fs.mkdirSync(onboardingDir, { recursive: true });
|
|
113
|
+
// Create first-wu-mistakes.md with v1.1 content (11 mistakes)
|
|
114
|
+
fs.writeFileSync(path.join(onboardingDir, FIRST_WU_MISTAKES_FILE), `# First WU Mistakes
|
|
115
|
+
|
|
116
|
+
**Last updated:** 2026-02-02
|
|
117
|
+
|
|
118
|
+
## Mistake 1: Not Using Worktrees
|
|
119
|
+
|
|
120
|
+
pnpm wu:prep --id WU-123
|
|
121
|
+
|
|
122
|
+
## Mistake 11: "Quick Fixing" on Main
|
|
123
|
+
|
|
124
|
+
## Quick Checklist
|
|
125
|
+
|
|
126
|
+
- [ ] Check spec_refs for plans
|
|
127
|
+
`);
|
|
128
|
+
// Set up target directory
|
|
129
|
+
const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding');
|
|
130
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
131
|
+
});
|
|
132
|
+
it('should sync first-wu-mistakes.md to template', async () => {
|
|
133
|
+
const result = await syncOnboardingDocs(tempDir, false);
|
|
134
|
+
expect(result.errors).toHaveLength(0);
|
|
135
|
+
expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/ai/onboarding/${FIRST_WU_MISTAKES_FILE}.template`);
|
|
136
|
+
// Verify template content
|
|
137
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding', `${FIRST_WU_MISTAKES_FILE}.template`);
|
|
138
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
139
|
+
// Should have {{DATE}} placeholder
|
|
140
|
+
expect(templateContent).toContain('{{DATE}}');
|
|
141
|
+
expect(templateContent).not.toContain('2026-02-02');
|
|
142
|
+
// Should have v1.1 content markers
|
|
143
|
+
expect(templateContent).toContain('Mistake 11:');
|
|
144
|
+
expect(templateContent).toContain('Quick Fixing" on Main');
|
|
145
|
+
expect(templateContent).toContain('wu:prep');
|
|
146
|
+
expect(templateContent).toContain('spec_refs');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('checkTemplateDrift', () => {
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
// Set up source files
|
|
152
|
+
const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
|
|
153
|
+
fs.mkdirSync(lumenflowDir, { recursive: true });
|
|
154
|
+
fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# Constraints
|
|
155
|
+
**Version:** 1.1
|
|
156
|
+
**Last updated:** 2026-02-02
|
|
157
|
+
7 constraints`);
|
|
158
|
+
// Set up template directory
|
|
159
|
+
const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
|
|
160
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
161
|
+
});
|
|
162
|
+
it('should detect drift when template is outdated', async () => {
|
|
163
|
+
// Create outdated template (v1.0, 6 constraints)
|
|
164
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
165
|
+
fs.writeFileSync(templatePath, `# Constraints
|
|
166
|
+
**Version:** 1.0
|
|
167
|
+
**Last updated:** {{DATE}}
|
|
168
|
+
6 constraints`);
|
|
169
|
+
const drift = await checkTemplateDrift(tempDir);
|
|
170
|
+
expect(drift.hasDrift).toBe(true);
|
|
171
|
+
expect(drift.driftingFiles.length).toBeGreaterThan(0);
|
|
172
|
+
expect(drift.driftingFiles.some((f) => f.includes(CONSTRAINTS_FILE))).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
it('should report no drift when templates are in sync', async () => {
|
|
175
|
+
// First sync templates
|
|
176
|
+
await syncCoreDocs(tempDir, false);
|
|
177
|
+
// Then check for drift
|
|
178
|
+
const drift = await checkTemplateDrift(tempDir);
|
|
179
|
+
// After sync, constraints should not be drifting
|
|
180
|
+
expect(drift.driftingFiles.filter((f) => f.includes(CONSTRAINTS_FILE))).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
it('should return detailed drift report', async () => {
|
|
183
|
+
// Create outdated template
|
|
184
|
+
const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
|
|
185
|
+
fs.writeFileSync(templatePath, 'outdated content');
|
|
186
|
+
const drift = await checkTemplateDrift(tempDir);
|
|
187
|
+
expect(drift.hasDrift).toBe(true);
|
|
188
|
+
expect(drift.driftingFiles).toBeDefined();
|
|
189
|
+
expect(Array.isArray(drift.driftingFiles)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('syncTemplates (full sync)', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
// Set up minimal directory structure
|
|
195
|
+
const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
|
|
196
|
+
fs.mkdirSync(lumenflowDir, { recursive: true });
|
|
197
|
+
fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), 'content');
|
|
198
|
+
fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), 'content');
|
|
199
|
+
const onboardingDir = path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
|
|
200
|
+
fs.mkdirSync(onboardingDir, { recursive: true });
|
|
201
|
+
fs.writeFileSync(path.join(onboardingDir, 'first-wu-mistakes.md'), 'content');
|
|
202
|
+
const skillsDir = path.join(tempDir, '.claude', 'skills', 'test-skill');
|
|
203
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
204
|
+
fs.writeFileSync(path.join(skillsDir, 'SKILL.md'), 'skill content');
|
|
205
|
+
});
|
|
206
|
+
it('should sync all template categories', async () => {
|
|
207
|
+
const result = await syncTemplates(tempDir, false);
|
|
208
|
+
expect(result.core.errors).toHaveLength(0);
|
|
209
|
+
expect(result.onboarding.errors).toHaveLength(0);
|
|
210
|
+
expect(result.skills.errors).toHaveLength(0);
|
|
211
|
+
// Should sync at least constraints and LUMENFLOW
|
|
212
|
+
expect(result.core.synced.length).toBeGreaterThanOrEqual(2);
|
|
213
|
+
// Should sync onboarding docs
|
|
214
|
+
expect(result.onboarding.synced.length).toBeGreaterThanOrEqual(1);
|
|
215
|
+
// Should sync skills
|
|
216
|
+
expect(result.skills.synced.length).toBeGreaterThanOrEqual(1);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
package/dist/init.js
CHANGED
|
@@ -1909,6 +1909,8 @@ export async function scaffoldProject(targetDir, options) {
|
|
|
1909
1909
|
// Create .lumenflow/agents directory with .gitkeep
|
|
1910
1910
|
await createDirectory(path.join(targetDir, LUMENFLOW_AGENTS_DIR), result, targetDir);
|
|
1911
1911
|
await createFile(path.join(targetDir, LUMENFLOW_AGENTS_DIR, '.gitkeep'), '', options.force ? 'force' : 'skip', result, targetDir);
|
|
1912
|
+
// WU-1342: Create .gitignore with required exclusions
|
|
1913
|
+
await scaffoldGitignore(targetDir, options, result);
|
|
1912
1914
|
// Optional: full docs scaffolding
|
|
1913
1915
|
if (options.full) {
|
|
1914
1916
|
await scaffoldFullDocs(targetDir, options, result, tokenDefaults);
|
|
@@ -1925,18 +1927,106 @@ export async function scaffoldProject(targetDir, options) {
|
|
|
1925
1927
|
}
|
|
1926
1928
|
return result;
|
|
1927
1929
|
}
|
|
1930
|
+
/**
|
|
1931
|
+
* WU-1342: .gitignore template with required exclusions
|
|
1932
|
+
* Includes node_modules, .lumenflow/state, and worktrees
|
|
1933
|
+
*/
|
|
1934
|
+
const GITIGNORE_TEMPLATE = `# Dependencies
|
|
1935
|
+
node_modules/
|
|
1936
|
+
|
|
1937
|
+
# LumenFlow state (local only, not shared)
|
|
1938
|
+
.lumenflow/state/
|
|
1939
|
+
|
|
1940
|
+
# Worktrees (isolated parallel work directories)
|
|
1941
|
+
worktrees/
|
|
1942
|
+
|
|
1943
|
+
# Build output
|
|
1944
|
+
dist/
|
|
1945
|
+
*.tsbuildinfo
|
|
1946
|
+
|
|
1947
|
+
# Environment files
|
|
1948
|
+
.env
|
|
1949
|
+
.env.local
|
|
1950
|
+
.env.*.local
|
|
1951
|
+
|
|
1952
|
+
# IDE
|
|
1953
|
+
.idea/
|
|
1954
|
+
.vscode/
|
|
1955
|
+
*.swp
|
|
1956
|
+
*.swo
|
|
1957
|
+
|
|
1958
|
+
# OS files
|
|
1959
|
+
.DS_Store
|
|
1960
|
+
Thumbs.db
|
|
1961
|
+
`;
|
|
1962
|
+
/** Gitignore file name constant to avoid duplicate string lint error */
|
|
1963
|
+
const GITIGNORE_FILE_NAME = '.gitignore';
|
|
1964
|
+
/**
|
|
1965
|
+
* WU-1342: Scaffold .gitignore file with LumenFlow exclusions
|
|
1966
|
+
* Supports merge mode to add exclusions to existing .gitignore
|
|
1967
|
+
*/
|
|
1968
|
+
async function scaffoldGitignore(targetDir, options, result) {
|
|
1969
|
+
const gitignorePath = path.join(targetDir, GITIGNORE_FILE_NAME);
|
|
1970
|
+
const fileMode = getFileMode(options);
|
|
1971
|
+
if (fileMode === 'merge' && fs.existsSync(gitignorePath)) {
|
|
1972
|
+
// Merge mode: append LumenFlow exclusions if not already present
|
|
1973
|
+
const existingContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1974
|
+
const linesToAdd = [];
|
|
1975
|
+
// Check each required exclusion
|
|
1976
|
+
const requiredExclusions = [
|
|
1977
|
+
{ pattern: 'node_modules', line: 'node_modules/' },
|
|
1978
|
+
{ pattern: '.lumenflow/state', line: '.lumenflow/state/' },
|
|
1979
|
+
{ pattern: 'worktrees', line: 'worktrees/' },
|
|
1980
|
+
];
|
|
1981
|
+
for (const { pattern, line } of requiredExclusions) {
|
|
1982
|
+
if (!existingContent.includes(pattern)) {
|
|
1983
|
+
linesToAdd.push(line);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (linesToAdd.length > 0) {
|
|
1987
|
+
const separator = existingContent.endsWith('\n') ? '' : '\n';
|
|
1988
|
+
const lumenflowBlock = `${separator}
|
|
1989
|
+
# LumenFlow (auto-added)
|
|
1990
|
+
${linesToAdd.join('\n')}
|
|
1991
|
+
`;
|
|
1992
|
+
fs.writeFileSync(gitignorePath, existingContent + lumenflowBlock);
|
|
1993
|
+
result.merged?.push(GITIGNORE_FILE_NAME);
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
result.skipped.push(GITIGNORE_FILE_NAME);
|
|
1997
|
+
}
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
// Skip or force mode
|
|
2001
|
+
await createFile(gitignorePath, GITIGNORE_TEMPLATE, fileMode, result, targetDir);
|
|
2002
|
+
}
|
|
1928
2003
|
/**
|
|
1929
2004
|
* WU-1307: LumenFlow scripts to inject into package.json
|
|
2005
|
+
* WU-1342: Expanded to include all 17 essential commands
|
|
1930
2006
|
* Uses standalone binaries (wu-claim, wu-done, gates) that work in consumer projects
|
|
1931
2007
|
* after installing @lumenflow/cli.
|
|
1932
2008
|
*/
|
|
1933
2009
|
const LUMENFLOW_SCRIPTS = {
|
|
2010
|
+
// Core WU lifecycle
|
|
1934
2011
|
'wu:claim': 'wu-claim',
|
|
1935
2012
|
'wu:done': 'wu-done',
|
|
1936
2013
|
'wu:create': 'wu-create',
|
|
1937
2014
|
'wu:status': 'wu-status',
|
|
1938
2015
|
'wu:block': 'wu-block',
|
|
1939
2016
|
'wu:unblock': 'wu-unblock',
|
|
2017
|
+
// WU-1342: Additional critical commands
|
|
2018
|
+
'wu:prep': 'wu-prep',
|
|
2019
|
+
'wu:recover': 'wu-recover',
|
|
2020
|
+
'wu:spawn': 'wu-spawn',
|
|
2021
|
+
'wu:validate': 'wu-validate',
|
|
2022
|
+
'wu:infer-lane': 'wu-infer-lane',
|
|
2023
|
+
// Memory commands
|
|
2024
|
+
'mem:init': 'mem-init',
|
|
2025
|
+
'mem:checkpoint': 'mem-checkpoint',
|
|
2026
|
+
'mem:inbox': 'mem-inbox',
|
|
2027
|
+
// Lane commands
|
|
2028
|
+
'lane:suggest': 'lane-suggest',
|
|
2029
|
+
// Gates
|
|
1940
2030
|
gates: 'gates',
|
|
1941
2031
|
'gates:docs': 'gates --docs-only',
|
|
1942
2032
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
2
3
|
/**
|
|
3
4
|
* Orchestrate Initiative Status CLI
|
|
4
5
|
*
|
|
@@ -10,10 +11,14 @@
|
|
|
10
11
|
*/
|
|
11
12
|
import { Command } from 'commander';
|
|
12
13
|
import { existsSync, readdirSync } from 'node:fs';
|
|
13
|
-
import { loadInitiativeWUs, calculateProgress, formatProgress } from '@lumenflow/initiatives';
|
|
14
|
+
import { loadInitiativeWUs, calculateProgress, formatProgress, getLaneAvailability, resolveLaneConfigsFromConfig, } from '@lumenflow/initiatives';
|
|
14
15
|
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
|
|
16
|
+
import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
|
|
15
17
|
import chalk from 'chalk';
|
|
16
18
|
const LOG_PREFIX = '[orchestrate:init-status]';
|
|
19
|
+
function getErrorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
17
22
|
function getCompletedWUs(wuIds) {
|
|
18
23
|
const completed = new Set();
|
|
19
24
|
if (!existsSync(LUMENFLOW_PATHS.STAMPS_DIR)) {
|
|
@@ -27,6 +32,21 @@ function getCompletedWUs(wuIds) {
|
|
|
27
32
|
}
|
|
28
33
|
return completed;
|
|
29
34
|
}
|
|
35
|
+
function formatLaneAvailability(availability, laneConfigs) {
|
|
36
|
+
const lanes = Object.keys(availability).sort((a, b) => a.localeCompare(b));
|
|
37
|
+
if (lanes.length === 0) {
|
|
38
|
+
return ' (no lanes found)';
|
|
39
|
+
}
|
|
40
|
+
return lanes
|
|
41
|
+
.map((lane) => {
|
|
42
|
+
const entry = availability[lane];
|
|
43
|
+
const wipLimit = laneConfigs[lane]?.wip_limit ?? 1;
|
|
44
|
+
const statusLabel = entry.available ? chalk.green('available') : chalk.red('occupied');
|
|
45
|
+
const occupiedBy = entry.occupiedBy ?? 'none';
|
|
46
|
+
return ` ${lane}: ${statusLabel} (wip_limit=${wipLimit}, lock_policy=${entry.policy}, in_progress=${entry.inProgressCount}, blocked=${entry.blockedCount}, occupied_by=${occupiedBy})`;
|
|
47
|
+
})
|
|
48
|
+
.join('\n');
|
|
49
|
+
}
|
|
30
50
|
const program = new Command()
|
|
31
51
|
.name('orchestrate:init-status')
|
|
32
52
|
.description('Show initiative progress status')
|
|
@@ -45,18 +65,26 @@ const program = new Command()
|
|
|
45
65
|
const completed = getCompletedWUs(wus.map((w) => w.id));
|
|
46
66
|
console.log(chalk.bold('WUs:'));
|
|
47
67
|
for (const wu of wus) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
let status = chalk.gray('○ ready');
|
|
69
|
+
if (completed.has(wu.id)) {
|
|
70
|
+
status = chalk.green('✓ done');
|
|
71
|
+
}
|
|
72
|
+
else if (wu.doc.status === 'in_progress') {
|
|
73
|
+
status = chalk.yellow('⟳ in_progress');
|
|
74
|
+
}
|
|
75
|
+
else if (wu.doc.status === 'blocked') {
|
|
76
|
+
status = chalk.red('⛔ blocked');
|
|
77
|
+
}
|
|
55
78
|
console.log(` ${wu.id}: ${wu.doc.title} [${status}]`);
|
|
56
79
|
}
|
|
80
|
+
const laneConfigs = resolveLaneConfigsFromConfig(getConfig());
|
|
81
|
+
const availability = getLaneAvailability(wus, { laneConfigs });
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(chalk.bold('Lane Availability:'));
|
|
84
|
+
console.log(formatLaneAvailability(availability, laneConfigs));
|
|
57
85
|
}
|
|
58
86
|
catch (err) {
|
|
59
|
-
console.error(chalk.red(`${LOG_PREFIX} Error: ${err
|
|
87
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${getErrorMessage(err)}`));
|
|
60
88
|
process.exit(EXIT_CODES.ERROR);
|
|
61
89
|
}
|
|
62
90
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
2
3
|
/**
|
|
3
4
|
* Orchestrate Initiative CLI
|
|
4
5
|
*
|
|
@@ -11,8 +12,12 @@
|
|
|
11
12
|
*/
|
|
12
13
|
import { Command } from 'commander';
|
|
13
14
|
import chalk from 'chalk';
|
|
14
|
-
import { loadInitiativeWUs, loadMultipleInitiatives,
|
|
15
|
+
import { loadInitiativeWUs, loadMultipleInitiatives, buildExecutionPlanWithLockPolicy, resolveLaneConfigsFromConfig, formatExecutionPlan, formatExecutionPlanWithEmbeddedSpawns, calculateProgress, formatProgress, buildCheckpointWave, formatCheckpointOutput, validateCheckpointFlags, resolveCheckpointModeAsync, LOG_PREFIX, } from '@lumenflow/initiatives';
|
|
15
16
|
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
17
|
+
import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
|
|
18
|
+
function getErrorMessage(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
16
21
|
const program = new Command()
|
|
17
22
|
.name('orchestrate-initiative')
|
|
18
23
|
.description('Orchestrate initiative execution with parallel agent spawning')
|
|
@@ -28,7 +33,7 @@ const program = new Command()
|
|
|
28
33
|
validateCheckpointFlags({ checkpointPerWave, dryRun, noCheckpoint });
|
|
29
34
|
}
|
|
30
35
|
catch (error) {
|
|
31
|
-
console.error(chalk.red(`${LOG_PREFIX} Error: ${error
|
|
36
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${getErrorMessage(error)}`));
|
|
32
37
|
process.exit(EXIT_CODES.ERROR);
|
|
33
38
|
}
|
|
34
39
|
if (!initIds || initIds.length === 0) {
|
|
@@ -76,7 +81,8 @@ const program = new Command()
|
|
|
76
81
|
return;
|
|
77
82
|
}
|
|
78
83
|
console.log(chalk.cyan(`${LOG_PREFIX} Building execution plan...`));
|
|
79
|
-
const
|
|
84
|
+
const laneConfigs = resolveLaneConfigsFromConfig(getConfig());
|
|
85
|
+
const plan = buildExecutionPlanWithLockPolicy(wus, { laneConfigs });
|
|
80
86
|
if (plan.waves.length === 0) {
|
|
81
87
|
console.log(chalk.green(`${LOG_PREFIX} All WUs are complete! Nothing to execute.`));
|
|
82
88
|
return;
|
|
@@ -99,7 +105,7 @@ const program = new Command()
|
|
|
99
105
|
console.log(chalk.cyan('Copy the spawn XML above to execute agents.'));
|
|
100
106
|
}
|
|
101
107
|
catch (error) {
|
|
102
|
-
console.error(chalk.red(`${LOG_PREFIX} Error: ${error
|
|
108
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${getErrorMessage(error)}`));
|
|
103
109
|
process.exit(EXIT_CODES.ERROR);
|
|
104
110
|
}
|
|
105
111
|
});
|