@lumenflow/cli 2.9.0 → 2.11.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/README.md +23 -2
- package/dist/__tests__/gates-integration-tests.test.js +112 -0
- package/dist/__tests__/init.test.js +225 -0
- package/dist/__tests__/safe-git.test.js +4 -4
- package/dist/__tests__/wu-create-required-fields.test.js +22 -0
- package/dist/__tests__/wu-create.test.js +72 -0
- package/dist/gates.js +6 -8
- package/dist/hooks/enforcement-generator.js +256 -5
- package/dist/hooks/enforcement-sync.js +52 -6
- package/dist/init.js +195 -2
- package/dist/mem-recover.js +221 -0
- package/dist/orchestrate-initiative.js +19 -1
- package/dist/state-doctor-fix.js +36 -1
- package/dist/state-doctor.js +10 -6
- package/dist/wu-create.js +37 -15
- package/dist/wu-recover.js +53 -2
- package/dist/wu-spawn.js +2 -2
- package/package.json +6 -6
- package/templates/core/.mcp.json.template +8 -0
- package/templates/core/LUMENFLOW.md.template +24 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +47 -0
- package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +183 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +68 -55
- package/templates/core/ai/onboarding/release-process.md.template +58 -4
- package/templates/core/ai/onboarding/starting-prompt.md.template +67 -3
- package/templates/core/ai/onboarding/vendor-support.md.template +73 -0
- package/templates/core/scripts/safe-git.template +29 -0
- package/templates/vendors/claude/.claude/hooks/pre-compact-checkpoint.sh +102 -0
- package/templates/vendors/claude/.claude/hooks/session-start-recovery.sh +74 -0
- package/templates/vendors/claude/.claude/settings.json.template +42 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +23 -6
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ This package provides CLI commands for the LumenFlow workflow framework, includi
|
|
|
134
134
|
| Command | Description |
|
|
135
135
|
| ----------------- | ------------------------------------------------------------------------------------------ |
|
|
136
136
|
| `gates` | Run quality gates with support for docs-only mode, incremental linting, and tiered testing |
|
|
137
|
-
| `lumenflow-gates` | Alias for `gates`
|
|
137
|
+
| `lumenflow-gates` | Alias for `gates` command |
|
|
138
138
|
|
|
139
139
|
### System & Setup
|
|
140
140
|
|
|
@@ -202,7 +202,7 @@ pnpm wu:done --id WU-123
|
|
|
202
202
|
|
|
203
203
|
# Memory operations
|
|
204
204
|
pnpm mem:checkpoint "Completed port definitions" --wu WU-123
|
|
205
|
-
pnpm mem:inbox --
|
|
205
|
+
pnpm mem:inbox --since 10m
|
|
206
206
|
|
|
207
207
|
# Initiative management
|
|
208
208
|
pnpm initiative:status INIT-007
|
|
@@ -245,6 +245,27 @@ The CLI integrates with other LumenFlow packages:
|
|
|
245
245
|
- `@lumenflow/agent` - Agent session management
|
|
246
246
|
- `@lumenflow/initiatives` - Initiative tracking
|
|
247
247
|
|
|
248
|
+
## MCP Server Setup (Claude Code)
|
|
249
|
+
|
|
250
|
+
LumenFlow provides an MCP (Model Context Protocol) server for deep integration with Claude Code.
|
|
251
|
+
|
|
252
|
+
When you run `lumenflow init --client claude`, a `.mcp.json` is automatically created:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"mcpServers": {
|
|
257
|
+
"lumenflow": {
|
|
258
|
+
"command": "npx",
|
|
259
|
+
"args": ["@lumenflow/mcp"]
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The `@lumenflow/mcp` server provides tools for WU lifecycle, memory coordination, and lane management directly within Claude Code.
|
|
266
|
+
|
|
267
|
+
See [AI Integrations](https://lumenflow.dev/guides/ai-integrations) for full MCP documentation.
|
|
268
|
+
|
|
248
269
|
## Documentation
|
|
249
270
|
|
|
250
271
|
For complete documentation, see [lumenflow.dev](https://lumenflow.dev/reference/cli).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file gates-integration-tests.test.ts
|
|
3
|
+
* @description Tests for gates infrastructure fixes (WU-1415)
|
|
4
|
+
*
|
|
5
|
+
* Bug 1: vitest --include is not a valid CLI option
|
|
6
|
+
* Bug 2: docs-only turbo filter uses directory names instead of package names
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { extractPackagesFromCodePaths, resolveDocsOnlyTestPlan } from '../gates.js';
|
|
10
|
+
describe('WU-1415: Gates infrastructure fixes', () => {
|
|
11
|
+
describe('Bug 1: vitest integration test command', () => {
|
|
12
|
+
it('should NOT use --include flag (vitest does not support it)', async () => {
|
|
13
|
+
// Import the module to inspect the command construction
|
|
14
|
+
// We need to verify that runIntegrationTests uses valid vitest syntax
|
|
15
|
+
//
|
|
16
|
+
// vitest run accepts positional glob patterns, NOT --include flags:
|
|
17
|
+
// WRONG: vitest run --include='**/*.integration.*'
|
|
18
|
+
// RIGHT: vitest run '**/*.integration.*'
|
|
19
|
+
//
|
|
20
|
+
// This test ensures we're using the correct vitest CLI syntax
|
|
21
|
+
const gatesModule = await import('../gates.js');
|
|
22
|
+
// The command construction happens in runIntegrationTests
|
|
23
|
+
// We can't directly test the internal function, but we can verify
|
|
24
|
+
// via the module's exported constants or by checking the implementation
|
|
25
|
+
// doesn't contain --include
|
|
26
|
+
// Read the source to verify no --include in vitest commands
|
|
27
|
+
const fs = await import('fs');
|
|
28
|
+
const path = await import('path');
|
|
29
|
+
const gatesPath = path.join(import.meta.dirname, '..', 'gates.ts');
|
|
30
|
+
const source = fs.readFileSync(gatesPath, 'utf-8');
|
|
31
|
+
// Find the runIntegrationTests function and check it doesn't use --include
|
|
32
|
+
const integrationTestMatch = source.match(/function runIntegrationTests[\s\S]*?^}/m);
|
|
33
|
+
if (integrationTestMatch) {
|
|
34
|
+
const functionBody = integrationTestMatch[0];
|
|
35
|
+
// vitest run should NOT have --include flags
|
|
36
|
+
expect(functionBody).not.toMatch(/vitest.*--include/);
|
|
37
|
+
// Instead, glob patterns should be positional args or via proper config
|
|
38
|
+
// The fix should pass patterns directly: vitest run 'pattern1' 'pattern2'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('Bug 2: docs-only turbo filter', () => {
|
|
43
|
+
describe('extractPackagesFromCodePaths', () => {
|
|
44
|
+
it('should extract scoped package names from packages/ paths', () => {
|
|
45
|
+
const codePaths = [
|
|
46
|
+
'packages/@lumenflow/cli/src/gates.ts',
|
|
47
|
+
'packages/@lumenflow/core/src/index.ts',
|
|
48
|
+
];
|
|
49
|
+
const packages = extractPackagesFromCodePaths(codePaths);
|
|
50
|
+
expect(packages).toContain('@lumenflow/cli');
|
|
51
|
+
expect(packages).toContain('@lumenflow/core');
|
|
52
|
+
});
|
|
53
|
+
it('should return empty array for apps/ paths that are not real turbo packages', () => {
|
|
54
|
+
// apps/docs/ directory name is 'docs' but the turbo package might be
|
|
55
|
+
// named differently (e.g., '@lumenflow/docs' or not exist at all)
|
|
56
|
+
//
|
|
57
|
+
// The current implementation returns 'docs' which causes turbo to fail:
|
|
58
|
+
// "No package found with name 'docs' in workspace"
|
|
59
|
+
//
|
|
60
|
+
// Fix: Either lookup actual package.json name or skip apps
|
|
61
|
+
const codePaths = ['apps/docs/src/content/docs/', 'apps/github-app/'];
|
|
62
|
+
const packages = extractPackagesFromCodePaths(codePaths);
|
|
63
|
+
// Current buggy behavior returns ['docs', 'github-app']
|
|
64
|
+
// Fixed behavior should either:
|
|
65
|
+
// - Return actual package names from package.json
|
|
66
|
+
// - Or return empty array (apps don't have turbo test tasks)
|
|
67
|
+
//
|
|
68
|
+
// For now, the fix should skip apps that aren't valid turbo packages
|
|
69
|
+
// because apps/docs has no test script and apps/github-app was deleted
|
|
70
|
+
expect(packages).not.toContain('docs');
|
|
71
|
+
expect(packages).not.toContain('github-app');
|
|
72
|
+
});
|
|
73
|
+
it('should handle mixed code_paths (packages + apps + docs)', () => {
|
|
74
|
+
const codePaths = [
|
|
75
|
+
'packages/@lumenflow/cli/src/file.ts',
|
|
76
|
+
'apps/docs/astro.config.mjs',
|
|
77
|
+
'docs/DISTRIBUTION.md',
|
|
78
|
+
];
|
|
79
|
+
const packages = extractPackagesFromCodePaths(codePaths);
|
|
80
|
+
// Should include the real package
|
|
81
|
+
expect(packages).toContain('@lumenflow/cli');
|
|
82
|
+
// Should NOT include apps (no valid turbo package)
|
|
83
|
+
expect(packages).not.toContain('docs');
|
|
84
|
+
// Should NOT include docs/ (not a package)
|
|
85
|
+
expect(packages.length).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
it('should return empty array for pure docs paths', () => {
|
|
88
|
+
const codePaths = ['docs/01-product/product-lines.md', 'docs/DISTRIBUTION.md'];
|
|
89
|
+
const packages = extractPackagesFromCodePaths(codePaths);
|
|
90
|
+
expect(packages).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('resolveDocsOnlyTestPlan', () => {
|
|
94
|
+
it('should return skip mode for pure documentation WUs', () => {
|
|
95
|
+
const plan = resolveDocsOnlyTestPlan({
|
|
96
|
+
codePaths: ['docs/README.md', 'apps/docs/content/'],
|
|
97
|
+
});
|
|
98
|
+
expect(plan.mode).toBe('skip');
|
|
99
|
+
expect(plan.packages).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
it('should return filtered mode only for valid package paths', () => {
|
|
102
|
+
const plan = resolveDocsOnlyTestPlan({
|
|
103
|
+
codePaths: ['packages/@lumenflow/cli/src/gates.ts', 'apps/docs/content/'],
|
|
104
|
+
});
|
|
105
|
+
expect(plan.mode).toBe('filtered');
|
|
106
|
+
expect(plan.packages).toContain('@lumenflow/cli');
|
|
107
|
+
// apps/docs should not be included
|
|
108
|
+
expect(plan.packages).not.toContain('docs');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -740,4 +740,229 @@ describe('lumenflow init', () => {
|
|
|
740
740
|
});
|
|
741
741
|
});
|
|
742
742
|
});
|
|
743
|
+
// WU-1408: safe-git and pre-commit hook scaffolding
|
|
744
|
+
describe('WU-1408: safe-git and pre-commit scaffolding', () => {
|
|
745
|
+
describe('safe-git wrapper', () => {
|
|
746
|
+
it('should scaffold scripts/safe-git', async () => {
|
|
747
|
+
const options = {
|
|
748
|
+
force: false,
|
|
749
|
+
full: true,
|
|
750
|
+
};
|
|
751
|
+
await scaffoldProject(tempDir, options);
|
|
752
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
753
|
+
expect(fs.existsSync(safeGitPath)).toBe(true);
|
|
754
|
+
});
|
|
755
|
+
it('should make safe-git executable', async () => {
|
|
756
|
+
const options = {
|
|
757
|
+
force: false,
|
|
758
|
+
full: true,
|
|
759
|
+
};
|
|
760
|
+
await scaffoldProject(tempDir, options);
|
|
761
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
762
|
+
const stats = fs.statSync(safeGitPath);
|
|
763
|
+
// Check for executable bit (owner, group, or other)
|
|
764
|
+
// eslint-disable-next-line no-bitwise
|
|
765
|
+
const isExecutable = (stats.mode & 0o111) !== 0;
|
|
766
|
+
expect(isExecutable).toBe(true);
|
|
767
|
+
});
|
|
768
|
+
it('should include worktree remove block in safe-git', async () => {
|
|
769
|
+
const options = {
|
|
770
|
+
force: false,
|
|
771
|
+
full: true,
|
|
772
|
+
};
|
|
773
|
+
await scaffoldProject(tempDir, options);
|
|
774
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
775
|
+
const content = fs.readFileSync(safeGitPath, 'utf-8');
|
|
776
|
+
expect(content).toContain('worktree');
|
|
777
|
+
expect(content).toContain('remove');
|
|
778
|
+
expect(content).toContain('BLOCKED');
|
|
779
|
+
});
|
|
780
|
+
it('should scaffold safe-git even in minimal mode', async () => {
|
|
781
|
+
const options = {
|
|
782
|
+
force: false,
|
|
783
|
+
full: false, // minimal mode
|
|
784
|
+
};
|
|
785
|
+
await scaffoldProject(tempDir, options);
|
|
786
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
787
|
+
expect(fs.existsSync(safeGitPath)).toBe(true);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
describe('pre-commit hook', () => {
|
|
791
|
+
it('should scaffold .husky/pre-commit', async () => {
|
|
792
|
+
const options = {
|
|
793
|
+
force: false,
|
|
794
|
+
full: true,
|
|
795
|
+
};
|
|
796
|
+
await scaffoldProject(tempDir, options);
|
|
797
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
798
|
+
expect(fs.existsSync(preCommitPath)).toBe(true);
|
|
799
|
+
});
|
|
800
|
+
it('should make pre-commit executable', async () => {
|
|
801
|
+
const options = {
|
|
802
|
+
force: false,
|
|
803
|
+
full: true,
|
|
804
|
+
};
|
|
805
|
+
await scaffoldProject(tempDir, options);
|
|
806
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
807
|
+
const stats = fs.statSync(preCommitPath);
|
|
808
|
+
// eslint-disable-next-line no-bitwise
|
|
809
|
+
const isExecutable = (stats.mode & 0o111) !== 0;
|
|
810
|
+
expect(isExecutable).toBe(true);
|
|
811
|
+
});
|
|
812
|
+
it('should NOT run pnpm test in pre-commit hook', async () => {
|
|
813
|
+
const options = {
|
|
814
|
+
force: false,
|
|
815
|
+
full: true,
|
|
816
|
+
};
|
|
817
|
+
await scaffoldProject(tempDir, options);
|
|
818
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
819
|
+
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
|
820
|
+
// The pre-commit hook should NOT assume pnpm test exists
|
|
821
|
+
expect(content).not.toContain('pnpm test');
|
|
822
|
+
expect(content).not.toContain('npm test');
|
|
823
|
+
});
|
|
824
|
+
it('should block commits to main/master in pre-commit', async () => {
|
|
825
|
+
const options = {
|
|
826
|
+
force: false,
|
|
827
|
+
full: true,
|
|
828
|
+
};
|
|
829
|
+
await scaffoldProject(tempDir, options);
|
|
830
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
831
|
+
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
|
832
|
+
// Should protect main branch
|
|
833
|
+
expect(content).toContain('main');
|
|
834
|
+
expect(content).toContain('BLOCK');
|
|
835
|
+
});
|
|
836
|
+
it('should scaffold pre-commit even in minimal mode', async () => {
|
|
837
|
+
const options = {
|
|
838
|
+
force: false,
|
|
839
|
+
full: false, // minimal mode
|
|
840
|
+
};
|
|
841
|
+
await scaffoldProject(tempDir, options);
|
|
842
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
843
|
+
expect(fs.existsSync(preCommitPath)).toBe(true);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
// WU-1413: MCP server configuration scaffolding
|
|
848
|
+
describe('WU-1413: .mcp.json scaffolding', () => {
|
|
849
|
+
const MCP_JSON_FILE = '.mcp.json';
|
|
850
|
+
describe('.mcp.json creation with --client claude', () => {
|
|
851
|
+
it('should scaffold .mcp.json when --client claude is used', async () => {
|
|
852
|
+
const options = {
|
|
853
|
+
force: false,
|
|
854
|
+
full: false,
|
|
855
|
+
client: 'claude',
|
|
856
|
+
};
|
|
857
|
+
await scaffoldProject(tempDir, options);
|
|
858
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
859
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(true);
|
|
860
|
+
});
|
|
861
|
+
it('should include lumenflow MCP server configuration', async () => {
|
|
862
|
+
const options = {
|
|
863
|
+
force: false,
|
|
864
|
+
full: false,
|
|
865
|
+
client: 'claude',
|
|
866
|
+
};
|
|
867
|
+
await scaffoldProject(tempDir, options);
|
|
868
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
869
|
+
const content = fs.readFileSync(mcpJsonPath, 'utf-8');
|
|
870
|
+
const mcpConfig = JSON.parse(content);
|
|
871
|
+
// Should have mcpServers key
|
|
872
|
+
expect(mcpConfig.mcpServers).toBeDefined();
|
|
873
|
+
// Should have lumenflow server entry
|
|
874
|
+
expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
|
|
875
|
+
// Should use npx command
|
|
876
|
+
expect(mcpConfig.mcpServers.lumenflow.command).toBe('npx');
|
|
877
|
+
// Should reference @lumenflow/mcp package
|
|
878
|
+
expect(mcpConfig.mcpServers.lumenflow.args).toContain('@lumenflow/mcp');
|
|
879
|
+
});
|
|
880
|
+
it('should be valid JSON', async () => {
|
|
881
|
+
const options = {
|
|
882
|
+
force: false,
|
|
883
|
+
full: false,
|
|
884
|
+
client: 'claude',
|
|
885
|
+
};
|
|
886
|
+
await scaffoldProject(tempDir, options);
|
|
887
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
888
|
+
const content = fs.readFileSync(mcpJsonPath, 'utf-8');
|
|
889
|
+
// Should parse without error
|
|
890
|
+
expect(() => JSON.parse(content)).not.toThrow();
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
describe('.mcp.json creation with --client all', () => {
|
|
894
|
+
it('should scaffold .mcp.json when --client all is used', async () => {
|
|
895
|
+
const options = {
|
|
896
|
+
force: false,
|
|
897
|
+
full: false,
|
|
898
|
+
client: 'all',
|
|
899
|
+
};
|
|
900
|
+
await scaffoldProject(tempDir, options);
|
|
901
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
902
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(true);
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
describe('.mcp.json NOT created with other clients', () => {
|
|
906
|
+
it('should NOT scaffold .mcp.json when --client none is used', async () => {
|
|
907
|
+
const options = {
|
|
908
|
+
force: false,
|
|
909
|
+
full: false,
|
|
910
|
+
client: 'none',
|
|
911
|
+
};
|
|
912
|
+
await scaffoldProject(tempDir, options);
|
|
913
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
914
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
915
|
+
});
|
|
916
|
+
it('should NOT scaffold .mcp.json when --client cursor is used', async () => {
|
|
917
|
+
const options = {
|
|
918
|
+
force: false,
|
|
919
|
+
full: false,
|
|
920
|
+
client: 'cursor',
|
|
921
|
+
};
|
|
922
|
+
await scaffoldProject(tempDir, options);
|
|
923
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
924
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
925
|
+
});
|
|
926
|
+
it('should NOT scaffold .mcp.json when no client is specified', async () => {
|
|
927
|
+
const options = {
|
|
928
|
+
force: false,
|
|
929
|
+
full: false,
|
|
930
|
+
};
|
|
931
|
+
await scaffoldProject(tempDir, options);
|
|
932
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
933
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
describe('.mcp.json file modes', () => {
|
|
937
|
+
it('should skip .mcp.json if it already exists (skip mode)', async () => {
|
|
938
|
+
// Create existing .mcp.json
|
|
939
|
+
const existingContent = '{"mcpServers":{"custom":{}}}';
|
|
940
|
+
fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), existingContent);
|
|
941
|
+
const options = {
|
|
942
|
+
force: false,
|
|
943
|
+
full: false,
|
|
944
|
+
client: 'claude',
|
|
945
|
+
};
|
|
946
|
+
const result = await scaffoldProject(tempDir, options);
|
|
947
|
+
expect(result.skipped).toContain(MCP_JSON_FILE);
|
|
948
|
+
// Content should not be changed
|
|
949
|
+
const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
|
|
950
|
+
expect(content).toBe(existingContent);
|
|
951
|
+
});
|
|
952
|
+
it('should overwrite .mcp.json in force mode', async () => {
|
|
953
|
+
// Create existing .mcp.json
|
|
954
|
+
fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), '{"custom":true}');
|
|
955
|
+
const options = {
|
|
956
|
+
force: true,
|
|
957
|
+
full: false,
|
|
958
|
+
client: 'claude',
|
|
959
|
+
};
|
|
960
|
+
const result = await scaffoldProject(tempDir, options);
|
|
961
|
+
expect(result.created).toContain(MCP_JSON_FILE);
|
|
962
|
+
const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
|
|
963
|
+
const mcpConfig = JSON.parse(content);
|
|
964
|
+
expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
});
|
|
743
968
|
});
|
|
@@ -13,7 +13,7 @@ const USER_NAME_CONFIG = 'user.name';
|
|
|
13
13
|
const TEST_EMAIL = 'test@test.com';
|
|
14
14
|
const TEST_USERNAME = 'Test';
|
|
15
15
|
const FORCE_BYPASSES_LOG = 'force-bypasses.log';
|
|
16
|
-
// Create a temporary directory for testing to avoid polluting the real .
|
|
16
|
+
// Create a temporary directory for testing to avoid polluting the real .lumenflow directory
|
|
17
17
|
const createTempDir = () => {
|
|
18
18
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'safe-git-test-'));
|
|
19
19
|
};
|
|
@@ -128,7 +128,7 @@ describe('safe-git', () => {
|
|
|
128
128
|
env: { ...process.env, LUMENFLOW_FORCE: '1' },
|
|
129
129
|
});
|
|
130
130
|
// Check that the force bypass log exists and contains the entry
|
|
131
|
-
const bypassLog = path.join(testRepo, '.
|
|
131
|
+
const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
|
|
132
132
|
expect(fs.existsSync(bypassLog)).toBe(true);
|
|
133
133
|
const logContent = fs.readFileSync(bypassLog, 'utf-8');
|
|
134
134
|
expect(logContent).toContain('reset --hard');
|
|
@@ -155,7 +155,7 @@ describe('safe-git', () => {
|
|
|
155
155
|
encoding: 'utf-8',
|
|
156
156
|
env: { ...process.env, LUMENFLOW_FORCE: '1', LUMENFLOW_FORCE_REASON: testReason },
|
|
157
157
|
});
|
|
158
|
-
const bypassLog = path.join(testRepo, '.
|
|
158
|
+
const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
|
|
159
159
|
const logContent = fs.readFileSync(bypassLog, 'utf-8');
|
|
160
160
|
expect(logContent).toContain(testReason);
|
|
161
161
|
});
|
|
@@ -182,7 +182,7 @@ describe('safe-git', () => {
|
|
|
182
182
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
183
183
|
});
|
|
184
184
|
// Check the bypasslog for the NO_REASON marker
|
|
185
|
-
const bypassLog = path.join(testRepo, '.
|
|
185
|
+
const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
|
|
186
186
|
const logContent = fs.readFileSync(bypassLog, 'utf-8');
|
|
187
187
|
expect(logContent).toContain('NO_REASON');
|
|
188
188
|
});
|
|
@@ -82,6 +82,28 @@ describe('wu:create required field aggregation (WU-1366)', () => {
|
|
|
82
82
|
expect(result.valid).toBe(false);
|
|
83
83
|
expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
|
|
84
84
|
});
|
|
85
|
+
it('should treat empty spec-refs array as missing for feature WUs', () => {
|
|
86
|
+
const result = validateCreateSpec({
|
|
87
|
+
id: TEST_WU_ID,
|
|
88
|
+
lane: TEST_LANE,
|
|
89
|
+
title: 'Test WU',
|
|
90
|
+
priority: 'P2',
|
|
91
|
+
type: 'feature',
|
|
92
|
+
opts: {
|
|
93
|
+
description: VALID_DESCRIPTION,
|
|
94
|
+
acceptance: TEST_ACCEPTANCE,
|
|
95
|
+
exposure: 'backend-only',
|
|
96
|
+
codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
|
|
97
|
+
testPathsUnit: [
|
|
98
|
+
'packages/@lumenflow/cli/src/__tests__/wu-create-required-fields.test.ts',
|
|
99
|
+
],
|
|
100
|
+
specRefs: [],
|
|
101
|
+
strict: false,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(result.valid).toBe(false);
|
|
105
|
+
expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
|
|
106
|
+
});
|
|
85
107
|
it('should return all errors at once, not fail on first error', () => {
|
|
86
108
|
const result = validateCreateSpec({
|
|
87
109
|
id: TEST_WU_ID,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file wu-create.test.ts
|
|
3
|
+
* Tests for wu:create helpers and warnings (WU-1429)
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { buildWUContent, collectInitiativeWarnings } from '../wu-create.js';
|
|
7
|
+
const BASE_WU = {
|
|
8
|
+
id: 'WU-1429',
|
|
9
|
+
lane: 'Framework: CLI',
|
|
10
|
+
title: 'Test WU',
|
|
11
|
+
priority: 'P2',
|
|
12
|
+
type: 'feature',
|
|
13
|
+
created: '2026-02-04',
|
|
14
|
+
opts: {
|
|
15
|
+
description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
|
|
16
|
+
acceptance: ['Acceptance criterion'],
|
|
17
|
+
exposure: 'backend-only',
|
|
18
|
+
codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
|
|
19
|
+
testPathsUnit: ['packages/@lumenflow/cli/src/__tests__/wu-create.test.ts'],
|
|
20
|
+
specRefs: ['lumenflow://plans/WU-1429-plan.md'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
describe('wu:create helpers (WU-1429)', () => {
|
|
24
|
+
it('should persist notes when provided', () => {
|
|
25
|
+
const wu = buildWUContent({
|
|
26
|
+
...BASE_WU,
|
|
27
|
+
opts: {
|
|
28
|
+
...BASE_WU.opts,
|
|
29
|
+
notes: 'Implementation notes for test',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
expect(wu.notes).toBe('Implementation notes for test');
|
|
33
|
+
});
|
|
34
|
+
it('should warn when initiative has phases but no --phase is provided', () => {
|
|
35
|
+
const warnings = collectInitiativeWarnings({
|
|
36
|
+
initiativeId: 'INIT-TEST',
|
|
37
|
+
initiativeDoc: {
|
|
38
|
+
phases: [{ id: 1, title: 'Phase 1' }],
|
|
39
|
+
},
|
|
40
|
+
phase: undefined,
|
|
41
|
+
specRefs: ['lumenflow://plans/WU-1429-plan.md'],
|
|
42
|
+
});
|
|
43
|
+
expect(warnings).toEqual(expect.arrayContaining([
|
|
44
|
+
'Initiative INIT-TEST has phases defined. Consider adding --phase to link this WU to a phase.',
|
|
45
|
+
]));
|
|
46
|
+
});
|
|
47
|
+
it('should warn when initiative has related_plan but no spec_refs', () => {
|
|
48
|
+
const warnings = collectInitiativeWarnings({
|
|
49
|
+
initiativeId: 'INIT-TEST',
|
|
50
|
+
initiativeDoc: {
|
|
51
|
+
related_plan: 'lumenflow://plans/INIT-TEST-plan.md',
|
|
52
|
+
},
|
|
53
|
+
phase: '1',
|
|
54
|
+
specRefs: [],
|
|
55
|
+
});
|
|
56
|
+
expect(warnings).toEqual(expect.arrayContaining([
|
|
57
|
+
'Initiative INIT-TEST has related_plan (lumenflow://plans/INIT-TEST-plan.md). Consider adding --spec-refs to link this WU to the plan.',
|
|
58
|
+
]));
|
|
59
|
+
});
|
|
60
|
+
it('should not warn when phase and spec_refs are provided', () => {
|
|
61
|
+
const warnings = collectInitiativeWarnings({
|
|
62
|
+
initiativeId: 'INIT-TEST',
|
|
63
|
+
initiativeDoc: {
|
|
64
|
+
phases: [{ id: 1, title: 'Phase 1' }],
|
|
65
|
+
related_plan: 'lumenflow://plans/INIT-TEST-plan.md',
|
|
66
|
+
},
|
|
67
|
+
phase: '1',
|
|
68
|
+
specRefs: ['lumenflow://plans/WU-1429-plan.md'],
|
|
69
|
+
});
|
|
70
|
+
expect(warnings).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/dist/gates.js
CHANGED
|
@@ -294,13 +294,9 @@ function extractPackageFromPath(codePath) {
|
|
|
294
294
|
return parts[0];
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (parts[0]) {
|
|
301
|
-
return parts[0];
|
|
302
|
-
}
|
|
303
|
-
}
|
|
297
|
+
// WU-1415: Skip apps/ paths - they aren't valid turbo packages for test filtering
|
|
298
|
+
// apps/ directories (e.g., apps/docs, apps/github-app) don't have turbo test tasks
|
|
299
|
+
// and using directory names as --filter args causes "No package found" errors
|
|
304
300
|
return null;
|
|
305
301
|
}
|
|
306
302
|
/**
|
|
@@ -1004,7 +1000,9 @@ async function runIntegrationTests({ agentLog, } = {}) {
|
|
|
1004
1000
|
};
|
|
1005
1001
|
try {
|
|
1006
1002
|
logLine('\n> Integration tests (high-risk changes detected)\n');
|
|
1007
|
-
|
|
1003
|
+
// WU-1415: vitest doesn't support --include flag
|
|
1004
|
+
// Pass glob patterns as positional arguments instead
|
|
1005
|
+
const result = run(`RUN_INTEGRATION_TESTS=1 ${pnpmCmd('vitest', 'run', "'**/*.integration.*'", "'**/golden-*.test.*'")}`, { agentLog });
|
|
1008
1006
|
const duration = Date.now() - start;
|
|
1009
1007
|
return {
|
|
1010
1008
|
ok: result.ok,
|