@lumenflow/cli 2.10.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 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 with support for docs-only mode and tiered testing |
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,
package/dist/init.js CHANGED
@@ -699,6 +699,16 @@ pnpm wu:done --id WU-XXX
699
699
  `;
700
700
  // Template for .aider.conf.yml
701
701
  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`;
702
+ // WU-1413: Template for .mcp.json (MCP server configuration for Claude Code)
703
+ const MCP_JSON_TEMPLATE = `{
704
+ "mcpServers": {
705
+ "lumenflow": {
706
+ "command": "npx",
707
+ "args": ["@lumenflow/mcp"]
708
+ }
709
+ }
710
+ }
711
+ `;
702
712
  // Template for docs/04-operations/tasks/backlog.md
703
713
  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`;
704
714
  // Template for docs/04-operations/tasks/status.md
@@ -2364,6 +2374,9 @@ export async function scaffoldProject(targetDir, options) {
2364
2374
  await createFile(path.join(targetDir, LUMENFLOW_AGENTS_DIR, '.gitkeep'), '', options.force ? 'force' : 'skip', result, targetDir);
2365
2375
  // WU-1342: Create .gitignore with required exclusions
2366
2376
  await scaffoldGitignore(targetDir, options, result);
2377
+ // WU-1408: Scaffold safe-git wrapper and pre-commit hook
2378
+ // These are core safety components needed for all projects
2379
+ await scaffoldSafetyScripts(targetDir, options, result);
2367
2380
  // Optional: full docs scaffolding
2368
2381
  if (options.full) {
2369
2382
  await scaffoldFullDocs(targetDir, options, result, tokenDefaults);
@@ -2490,6 +2503,128 @@ const LUMENFLOW_SCRIPTS = {
2490
2503
  gates: 'gates',
2491
2504
  'gates:docs': 'gates --docs-only',
2492
2505
  };
2506
+ /** WU-1408: Safety script path constants */
2507
+ const SCRIPTS_DIR = 'scripts';
2508
+ const SAFE_GIT_FILE = 'safe-git';
2509
+ const HUSKY_DIR = '.husky';
2510
+ const PRE_COMMIT_FILE = 'pre-commit';
2511
+ const SAFE_GIT_TEMPLATE_PATH = 'core/scripts/safe-git.template';
2512
+ const PRE_COMMIT_TEMPLATE_PATH = 'core/.husky/pre-commit.template';
2513
+ /**
2514
+ * WU-1408: Scaffold safety scripts (safe-git wrapper and pre-commit hook)
2515
+ * These are core safety components needed for LumenFlow enforcement:
2516
+ * - scripts/safe-git: Blocks dangerous git operations (e.g., manual worktree remove)
2517
+ * - .husky/pre-commit: Blocks direct commits to main/master, enforces WU workflow
2518
+ *
2519
+ * Both scripts are scaffolded in all modes (full and minimal) because they are
2520
+ * required for lumenflow-doctor to pass.
2521
+ */
2522
+ async function scaffoldSafetyScripts(targetDir, options, result) {
2523
+ const fileMode = getFileMode(options);
2524
+ // Scaffold scripts/safe-git
2525
+ const safeGitPath = path.join(targetDir, SCRIPTS_DIR, SAFE_GIT_FILE);
2526
+ try {
2527
+ const safeGitTemplate = loadTemplate(SAFE_GIT_TEMPLATE_PATH);
2528
+ await createExecutableScript(safeGitPath, safeGitTemplate, fileMode, result, targetDir);
2529
+ }
2530
+ catch {
2531
+ // Fallback to hardcoded template if template file not found
2532
+ await createExecutableScript(safeGitPath, SAFE_GIT_TEMPLATE, fileMode, result, targetDir);
2533
+ }
2534
+ // Scaffold .husky/pre-commit
2535
+ const preCommitPath = path.join(targetDir, HUSKY_DIR, PRE_COMMIT_FILE);
2536
+ try {
2537
+ const preCommitTemplate = loadTemplate(PRE_COMMIT_TEMPLATE_PATH);
2538
+ await createExecutableScript(preCommitPath, preCommitTemplate, fileMode, result, targetDir);
2539
+ }
2540
+ catch {
2541
+ // Fallback to hardcoded template if template file not found
2542
+ await createExecutableScript(preCommitPath, PRE_COMMIT_TEMPLATE, fileMode, result, targetDir);
2543
+ }
2544
+ }
2545
+ /**
2546
+ * WU-1408: Fallback safe-git template
2547
+ * Blocks dangerous git operations in LumenFlow environment
2548
+ */
2549
+ const SAFE_GIT_TEMPLATE = `#!/bin/sh
2550
+ #
2551
+ # safe-git - LumenFlow safety wrapper for git
2552
+ #
2553
+ # Blocks dangerous operations that can corrupt agent state.
2554
+ # For all other commands, passes through to system git.
2555
+ #
2556
+
2557
+ set -e
2558
+
2559
+ # Block 'worktree remove'
2560
+ if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then
2561
+ echo "" >&2
2562
+ echo "=== LUMENFLOW SAFETY BLOCK ===" >&2
2563
+ echo "" >&2
2564
+ echo "BLOCKED: Manual 'git worktree remove' is unsafe in this environment." >&2
2565
+ echo "" >&2
2566
+ echo "REASON: Manual removal leaves orphan directories and corrupts agent state." >&2
2567
+ echo "" >&2
2568
+ echo "USE INSTEAD:" >&2
2569
+ echo " pnpm wu:done --id <ID> (To complete a task)" >&2
2570
+ echo " pnpm wu:cleanup --id <ID> (To discard a task)" >&2
2571
+ echo "==============================" >&2
2572
+ exit 1
2573
+ fi
2574
+
2575
+ # Pass through to real git
2576
+ exec git "$@"
2577
+ `;
2578
+ /**
2579
+ * WU-1408: Fallback pre-commit template
2580
+ * Blocks direct commits to main/master, allows commits on lane branches
2581
+ * Does NOT run pnpm test (which fails on new projects)
2582
+ */
2583
+ const PRE_COMMIT_TEMPLATE = `#!/bin/sh
2584
+ #
2585
+ # LumenFlow Pre-Commit Hook
2586
+ #
2587
+ # Enforces worktree discipline by blocking direct commits to main/master.
2588
+ # Does NOT assume pnpm test or any other commands exist.
2589
+ #
2590
+ # Rules:
2591
+ # 1. BLOCK commits to main/master (use WU workflow instead)
2592
+ # 2. ALLOW commits on lane branches (lane/*/wu-*)
2593
+ # 3. ALLOW commits on tmp/* branches (CLI micro-worktrees)
2594
+ #
2595
+
2596
+ # Skip on tmp/* branches (CLI micro-worktrees)
2597
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
2598
+ case "$BRANCH" in tmp/*) exit 0 ;; esac
2599
+
2600
+ # Check for force bypass
2601
+ if [ "$LUMENFLOW_FORCE" = "1" ]; then
2602
+ exit 0
2603
+ fi
2604
+
2605
+ # Block direct commits to main/master
2606
+ case "$BRANCH" in
2607
+ main|master)
2608
+ echo "" >&2
2609
+ echo "=== DIRECT COMMIT TO \${BRANCH} BLOCKED ===" >&2
2610
+ echo "" >&2
2611
+ echo "LumenFlow protects main from direct commits." >&2
2612
+ echo "" >&2
2613
+ echo "USE INSTEAD:" >&2
2614
+ echo " pnpm wu:claim --id WU-XXXX --lane \\"<Lane>\\"" >&2
2615
+ echo " cd worktrees/<lane>-wu-xxxx" >&2
2616
+ echo " # Make commits in the worktree" >&2
2617
+ echo "" >&2
2618
+ echo "EMERGENCY BYPASS (logged):" >&2
2619
+ echo " LUMENFLOW_FORCE=1 git commit ..." >&2
2620
+ echo "==========================================" >&2
2621
+ exit 1
2622
+ ;;
2623
+ esac
2624
+
2625
+ # Allow commits on other branches
2626
+ exit 0
2627
+ `;
2493
2628
  /**
2494
2629
  * WU-1300: Inject LumenFlow scripts into package.json
2495
2630
  * - Creates package.json if it doesn't exist
@@ -2657,6 +2792,15 @@ async function scaffoldClientFiles(targetDir, options, result, tokens, client) {
2657
2792
  settingsContent = CLAUDE_SETTINGS_TEMPLATE;
2658
2793
  }
2659
2794
  await createFile(path.join(targetDir, CLAUDE_DIR, 'settings.json'), settingsContent, options.force ? 'force' : 'skip', result, targetDir);
2795
+ // WU-1413: Scaffold .mcp.json for MCP server integration
2796
+ let mcpJsonContent;
2797
+ try {
2798
+ mcpJsonContent = loadTemplate('core/.mcp.json.template');
2799
+ }
2800
+ catch {
2801
+ mcpJsonContent = MCP_JSON_TEMPLATE;
2802
+ }
2803
+ await createFile(path.join(targetDir, '.mcp.json'), mcpJsonContent, fileMode, result, targetDir);
2660
2804
  // WU-1394: Scaffold recovery hook scripts with executable permissions
2661
2805
  const hooksDir = path.join(targetDir, CLAUDE_DIR, 'hooks');
2662
2806
  await createDirectory(hooksDir, result, targetDir);
@@ -91,7 +91,25 @@ const program = new Command()
91
91
  console.log(formatExecutionPlan(initiative, plan));
92
92
  if (dryRun) {
93
93
  console.log(chalk.yellow(`${LOG_PREFIX} Dry run mode - no agents spawned`));
94
- console.log(chalk.cyan('To execute this plan, remove the --dry-run flag.'));
94
+ console.log('');
95
+ console.log(chalk.bold('Next Steps (Recommended Defaults):'));
96
+ console.log('');
97
+ console.log(chalk.cyan(' Option 1 (Recommended): Checkpoint-per-wave mode'));
98
+ console.log(' pnpm orchestrate:initiative -i ' + initIds[0] + ' -c');
99
+ console.log(' Best for: Large initiatives, context management, idempotent resumption');
100
+ console.log('');
101
+ console.log(chalk.cyan(' Option 2: Full execution (polling mode)'));
102
+ console.log(' pnpm orchestrate:initiative -i ' + initIds[0]);
103
+ console.log(' Best for: Small initiatives (<4 WUs), quick execution');
104
+ console.log('');
105
+ console.log(chalk.cyan(' Option 3: Manual spawn per WU'));
106
+ console.log(' pnpm wu:spawn --id <WU-ID> --client claude-code');
107
+ console.log(' Best for: Testing, debugging, single WU execution');
108
+ console.log('');
109
+ console.log(chalk.bold('Monitoring Commands:'));
110
+ console.log(' pnpm mem:inbox --since 10m # Check for signals from agents');
111
+ console.log(' pnpm orchestrate:init-status -i ' + initIds[0] + ' # Check progress');
112
+ console.log(' pnpm orchestrate:monitor # Live agent activity');
95
113
  return;
96
114
  }
97
115
  // WU-1202: Output spawn XML for actual execution (not dry-run)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * State Doctor Fix Operations (WU-1230)
2
+ * State Doctor Fix Operations (WU-1230, WU-1420)
3
3
  *
4
4
  * Provides fix dependencies for state:doctor --fix that use micro-worktree
5
5
  * isolation for all tracked file changes. This ensures:
@@ -8,6 +8,7 @@
8
8
  * 2. Removal of stale WU references from backlog.md and status.md
9
9
  * 3. All changes pushed via merge, not direct file modification
10
10
  * 4. WU-1362: Retry logic for push failures (inherited from withMicroWorktree)
11
+ * 5. WU-1420: Emit corrective events to reconcile YAML vs state store mismatches
11
12
  *
12
13
  * Retry behavior is configured via .lumenflow.config.yaml git.push_retry section.
13
14
  * Default: 3 retries with exponential backoff and jitter.
@@ -72,6 +73,9 @@ async function readFileSafe(filePath) {
72
73
  * WU-1230: All file modifications happen in a micro-worktree and are pushed
73
74
  * to origin/main via merge. This prevents direct modifications to local main.
74
75
  *
76
+ * WU-1420: Includes emitEvent for fixing status mismatches by emitting
77
+ * corrective events (release, complete) to reconcile state store with YAML.
78
+ *
75
79
  * @param baseDir - Project base directory
76
80
  * @returns Partial StateDoctorDeps with fix operations
77
81
  */
@@ -190,5 +194,36 @@ export function createStateDoctorFixDeps(_baseDir) {
190
194
  },
191
195
  });
192
196
  },
197
+ /**
198
+ * Emit a corrective event to fix status mismatch (WU-1420)
199
+ *
200
+ * Appends a release or complete event to wu-events.jsonl to reconcile
201
+ * the state store with the WU YAML status.
202
+ */
203
+ emitEvent: async (event) => {
204
+ await withMicroWorktree({
205
+ operation: OPERATION_NAME,
206
+ id: `emit-event-${event.wuId.toLowerCase()}-${event.type}`,
207
+ logPrefix: LOG_PREFIX,
208
+ pushOnly: true,
209
+ execute: async ({ worktreePath }) => {
210
+ const eventsPath = path.join(worktreePath, WU_EVENTS_FILE);
211
+ // Build the event object
212
+ const eventLine = JSON.stringify({
213
+ wuId: event.wuId,
214
+ type: event.type,
215
+ reason: event.reason,
216
+ timestamp: event.timestamp || new Date().toISOString(),
217
+ });
218
+ // Append to events file (create if needed)
219
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true });
220
+ await fs.appendFile(eventsPath, eventLine + '\n', 'utf-8');
221
+ return {
222
+ commitMessage: `fix(state-doctor): emit ${event.type} event for ${event.wuId} to reconcile state`,
223
+ files: [WU_EVENTS_FILE],
224
+ };
225
+ },
226
+ });
227
+ },
193
228
  };
194
229
  }
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * State Doctor CLI (WU-1209)
3
+ * State Doctor CLI (WU-1209, WU-1420)
4
4
  *
5
5
  * Integrity checker for LumenFlow state that detects:
6
6
  * - Orphaned WUs (done status but no stamp)
7
7
  * - Dangling signals (reference non-existent WUs)
8
8
  * - Broken memory relationships (events for missing WU specs)
9
+ * - Status mismatches between WU YAML and state store (WU-1420)
9
10
  *
10
11
  * Inspired by Beads bd doctor command.
11
12
  *
@@ -171,7 +172,7 @@ async function createDeps(baseDir) {
171
172
  */
172
173
  listStamps: async () => {
173
174
  try {
174
- const stampsDir = path.join(baseDir, config.beacon.stampsDir);
175
+ const stampsDir = path.join(baseDir, config.state.stampsDir);
175
176
  const stampFiles = await fg('WU-*.done', { cwd: stampsDir });
176
177
  return stampFiles.map((file) => file.replace('.done', ''));
177
178
  }
@@ -321,6 +322,8 @@ function getIssueTypeLabel(type) {
321
322
  return 'Dangling Signal';
322
323
  case ISSUE_TYPES.BROKEN_EVENT:
323
324
  return 'Broken Event';
325
+ case ISSUE_TYPES.STATUS_MISMATCH:
326
+ return 'Status Mismatch';
324
327
  default:
325
328
  return type;
326
329
  }
@@ -330,10 +333,11 @@ function getIssueTypeLabel(type) {
330
333
  */
331
334
  function printSummary(result) {
332
335
  console.log('=== Summary ===');
333
- console.log(` Orphaned WUs: ${result.summary.orphanedWUs}`);
334
- console.log(` Dangling Signals: ${result.summary.danglingSignals}`);
335
- console.log(` Broken Events: ${result.summary.brokenEvents}`);
336
- console.log(` Total Issues: ${result.summary.totalIssues}`);
336
+ console.log(` Orphaned WUs: ${result.summary.orphanedWUs}`);
337
+ console.log(` Dangling Signals: ${result.summary.danglingSignals}`);
338
+ console.log(` Broken Events: ${result.summary.brokenEvents}`);
339
+ console.log(` Status Mismatches: ${result.summary.statusMismatches}`);
340
+ console.log(` Total Issues: ${result.summary.totalIssues}`);
337
341
  }
338
342
  /**
339
343
  * Print fixed issues section
package/dist/wu-create.js CHANGED
@@ -43,7 +43,7 @@ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
43
43
  import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
44
44
  import { validateWU } from '@lumenflow/core/dist/wu-schema.js';
45
45
  import { getPlanPath, getPlanProtocolRef, getPlansDir, } from '@lumenflow/core/dist/lumenflow-home.js';
46
- import { validateSpecRefs } from '@lumenflow/core/dist/wu-create-validators.js';
46
+ import { hasSpecRefs, validateSpecRefs } from '@lumenflow/core/dist/wu-create-validators.js';
47
47
  import { COMMIT_FORMATS, FILE_SYSTEM, READINESS_UI, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
48
48
  // WU-1593: Use centralized validateWUIDFormat (DRY)
49
49
  import { ensureOnMain, validateWUIDFormat } from '@lumenflow/core/dist/wu-helpers.js';
@@ -114,6 +114,21 @@ export function warnIfBetterLaneExists(providedLane, codePathsArray, title, desc
114
114
  // Non-blocking - if inference fails, continue silently
115
115
  }
116
116
  }
117
+ export function collectInitiativeWarnings({ initiativeId, initiativeDoc, phase, specRefs, }) {
118
+ const warnings = [];
119
+ const phaseCheck = checkInitiativePhases(initiativeDoc);
120
+ if (!phaseCheck.hasPhases && phaseCheck.warning) {
121
+ warnings.push(phaseCheck.warning);
122
+ }
123
+ if (phaseCheck.hasPhases && !phase) {
124
+ warnings.push(`Initiative ${initiativeId} has phases defined. Consider adding --phase to link this WU to a phase.`);
125
+ }
126
+ const relatedPlan = initiativeDoc.related_plan;
127
+ if (relatedPlan && !hasSpecRefs(specRefs)) {
128
+ warnings.push(`Initiative ${initiativeId} has related_plan (${relatedPlan}). Consider adding --spec-refs to link this WU to the plan.`);
129
+ }
130
+ return warnings;
131
+ }
117
132
  /**
118
133
  * Check if WU already exists
119
134
  * @param {string} id - WU ID to check
@@ -211,8 +226,8 @@ function createPlanTemplate(wuId, title) {
211
226
  console.log(`${LOG_PREFIX} ✅ Created plan template: ${planPath}`);
212
227
  return planPath;
213
228
  }
214
- function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
215
- const { description, acceptance, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
229
+ export function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
230
+ const { description, acceptance, notes, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
216
231
  // Arrays come directly from Commander.js repeatable options - no parsing needed
217
232
  const code_paths = codePaths ?? [];
218
233
  const tests = {
@@ -235,7 +250,7 @@ function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
235
250
  artifacts: [`.lumenflow/stamps/${id}.done`],
236
251
  dependencies: [],
237
252
  risks: [],
238
- notes: '',
253
+ notes: notes ?? '',
239
254
  requires_review: false,
240
255
  ...(initiative && { initiative }),
241
256
  ...(phase && { phase: parseInt(phase, 10) }),
@@ -285,7 +300,7 @@ export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
285
300
  errors.push('At least one test path flag is required (--test-paths-manual, --test-paths-unit, or --test-paths-e2e)');
286
301
  }
287
302
  }
288
- if (effectiveType === 'feature' && !opts.specRefs) {
303
+ if (effectiveType === 'feature' && !hasSpecRefs(opts.specRefs)) {
289
304
  errors.push('--spec-refs is required for type: feature WUs\n' +
290
305
  ' Tip: Create a plan first with: pnpm plan:create --id <WU-ID> --title "..."\n' +
291
306
  ' Then use --plan flag or --spec-refs lumenflow://plans/<WU-ID>-plan.md');
@@ -511,6 +526,7 @@ async function main() {
511
526
  // WU-1364: Full spec inline options
512
527
  WU_OPTIONS.description,
513
528
  WU_OPTIONS.acceptance,
529
+ WU_OPTIONS.notes,
514
530
  WU_OPTIONS.codePaths,
515
531
  WU_OPTIONS.testPathsManual,
516
532
  WU_OPTIONS.testPathsUnit,
@@ -588,6 +604,7 @@ async function main() {
588
604
  opts: {
589
605
  description: args.description,
590
606
  acceptance: args.acceptance,
607
+ notes: args.notes,
591
608
  codePaths: args.codePaths,
592
609
  testPathsManual: args.testPathsManual,
593
610
  testPathsUnit: args.testPathsUnit,
@@ -613,16 +630,6 @@ async function main() {
613
630
  die(`${LOG_PREFIX} ❌ Spec validation failed:\n\n${errorList}`);
614
631
  }
615
632
  console.log(`${LOG_PREFIX} ✅ Spec validation passed`);
616
- // WU-1211: Warn if linking to initiative with no phases defined
617
- if (args.initiative) {
618
- const initiative = findInitiative(args.initiative);
619
- if (initiative) {
620
- const phaseCheck = checkInitiativePhases(initiative.doc);
621
- if (!phaseCheck.hasPhases && phaseCheck.warning) {
622
- console.warn(`${LOG_PREFIX} ⚠️ ${phaseCheck.warning}`);
623
- }
624
- }
625
- }
626
633
  const specRefsList = mergedSpecRefs;
627
634
  const specRefsValidation = validateSpecRefs(specRefsList);
628
635
  if (!specRefsValidation.valid) {
@@ -636,6 +643,20 @@ async function main() {
636
643
  console.warn(`${LOG_PREFIX} ⚠️ ${warning}`);
637
644
  }
638
645
  }
646
+ if (args.initiative) {
647
+ const initiative = findInitiative(args.initiative);
648
+ if (initiative) {
649
+ const warnings = collectInitiativeWarnings({
650
+ initiativeId: initiative.id,
651
+ initiativeDoc: initiative.doc,
652
+ phase: args.phase,
653
+ specRefs: specRefsList,
654
+ });
655
+ for (const warning of warnings) {
656
+ console.warn(`${LOG_PREFIX} ⚠️ ${warning}`);
657
+ }
658
+ }
659
+ }
639
660
  if (args.plan) {
640
661
  createPlanTemplate(wuId, args.title);
641
662
  }
@@ -664,6 +685,7 @@ async function main() {
664
685
  // WU-1364: Full spec inline options
665
686
  description: args.description,
666
687
  acceptance: args.acceptance,
688
+ notes: args.notes,
667
689
  codePaths: args.codePaths,
668
690
  testPathsManual: args.testPathsManual,
669
691
  testPathsUnit: args.testPathsUnit,
@@ -15,16 +15,19 @@
15
15
  * pnpm wu:recover --id WU-123 --action resume # Apply fix
16
16
  * pnpm wu:recover --id WU-123 --action nuke --force # Destructive
17
17
  */
18
- import { existsSync, rmSync } from 'node:fs';
18
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
19
19
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
20
20
  import { computeContext } from '@lumenflow/core/dist/context/index.js';
21
21
  import { analyzeRecovery, } from '@lumenflow/core/dist/recovery/recovery-analyzer.js';
22
22
  import { die } from '@lumenflow/core/dist/error-handler.js';
23
23
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
24
24
  import { readWU, writeWU } from '@lumenflow/core/dist/wu-yaml.js';
25
- import { CONTEXT_VALIDATION, EMOJI, WU_STATUS, DEFAULTS, toKebab, } from '@lumenflow/core/dist/wu-constants.js';
25
+ import { CONTEXT_VALIDATION, EMOJI, WU_STATUS, DEFAULTS, toKebab, FILE_SYSTEM, } from '@lumenflow/core/dist/wu-constants.js';
26
26
  import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
27
27
  import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
28
+ import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
29
+ import { generateBacklog, generateStatus } from '@lumenflow/core/dist/backlog-generator.js';
30
+ import { releaseLaneLock } from '@lumenflow/core/dist/lane-lock.js';
28
31
  import { join, relative } from 'node:path';
29
32
  const { RECOVERY_ACTIONS } = CONTEXT_VALIDATION;
30
33
  const LOG_PREFIX = '[wu:recover]';
@@ -159,6 +162,7 @@ async function executeResume(wuId) {
159
162
  * Execute reset action - discard worktree and reset to ready
160
163
  *
161
164
  * WU-1226: Uses micro-worktree isolation for WU YAML state changes.
165
+ * WU-1419: Emits release event to state store so WU can be re-claimed.
162
166
  * Worktree removal still happens directly (git operation, not file write).
163
167
  * Changes are pushed via merge, not direct file modification on main.
164
168
  */
@@ -171,6 +175,7 @@ async function executeReset(wuId) {
171
175
  }
172
176
  const doc = readWU(wuPath, wuId);
173
177
  const worktreePath = getWorktreePath(wuId, doc.lane || '');
178
+ const lane = doc.lane || '';
174
179
  // Remove worktree if exists (git operation, safe to do directly)
175
180
  // WU-1097: Use worktreeRemove() instead of deprecated run() with shell strings
176
181
  // This properly handles paths with spaces and special characters
@@ -193,6 +198,7 @@ async function executeReset(wuId) {
193
198
  }
194
199
  }
195
200
  // WU-1226: Use micro-worktree isolation for WU YAML state changes
201
+ // WU-1419: Also emit release event to state store
196
202
  try {
197
203
  await withMicroWorktree({
198
204
  operation: OPERATION_NAME,
@@ -211,12 +217,57 @@ async function executeReset(wuId) {
211
217
  Reflect.deleteProperty(microDoc, 'session_id');
212
218
  Reflect.deleteProperty(microDoc, 'baseline_main_sha');
213
219
  writeWU(microWuPath, microDoc);
220
+ // WU-1419: Emit release event to state store so re-claiming works
221
+ // Without this, state store still thinks WU is in_progress, blocking re-claim
222
+ const stateDir = join(microPath, '.lumenflow', 'state');
223
+ const store = new WUStateStore(stateDir);
224
+ await store.load();
225
+ // Only emit release event if WU is currently in_progress in state store
226
+ const currentState = store.getWUState(wuId);
227
+ if (currentState && currentState.status === 'in_progress') {
228
+ await store.release(wuId, 'Reset via wu:recover --action reset');
229
+ console.log(`${LOG_PREFIX} Emitted release event to state store`);
230
+ // Regenerate backlog.md and status.md from state store
231
+ const microBacklogPath = join(microPath, WU_PATHS.BACKLOG());
232
+ const microStatusPath = join(microPath, WU_PATHS.STATUS());
233
+ const backlogContent = await generateBacklog(store);
234
+ writeFileSync(microBacklogPath, backlogContent, {
235
+ encoding: FILE_SYSTEM.UTF8,
236
+ });
237
+ const statusContent = await generateStatus(store);
238
+ writeFileSync(microStatusPath, statusContent, {
239
+ encoding: FILE_SYSTEM.UTF8,
240
+ });
241
+ return {
242
+ commitMessage: `fix(wu-recover): reset ${wuId} - clear claim and emit release event`,
243
+ files: [
244
+ relative(process.cwd(), wuPath),
245
+ WU_PATHS.STATUS(),
246
+ WU_PATHS.BACKLOG(),
247
+ '.lumenflow/state/wu-events.jsonl',
248
+ ],
249
+ };
250
+ }
251
+ // WU not in state store as in_progress, just update YAML
214
252
  return {
215
253
  commitMessage: `fix(wu-recover): reset ${wuId} - clear claim and set status to ready`,
216
254
  files: [relative(process.cwd(), wuPath)],
217
255
  };
218
256
  },
219
257
  });
258
+ // Release lane lock so another WU can be claimed
259
+ if (lane) {
260
+ try {
261
+ const releaseResult = releaseLaneLock(lane, { wuId });
262
+ if (releaseResult.released && !releaseResult.notFound) {
263
+ console.log(`${LOG_PREFIX} Lane lock released for "${lane}"`);
264
+ }
265
+ }
266
+ catch (err) {
267
+ // Non-blocking: lock release failure should not block the reset operation
268
+ console.warn(`${LOG_PREFIX} Warning: Could not release lane lock: ${err.message}`);
269
+ }
270
+ }
220
271
  console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Reset completed - ${wuId} is now ready for re-claiming`);
221
272
  return true;
222
273
  }
package/dist/wu-spawn.js CHANGED
@@ -630,10 +630,10 @@ When creating \`.lumenflow/\` stamps or other artifacts:
630
630
  # CORRECT: Create stamp in worktree
631
631
  WORKTREE_ROOT=$(git rev-parse --show-toplevel)
632
632
  mkdir -p "$WORKTREE_ROOT/.lumenflow/agent-runs"
633
- touch "$WORKTREE_ROOT/.lumenflow/agent-runs/beacon-guardian.stamp"
633
+ touch "$WORKTREE_ROOT/.lumenflow/agent-runs/code-reviewer.stamp"
634
634
 
635
635
  # WRONG: Hardcoded path to main
636
- # touch /path/to/main/.lumenflow/agent-runs/beacon-guardian.stamp
636
+ # touch /path/to/main/.lumenflow/agent-runs/code-reviewer.stamp
637
637
  \`\`\`
638
638
 
639
639
  ### Why This Matters
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/cli",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Command-line interface for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -151,11 +151,11 @@
151
151
  "pretty-ms": "^9.2.0",
152
152
  "simple-git": "^3.30.0",
153
153
  "yaml": "^2.8.2",
154
- "@lumenflow/core": "2.10.0",
155
- "@lumenflow/metrics": "2.10.0",
156
- "@lumenflow/initiatives": "2.10.0",
157
- "@lumenflow/agent": "2.10.0",
158
- "@lumenflow/memory": "2.10.0"
154
+ "@lumenflow/core": "2.11.0",
155
+ "@lumenflow/metrics": "2.11.0",
156
+ "@lumenflow/memory": "2.11.0",
157
+ "@lumenflow/initiatives": "2.11.0",
158
+ "@lumenflow/agent": "2.11.0"
159
159
  },
160
160
  "devDependencies": {
161
161
  "@vitest/coverage-v8": "^4.0.17",
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "lumenflow": {
4
+ "command": "npx",
5
+ "args": ["@lumenflow/mcp"]
6
+ }
7
+ }
8
+ }
@@ -55,6 +55,13 @@ Claude Code is Anthropic's official CLI for Claude. It has the deepest integrati
55
55
  - `.claude/settings.json` - Permission configuration
56
56
  - `.claude/agents/` - Agent definitions
57
57
  - `.claude/skills/` - Skill definitions
58
+ - `.mcp.json` - MCP server configuration (WU-1413)
59
+
60
+ **MCP Integration:** Claude Code supports MCP (Model Context Protocol) servers. When initializing with `--client claude`, LumenFlow automatically configures the `@lumenflow/mcp` server which provides:
61
+
62
+ - WU lifecycle tools (status, claim, gates)
63
+ - Memory coordination tools (checkpoint, signal, inbox)
64
+ - Lane management tools (health, suggest)
58
65
 
59
66
  **Auto-detection:** Environment variables `CLAUDE_PROJECT_DIR` or `CLAUDE_CODE`
60
67
 
@@ -194,6 +201,72 @@ This creates all vendor-specific configuration files alongside the universal ent
194
201
 
195
202
  ---
196
203
 
204
+ ## MCP Server Integration
205
+
206
+ LumenFlow provides an MCP (Model Context Protocol) server package for deeper AI integration.
207
+
208
+ ### What is MCP?
209
+
210
+ MCP is a protocol that allows AI assistants to access tools and context from external servers. LumenFlow's MCP server (`@lumenflow/mcp`) exposes workflow tools directly to compatible AI assistants.
211
+
212
+ ### Supported Assistants
213
+
214
+ | Assistant | MCP Support | Configuration |
215
+ | ------------- | ----------- | --------------- |
216
+ | Claude Code | Yes | `.mcp.json` |
217
+ | Antigravity | Yes | TBD |
218
+ | Cursor | No | Uses rules file |
219
+ | Windsurf | No | Uses rules file |
220
+
221
+ ### Setup (Claude Code)
222
+
223
+ When you run `lumenflow init --client claude`, a `.mcp.json` is automatically created:
224
+
225
+ ```json
226
+ {
227
+ "mcpServers": {
228
+ "lumenflow": {
229
+ "command": "npx",
230
+ "args": ["@lumenflow/mcp"]
231
+ }
232
+ }
233
+ }
234
+ ```
235
+
236
+ ### Manual Setup
237
+
238
+ If you already have LumenFlow but need to add MCP support:
239
+
240
+ ```bash
241
+ # Option 1: Re-run init with force to regenerate all files
242
+ lumenflow init --client claude --force
243
+
244
+ # Option 2: Manually create .mcp.json
245
+ echo '{
246
+ "mcpServers": {
247
+ "lumenflow": {
248
+ "command": "npx",
249
+ "args": ["@lumenflow/mcp"]
250
+ }
251
+ }
252
+ }' > .mcp.json
253
+ ```
254
+
255
+ ### Available MCP Tools
256
+
257
+ The `@lumenflow/mcp` server provides these tools to AI assistants:
258
+
259
+ | Tool | Description |
260
+ | -------------- | ------------------------------------ |
261
+ | `wu_status` | Get current WU status and location |
262
+ | `wu_claim` | Claim a WU and create worktree |
263
+ | `gates` | Run quality gates |
264
+ | `mem_checkpoint` | Save progress checkpoint |
265
+ | `mem_inbox` | Check coordination signals |
266
+ | `lane_health` | Check lane configuration health |
267
+
268
+ ---
269
+
197
270
  ## Sync Vendor Configs
198
271
 
199
272
  To ensure all vendor configs are in sync with the template:
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+ #
3
+ # safe-git
4
+ #
5
+ # A wrapper around git that blocks dangerous operations in the LumenFlow environment.
6
+ # Specifically intercepts 'worktree remove' to prevent orphan directories and state corruption.
7
+ # For all other commands, it passes through to the system git.
8
+ #
9
+
10
+ set -e
11
+
12
+ # Block 'worktree remove'
13
+ if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then
14
+ echo "" >&2
15
+ echo "=== LUMENFLOW SAFETY BLOCK ===" >&2
16
+ echo "" >&2
17
+ echo "BLOCKED: Manual 'git worktree remove' is unsafe in this environment." >&2
18
+ echo "" >&2
19
+ echo "REASON: Manual removal leaves orphan directories and corrupts agent state." >&2
20
+ echo "" >&2
21
+ echo "USE INSTEAD:" >&2
22
+ echo " pnpm wu:done --id <ID> (To complete a task)" >&2
23
+ echo " pnpm wu:cleanup --id <ID> (To discard a task)" >&2
24
+ echo "==============================" >&2
25
+ exit 1
26
+ fi
27
+
28
+ # Pass through to real git
29
+ exec git "$@"