@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.
Files changed (31) hide show
  1. package/README.md +23 -2
  2. package/dist/__tests__/gates-integration-tests.test.js +112 -0
  3. package/dist/__tests__/init.test.js +225 -0
  4. package/dist/__tests__/safe-git.test.js +4 -4
  5. package/dist/__tests__/wu-create-required-fields.test.js +22 -0
  6. package/dist/__tests__/wu-create.test.js +72 -0
  7. package/dist/gates.js +6 -8
  8. package/dist/hooks/enforcement-generator.js +256 -5
  9. package/dist/hooks/enforcement-sync.js +52 -6
  10. package/dist/init.js +195 -2
  11. package/dist/mem-recover.js +221 -0
  12. package/dist/orchestrate-initiative.js +19 -1
  13. package/dist/state-doctor-fix.js +36 -1
  14. package/dist/state-doctor.js +10 -6
  15. package/dist/wu-create.js +37 -15
  16. package/dist/wu-recover.js +53 -2
  17. package/dist/wu-spawn.js +2 -2
  18. package/package.json +6 -6
  19. package/templates/core/.mcp.json.template +8 -0
  20. package/templates/core/LUMENFLOW.md.template +24 -0
  21. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +47 -0
  22. package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +183 -0
  23. package/templates/core/ai/onboarding/quick-ref-commands.md.template +68 -55
  24. package/templates/core/ai/onboarding/release-process.md.template +58 -4
  25. package/templates/core/ai/onboarding/starting-prompt.md.template +67 -3
  26. package/templates/core/ai/onboarding/vendor-support.md.template +73 -0
  27. package/templates/core/scripts/safe-git.template +29 -0
  28. package/templates/vendors/claude/.claude/hooks/pre-compact-checkpoint.sh +102 -0
  29. package/templates/vendors/claude/.claude/hooks/session-start-recovery.sh +74 -0
  30. package/templates/vendors/claude/.claude/settings.json.template +42 -0
  31. 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` - run quality 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 --unread
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 .beacon directory
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, '.beacon', FORCE_BYPASSES_LOG);
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, '.beacon', FORCE_BYPASSES_LOG);
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, '.beacon', FORCE_BYPASSES_LOG);
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
- // Handle apps/name/...
298
- if (normalized.startsWith('apps/')) {
299
- const parts = normalized.slice('apps/'.length).split('/');
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
- const result = run(`RUN_INTEGRATION_TESTS=1 ${pnpmCmd('vitest', 'run', "--include='**/*.integration.*'", "--include='**/golden-*.test.*'")}`, { agentLog });
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,