@lumenflow/cli 2.7.0 → 2.9.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 (84) hide show
  1. package/README.md +121 -105
  2. package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
  3. package/dist/__tests__/commands/integrate.test.js +165 -0
  4. package/dist/__tests__/commands.test.js +75 -0
  5. package/dist/__tests__/doctor.test.js +510 -0
  6. package/dist/__tests__/gates-config.test.js +0 -1
  7. package/dist/__tests__/hooks/enforcement.test.js +279 -0
  8. package/dist/__tests__/init-greenfield.test.js +247 -0
  9. package/dist/__tests__/init-quick-ref.test.js +0 -1
  10. package/dist/__tests__/init-template-portability.test.js +0 -1
  11. package/dist/__tests__/init.test.js +249 -0
  12. package/dist/__tests__/initiative-e2e.test.js +442 -0
  13. package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
  14. package/dist/__tests__/memory-integration.test.js +333 -0
  15. package/dist/__tests__/release.test.js +1 -1
  16. package/dist/__tests__/safe-git.test.js +0 -1
  17. package/dist/__tests__/state-doctor.test.js +54 -0
  18. package/dist/__tests__/sync-templates.test.js +255 -0
  19. package/dist/__tests__/wu-create-required-fields.test.js +121 -0
  20. package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
  21. package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
  22. package/dist/backlog-prune.js +0 -1
  23. package/dist/cli-entry-point.js +0 -1
  24. package/dist/commands/integrate.js +229 -0
  25. package/dist/commands.js +171 -0
  26. package/dist/docs-sync.js +46 -0
  27. package/dist/doctor.js +479 -10
  28. package/dist/gates.js +0 -7
  29. package/dist/hooks/enforcement-checks.js +209 -0
  30. package/dist/hooks/enforcement-generator.js +365 -0
  31. package/dist/hooks/enforcement-sync.js +243 -0
  32. package/dist/hooks/index.js +7 -0
  33. package/dist/init.js +502 -17
  34. package/dist/initiative-add-wu.js +0 -2
  35. package/dist/initiative-create.js +0 -3
  36. package/dist/initiative-edit.js +0 -5
  37. package/dist/initiative-plan.js +0 -1
  38. package/dist/initiative-remove-wu.js +0 -2
  39. package/dist/lane-health.js +0 -2
  40. package/dist/lane-suggest.js +0 -1
  41. package/dist/mem-checkpoint.js +0 -2
  42. package/dist/mem-cleanup.js +0 -2
  43. package/dist/mem-context.js +0 -3
  44. package/dist/mem-create.js +0 -2
  45. package/dist/mem-delete.js +0 -3
  46. package/dist/mem-inbox.js +0 -2
  47. package/dist/mem-index.js +0 -1
  48. package/dist/mem-init.js +0 -2
  49. package/dist/mem-profile.js +0 -1
  50. package/dist/mem-promote.js +0 -1
  51. package/dist/mem-ready.js +0 -2
  52. package/dist/mem-signal.js +0 -2
  53. package/dist/mem-start.js +0 -2
  54. package/dist/mem-summarize.js +0 -2
  55. package/dist/metrics-cli.js +1 -1
  56. package/dist/metrics-snapshot.js +1 -1
  57. package/dist/onboarding-smoke-test.js +0 -5
  58. package/dist/orchestrate-init-status.js +0 -1
  59. package/dist/orchestrate-initiative.js +0 -1
  60. package/dist/orchestrate-monitor.js +0 -1
  61. package/dist/plan-create.js +0 -2
  62. package/dist/plan-edit.js +0 -2
  63. package/dist/plan-link.js +0 -2
  64. package/dist/plan-promote.js +0 -2
  65. package/dist/signal-cleanup.js +0 -4
  66. package/dist/state-bootstrap.js +0 -1
  67. package/dist/state-cleanup.js +0 -4
  68. package/dist/state-doctor-fix.js +5 -8
  69. package/dist/state-doctor.js +0 -11
  70. package/dist/sync-templates.js +188 -34
  71. package/dist/wu-block.js +100 -48
  72. package/dist/wu-claim.js +1 -22
  73. package/dist/wu-cleanup.js +0 -1
  74. package/dist/wu-create.js +0 -2
  75. package/dist/wu-done-auto-cleanup.js +139 -0
  76. package/dist/wu-done.js +11 -4
  77. package/dist/wu-edit.js +0 -12
  78. package/dist/wu-preflight.js +0 -1
  79. package/dist/wu-prep.js +0 -1
  80. package/dist/wu-proto.js +0 -1
  81. package/dist/wu-spawn.js +0 -3
  82. package/dist/wu-unblock.js +0 -2
  83. package/dist/wu-validate.js +0 -1
  84. package/package.json +9 -7
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @file wu-create-required-fields.test.ts
3
+ * Test suite for wu:create required field aggregation (WU-1366)
4
+ *
5
+ * WU-1366: Missing required fields in wu:create should be reported together
6
+ * in a single error block.
7
+ *
8
+ * Tests:
9
+ * - Multiple missing required fields are collected and reported together
10
+ * - Error block formatting includes all missing fields
11
+ * - Single missing field still reports correctly
12
+ */
13
+ import { describe, it, expect } from 'vitest';
14
+ import { validateCreateSpec } from '../wu-create.js';
15
+ /** Default lane for test cases */
16
+ const TEST_LANE = 'Framework: CLI';
17
+ /** Default test WU ID */
18
+ const TEST_WU_ID = 'WU-9999';
19
+ /** Minimum valid description with required sections */
20
+ const VALID_DESCRIPTION = 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.';
21
+ /** Default acceptance criteria for tests */
22
+ const TEST_ACCEPTANCE = ['Acceptance criterion'];
23
+ describe('wu:create required field aggregation (WU-1366)', () => {
24
+ describe('validateCreateSpec error aggregation', () => {
25
+ it('should aggregate multiple missing required fields into a single error block', () => {
26
+ // Missing: description, acceptance, exposure, code-paths, test-paths
27
+ const result = validateCreateSpec({
28
+ id: TEST_WU_ID,
29
+ lane: TEST_LANE,
30
+ title: 'Test WU',
31
+ priority: 'P2',
32
+ type: 'feature',
33
+ opts: {
34
+ // All required fields missing
35
+ strict: false, // Skip path existence checks
36
+ },
37
+ });
38
+ expect(result.valid).toBe(false);
39
+ // Should have multiple errors collected
40
+ expect(result.errors.length).toBeGreaterThan(1);
41
+ // Verify specific missing fields are included
42
+ expect(result.errors.some((e) => e.includes('--description'))).toBe(true);
43
+ expect(result.errors.some((e) => e.includes('--acceptance'))).toBe(true);
44
+ expect(result.errors.some((e) => e.includes('--exposure'))).toBe(true);
45
+ });
46
+ it('should report code-paths and test-paths as missing for non-documentation WUs', () => {
47
+ const result = validateCreateSpec({
48
+ id: TEST_WU_ID,
49
+ lane: TEST_LANE,
50
+ title: 'Test WU',
51
+ priority: 'P2',
52
+ type: 'feature',
53
+ opts: {
54
+ description: VALID_DESCRIPTION,
55
+ acceptance: TEST_ACCEPTANCE,
56
+ exposure: 'backend-only',
57
+ // Missing: codePaths, testPaths, specRefs
58
+ strict: false,
59
+ },
60
+ });
61
+ expect(result.valid).toBe(false);
62
+ expect(result.errors.some((e) => e.includes('--code-paths'))).toBe(true);
63
+ expect(result.errors.some((e) => e.includes('test path'))).toBe(true);
64
+ });
65
+ it('should report spec-refs as missing for feature WUs', () => {
66
+ const result = validateCreateSpec({
67
+ id: TEST_WU_ID,
68
+ lane: TEST_LANE,
69
+ title: 'Test WU',
70
+ priority: 'P2',
71
+ type: 'feature',
72
+ opts: {
73
+ description: VALID_DESCRIPTION,
74
+ acceptance: TEST_ACCEPTANCE,
75
+ exposure: 'backend-only',
76
+ codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
77
+ testPathsUnit: ['packages/@lumenflow/cli/src/__tests__/wu-create.test.ts'],
78
+ // Missing: specRefs (required for feature type)
79
+ strict: false,
80
+ },
81
+ });
82
+ expect(result.valid).toBe(false);
83
+ expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
84
+ });
85
+ it('should return all errors at once, not fail on first error', () => {
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
+ // All required fields missing to ensure multiple errors
94
+ strict: false,
95
+ },
96
+ });
97
+ expect(result.valid).toBe(false);
98
+ // Verify multiple errors are present (at least 3: description, acceptance, exposure)
99
+ expect(result.errors.length).toBeGreaterThanOrEqual(3);
100
+ });
101
+ it('should validate documentation WUs without requiring code-paths', () => {
102
+ const result = validateCreateSpec({
103
+ id: TEST_WU_ID,
104
+ lane: 'Content: Documentation',
105
+ title: 'Test Docs WU',
106
+ priority: 'P2',
107
+ type: 'documentation',
108
+ opts: {
109
+ description: VALID_DESCRIPTION,
110
+ acceptance: TEST_ACCEPTANCE,
111
+ exposure: 'documentation',
112
+ testPathsManual: ['Verify docs render correctly'],
113
+ strict: false,
114
+ },
115
+ });
116
+ // Documentation WUs should not require code-paths
117
+ const hasCodePathsError = result.errors.some((e) => e.includes('--code-paths'));
118
+ expect(hasCodePathsError).toBe(false);
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @file wu-done-auto-cleanup.test.ts
3
+ * Test suite for wu:done auto cleanup on success (WU-1366)
4
+ *
5
+ * WU-1366: State cleanup runs automatically after wu:done success (non-fatal)
6
+ *
7
+ * Tests:
8
+ * - shouldRunAutoCleanup respects config.cleanup.trigger setting
9
+ * - runAutoCleanupAfterDone is non-fatal (logs errors but doesn't throw)
10
+ */
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ /** Common mock path for lumenflow config module */
13
+ const CONFIG_MODULE_PATH = '@lumenflow/core/dist/lumenflow-config.js';
14
+ // Test the exported functions directly with minimal mocking
15
+ describe('wu:done auto cleanup (WU-1366)', () => {
16
+ let consoleLogSpy;
17
+ let consoleWarnSpy;
18
+ beforeEach(() => {
19
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
20
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
21
+ });
22
+ afterEach(() => {
23
+ consoleLogSpy.mockRestore();
24
+ consoleWarnSpy.mockRestore();
25
+ vi.resetModules();
26
+ });
27
+ describe('shouldRunAutoCleanup', () => {
28
+ it('should return true when config.cleanup.trigger is on_done', async () => {
29
+ // Mock getConfig to return on_done trigger
30
+ vi.doMock(CONFIG_MODULE_PATH, () => ({
31
+ getConfig: vi.fn().mockReturnValue({
32
+ cleanup: { trigger: 'on_done' },
33
+ }),
34
+ }));
35
+ const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
36
+ const result = shouldRunAutoCleanup();
37
+ expect(result).toBe(true);
38
+ });
39
+ it('should return false when config.cleanup.trigger is manual', async () => {
40
+ vi.doMock(CONFIG_MODULE_PATH, () => ({
41
+ getConfig: vi.fn().mockReturnValue({
42
+ cleanup: { trigger: 'manual' },
43
+ }),
44
+ }));
45
+ const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
46
+ const result = shouldRunAutoCleanup();
47
+ expect(result).toBe(false);
48
+ });
49
+ it('should return false when config.cleanup.trigger is on_init', async () => {
50
+ vi.doMock(CONFIG_MODULE_PATH, () => ({
51
+ getConfig: vi.fn().mockReturnValue({
52
+ cleanup: { trigger: 'on_init' },
53
+ }),
54
+ }));
55
+ const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
56
+ const result = shouldRunAutoCleanup();
57
+ expect(result).toBe(false);
58
+ });
59
+ it('should return true when cleanup config is missing (default behavior)', async () => {
60
+ vi.doMock(CONFIG_MODULE_PATH, () => ({
61
+ getConfig: vi.fn().mockReturnValue({}),
62
+ }));
63
+ const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
64
+ const result = shouldRunAutoCleanup();
65
+ expect(result).toBe(true);
66
+ });
67
+ });
68
+ describe('runAutoCleanupAfterDone non-fatal behavior', () => {
69
+ it('should not throw when cleanup throws an error', async () => {
70
+ // Mock config to enable cleanup
71
+ vi.doMock(CONFIG_MODULE_PATH, () => ({
72
+ getConfig: vi.fn().mockReturnValue({
73
+ cleanup: { trigger: 'on_done' },
74
+ directories: { wuDir: 'docs/tasks/wu' },
75
+ }),
76
+ }));
77
+ // Mock cleanupState to throw
78
+ vi.doMock('@lumenflow/core/dist/state-cleanup-core.js', () => ({
79
+ cleanupState: vi.fn().mockRejectedValue(new Error('Cleanup failed')),
80
+ }));
81
+ // Mock the memory functions to avoid actual file operations
82
+ vi.doMock('@lumenflow/memory/dist/signal-cleanup-core.js', () => ({
83
+ cleanupSignals: vi.fn().mockResolvedValue({
84
+ success: true,
85
+ removedIds: [],
86
+ retainedIds: [],
87
+ bytesFreed: 0,
88
+ compactionRatio: 0,
89
+ breakdown: {},
90
+ }),
91
+ }));
92
+ vi.doMock('@lumenflow/memory/dist/mem-cleanup-core.js', () => ({
93
+ cleanupMemory: vi.fn().mockResolvedValue({
94
+ success: true,
95
+ removedIds: [],
96
+ retainedIds: [],
97
+ bytesFreed: 0,
98
+ compactionRatio: 0,
99
+ breakdown: {},
100
+ }),
101
+ }));
102
+ vi.doMock('@lumenflow/core/dist/wu-events-cleanup.js', () => ({
103
+ archiveWuEvents: vi.fn().mockResolvedValue({
104
+ success: true,
105
+ archivedWuIds: [],
106
+ retainedWuIds: [],
107
+ bytesArchived: 0,
108
+ archivedEventCount: 0,
109
+ retainedEventCount: 0,
110
+ breakdown: {},
111
+ }),
112
+ }));
113
+ const { runAutoCleanupAfterDone } = await import('../wu-done-auto-cleanup.js');
114
+ // Should not throw - cleanup errors are non-fatal
115
+ await expect(runAutoCleanupAfterDone('/test/dir')).resolves.not.toThrow();
116
+ // Should log warning about the error
117
+ expect(consoleWarnSpy).toHaveBeenCalled();
118
+ });
119
+ it('should skip cleanup when trigger is manual', async () => {
120
+ vi.doMock(CONFIG_MODULE_PATH, () => ({
121
+ getConfig: vi.fn().mockReturnValue({
122
+ cleanup: { trigger: 'manual' },
123
+ }),
124
+ }));
125
+ const mockCleanupState = vi.fn();
126
+ vi.doMock('@lumenflow/core/dist/state-cleanup-core.js', () => ({
127
+ cleanupState: mockCleanupState,
128
+ }));
129
+ const { runAutoCleanupAfterDone } = await import('../wu-done-auto-cleanup.js');
130
+ await runAutoCleanupAfterDone('/test/dir');
131
+ // Cleanup should not be called when trigger is manual
132
+ expect(mockCleanupState).not.toHaveBeenCalled();
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,388 @@
1
+ /**
2
+ * WU Lifecycle Integration Tests (WU-1363)
3
+ *
4
+ * Integration tests covering the full WU lifecycle:
5
+ * - AC1: wu:create, wu:claim, wu:status
6
+ * - AC2: wu:prep, wu:done workflow
7
+ *
8
+ * These tests validate the end-to-end behavior of WU lifecycle commands
9
+ * by running them in isolated temporary directories with proper git setup.
10
+ *
11
+ * TDD: Tests written BEFORE implementation verification.
12
+ */
13
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
14
+ import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { tmpdir } from 'node:os';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
19
+ import { WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
20
+ // Test constants
21
+ const TEST_WU_ID = 'WU-9901';
22
+ const TEST_LANE = 'Framework: CLI';
23
+ const TEST_TITLE = 'Integration test WU';
24
+ const TEST_DESCRIPTION = 'Context: Integration test. Problem: Need to test lifecycle. Solution: Run integration tests.';
25
+ /**
26
+ * Helper to create a minimal LumenFlow project structure
27
+ */
28
+ function createTestProject(baseDir) {
29
+ // Create directory structure
30
+ const dirs = [
31
+ 'docs/04-operations/tasks/wu',
32
+ 'docs/04-operations/tasks/initiatives',
33
+ '.lumenflow/state',
34
+ '.lumenflow/stamps',
35
+ 'packages/@lumenflow/cli/src/__tests__',
36
+ ];
37
+ for (const dir of dirs) {
38
+ mkdirSync(join(baseDir, dir), { recursive: true });
39
+ }
40
+ // Create minimal .lumenflow.config.yaml
41
+ const configContent = `
42
+ version: 1
43
+ lanes:
44
+ definitions:
45
+ - name: 'Framework: CLI'
46
+ wip_limit: 1
47
+ code_paths:
48
+ - 'packages/@lumenflow/cli/**'
49
+ git:
50
+ requireRemote: false
51
+ experimental:
52
+ context_validation: false
53
+ `;
54
+ writeFileSync(join(baseDir, '.lumenflow.config.yaml'), configContent);
55
+ // Create minimal package.json
56
+ const packageJson = {
57
+ name: 'test-project',
58
+ version: '1.0.0',
59
+ type: 'module',
60
+ };
61
+ writeFileSync(join(baseDir, 'package.json'), JSON.stringify(packageJson, null, 2));
62
+ // Initialize git repo using execFileSync (safer than execSync)
63
+ execFileSync('git', ['init'], { cwd: baseDir, stdio: 'pipe' });
64
+ execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: baseDir, stdio: 'pipe' });
65
+ execFileSync('git', ['config', 'user.name', 'Test'], { cwd: baseDir, stdio: 'pipe' });
66
+ writeFileSync(join(baseDir, 'README.md'), '# Test Project\n');
67
+ execFileSync('git', ['add', '.'], { cwd: baseDir, stdio: 'pipe' });
68
+ execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: baseDir, stdio: 'pipe' });
69
+ }
70
+ /**
71
+ * Helper to create a WU YAML file directly
72
+ */
73
+ function createWUFile(baseDir, id, options = {}) {
74
+ const wuDir = join(baseDir, 'docs/04-operations/tasks/wu');
75
+ const wuPath = join(wuDir, `${id}.yaml`);
76
+ const doc = {
77
+ id,
78
+ title: options.title || TEST_TITLE,
79
+ lane: options.lane || TEST_LANE,
80
+ status: options.status || WU_STATUS.READY,
81
+ type: 'feature',
82
+ priority: 'P2',
83
+ created: '2026-02-03',
84
+ description: options.description || TEST_DESCRIPTION,
85
+ acceptance: options.acceptance || ['Test criterion 1', 'Test criterion 2'],
86
+ code_paths: options.codePaths || ['packages/@lumenflow/cli/src/__tests__'],
87
+ tests: {
88
+ unit: ['packages/@lumenflow/cli/src/__tests__/wu-lifecycle-integration.test.ts'],
89
+ },
90
+ exposure: 'backend-only',
91
+ };
92
+ writeFileSync(wuPath, stringifyYAML(doc));
93
+ return wuPath;
94
+ }
95
+ describe('WU Lifecycle Integration Tests (WU-1363)', () => {
96
+ let tempDir;
97
+ let originalCwd;
98
+ beforeEach(() => {
99
+ tempDir = join(tmpdir(), `wu-lifecycle-integration-${Date.now()}-${Math.random().toString(36).slice(2)}`);
100
+ mkdirSync(tempDir, { recursive: true });
101
+ originalCwd = process.cwd();
102
+ createTestProject(tempDir);
103
+ vi.resetModules(); // Reset module cache for fresh imports
104
+ });
105
+ afterEach(() => {
106
+ process.chdir(originalCwd);
107
+ if (existsSync(tempDir)) {
108
+ try {
109
+ rmSync(tempDir, { recursive: true, force: true });
110
+ }
111
+ catch {
112
+ // Ignore cleanup errors
113
+ }
114
+ }
115
+ vi.clearAllMocks();
116
+ });
117
+ describe('AC1: Integration tests for wu:create, wu:claim, wu:status', () => {
118
+ describe('wu:create core functionality', () => {
119
+ it('should create a WU YAML file with correct structure', async () => {
120
+ // Arrange
121
+ process.chdir(tempDir);
122
+ // Act - Create WU file directly (simulating wu:create core behavior)
123
+ const wuPath = createWUFile(tempDir, TEST_WU_ID, {
124
+ status: WU_STATUS.READY,
125
+ lane: TEST_LANE,
126
+ title: TEST_TITLE,
127
+ description: TEST_DESCRIPTION,
128
+ acceptance: ['Criterion 1', 'Criterion 2'],
129
+ codePaths: ['packages/@lumenflow/cli/src'],
130
+ });
131
+ // Assert
132
+ expect(existsSync(wuPath)).toBe(true);
133
+ const content = readFileSync(wuPath, 'utf-8');
134
+ const doc = parseYAML(content);
135
+ expect(doc.id).toBe(TEST_WU_ID);
136
+ expect(doc.lane).toBe(TEST_LANE);
137
+ expect(doc.status).toBe(WU_STATUS.READY);
138
+ expect(doc.title).toBe(TEST_TITLE);
139
+ expect(doc.acceptance).toHaveLength(2);
140
+ });
141
+ it('should validate required fields are present', () => {
142
+ // Arrange
143
+ process.chdir(tempDir);
144
+ // Create a minimal WU and verify required fields
145
+ const wuPath = createWUFile(tempDir, TEST_WU_ID);
146
+ const content = readFileSync(wuPath, 'utf-8');
147
+ const doc = parseYAML(content);
148
+ // Assert required fields exist
149
+ expect(doc.id).toBeDefined();
150
+ expect(doc.title).toBeDefined();
151
+ expect(doc.lane).toBeDefined();
152
+ expect(doc.status).toBeDefined();
153
+ expect(doc.description).toBeDefined();
154
+ expect(doc.acceptance).toBeDefined();
155
+ expect(doc.code_paths).toBeDefined();
156
+ });
157
+ });
158
+ describe('wu:claim core functionality', () => {
159
+ it('should update WU status to in_progress when claimed', async () => {
160
+ // Arrange
161
+ process.chdir(tempDir);
162
+ const wuPath = createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
163
+ // Act - Simulate claim by updating status
164
+ const content = readFileSync(wuPath, 'utf-8');
165
+ const doc = parseYAML(content);
166
+ doc.status = WU_STATUS.IN_PROGRESS;
167
+ doc.claimed_at = new Date().toISOString();
168
+ doc.worktree_path = `worktrees/framework-cli-${TEST_WU_ID.toLowerCase()}`;
169
+ writeFileSync(wuPath, stringifyYAML(doc));
170
+ // Assert
171
+ const updatedContent = readFileSync(wuPath, 'utf-8');
172
+ const updatedDoc = parseYAML(updatedContent);
173
+ expect(updatedDoc.status).toBe(WU_STATUS.IN_PROGRESS);
174
+ expect(updatedDoc.claimed_at).toBeDefined();
175
+ expect(updatedDoc.worktree_path).toContain('worktrees');
176
+ });
177
+ it('should reject claim when WU does not exist', () => {
178
+ // Arrange
179
+ process.chdir(tempDir);
180
+ const nonExistentPath = join(tempDir, 'docs/04-operations/tasks/wu', 'WU-9999.yaml');
181
+ // Assert
182
+ expect(existsSync(nonExistentPath)).toBe(false);
183
+ });
184
+ it('should reject claim when WU is not in ready status', () => {
185
+ // Arrange
186
+ process.chdir(tempDir);
187
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.DONE });
188
+ // Read and verify status prevents claiming
189
+ const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
190
+ const content = readFileSync(wuPath, 'utf-8');
191
+ const doc = parseYAML(content);
192
+ // Assert
193
+ expect(doc.status).toBe(WU_STATUS.DONE);
194
+ expect(doc.status).not.toBe(WU_STATUS.READY);
195
+ });
196
+ });
197
+ describe('wu:status core functionality', () => {
198
+ it('should return WU details correctly', () => {
199
+ // Arrange
200
+ process.chdir(tempDir);
201
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
202
+ // Act - Read WU status
203
+ const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
204
+ const content = readFileSync(wuPath, 'utf-8');
205
+ const doc = parseYAML(content);
206
+ // Assert
207
+ expect(doc.id).toBe(TEST_WU_ID);
208
+ expect(doc.status).toBe(WU_STATUS.READY);
209
+ expect(doc.lane).toBe(TEST_LANE);
210
+ });
211
+ it('should return valid commands for ready status', () => {
212
+ // Arrange
213
+ process.chdir(tempDir);
214
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
215
+ // Act
216
+ const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
217
+ const content = readFileSync(wuPath, 'utf-8');
218
+ const doc = parseYAML(content);
219
+ // Compute valid commands based on status
220
+ const validCommands = doc.status === WU_STATUS.READY
221
+ ? ['wu:claim', 'wu:edit', 'wu:delete']
222
+ : ['wu:prep', 'wu:block'];
223
+ // Assert
224
+ expect(validCommands).toContain('wu:claim');
225
+ });
226
+ it('should return valid commands for in_progress status', () => {
227
+ // Arrange
228
+ process.chdir(tempDir);
229
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
230
+ // Act
231
+ const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
232
+ const content = readFileSync(wuPath, 'utf-8');
233
+ const doc = parseYAML(content);
234
+ // Compute valid commands based on status
235
+ const validCommands = doc.status === WU_STATUS.IN_PROGRESS ? ['wu:prep', 'wu:block'] : ['wu:claim'];
236
+ // Assert
237
+ expect(validCommands).toContain('wu:prep');
238
+ });
239
+ });
240
+ });
241
+ describe('AC2: Integration tests for wu:prep, wu:done workflow', () => {
242
+ describe('wu:prep core functionality', () => {
243
+ it('should validate WU is in in_progress status', () => {
244
+ // Arrange
245
+ process.chdir(tempDir);
246
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
247
+ // Act
248
+ const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
249
+ const content = readFileSync(wuPath, 'utf-8');
250
+ const doc = parseYAML(content);
251
+ // Assert
252
+ expect(doc.status).toBe(WU_STATUS.IN_PROGRESS);
253
+ });
254
+ it('should generate next command pointing to wu:done', () => {
255
+ // Arrange
256
+ process.chdir(tempDir);
257
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
258
+ // Act - Generate next command
259
+ const mainCheckoutPath = tempDir;
260
+ const nextCommand = `cd ${mainCheckoutPath} && pnpm wu:done --id ${TEST_WU_ID}`;
261
+ // Assert
262
+ expect(nextCommand).toContain('wu:done');
263
+ expect(nextCommand).toContain(TEST_WU_ID);
264
+ expect(nextCommand).toContain(mainCheckoutPath);
265
+ });
266
+ it('should reject prep when WU is not in_progress', () => {
267
+ // Arrange
268
+ process.chdir(tempDir);
269
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
270
+ // Act
271
+ const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
272
+ const content = readFileSync(wuPath, 'utf-8');
273
+ const doc = parseYAML(content);
274
+ // Assert
275
+ expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
276
+ });
277
+ });
278
+ describe('wu:done core functionality', () => {
279
+ it('should create stamp file on completion', () => {
280
+ // Arrange
281
+ process.chdir(tempDir);
282
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
283
+ // Act - Create stamp file
284
+ const stampDir = join(tempDir, '.lumenflow/stamps');
285
+ mkdirSync(stampDir, { recursive: true });
286
+ const stampPath = join(stampDir, `${TEST_WU_ID}.done`);
287
+ const stampContent = {
288
+ completed_at: new Date().toISOString(),
289
+ wu_id: TEST_WU_ID,
290
+ };
291
+ writeFileSync(stampPath, JSON.stringify(stampContent, null, 2));
292
+ // Assert
293
+ expect(existsSync(stampPath)).toBe(true);
294
+ const savedStamp = JSON.parse(readFileSync(stampPath, 'utf-8'));
295
+ expect(savedStamp.wu_id).toBe(TEST_WU_ID);
296
+ });
297
+ it('should update WU status to done', () => {
298
+ // Arrange
299
+ process.chdir(tempDir);
300
+ const wuPath = createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
301
+ // Act - Update status to done
302
+ const content = readFileSync(wuPath, 'utf-8');
303
+ const doc = parseYAML(content);
304
+ doc.status = WU_STATUS.DONE;
305
+ doc.completed_at = new Date().toISOString();
306
+ writeFileSync(wuPath, stringifyYAML(doc));
307
+ // Assert
308
+ const updatedContent = readFileSync(wuPath, 'utf-8');
309
+ const updatedDoc = parseYAML(updatedContent);
310
+ expect(updatedDoc.status).toBe(WU_STATUS.DONE);
311
+ expect(updatedDoc.completed_at).toBeDefined();
312
+ });
313
+ it('should reject done when WU is not in_progress', () => {
314
+ // Arrange
315
+ process.chdir(tempDir);
316
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
317
+ // Act
318
+ const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
319
+ const content = readFileSync(wuPath, 'utf-8');
320
+ const doc = parseYAML(content);
321
+ // Assert - Cannot complete a WU that isn't in_progress
322
+ expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
323
+ });
324
+ it('should support skip-gates flag with reason and fix-wu', () => {
325
+ // Arrange
326
+ process.chdir(tempDir);
327
+ createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
328
+ // Act - Simulate skip-gates audit log
329
+ const skipGatesLog = {
330
+ wu_id: TEST_WU_ID,
331
+ skipped_at: new Date().toISOString(),
332
+ reason: 'pre-existing on main',
333
+ fix_wu: 'WU-1234',
334
+ };
335
+ const auditPath = join(tempDir, '.lumenflow/skip-gates-audit.log');
336
+ writeFileSync(auditPath, JSON.stringify(skipGatesLog) + '\n');
337
+ // Assert
338
+ expect(existsSync(auditPath)).toBe(true);
339
+ const logContent = readFileSync(auditPath, 'utf-8');
340
+ expect(logContent).toContain('pre-existing on main');
341
+ expect(logContent).toContain('WU-1234');
342
+ });
343
+ });
344
+ describe('wu:prep + wu:done complete workflow', () => {
345
+ it('should complete full lifecycle from create to done', () => {
346
+ // This test validates the complete workflow state transitions:
347
+ // 1. Create WU (status: ready)
348
+ // 2. Claim WU (status: in_progress, worktree created)
349
+ // 3. Prep WU (gates run, provides next command)
350
+ // 4. Done WU (status: done, stamp created)
351
+ // Arrange
352
+ process.chdir(tempDir);
353
+ // Step 1: Create WU
354
+ const wuPath = createWUFile(tempDir, TEST_WU_ID, {
355
+ status: WU_STATUS.READY,
356
+ lane: TEST_LANE,
357
+ title: TEST_TITLE,
358
+ description: TEST_DESCRIPTION,
359
+ acceptance: ['Full lifecycle test'],
360
+ });
361
+ expect(existsSync(wuPath)).toBe(true);
362
+ // Step 2: Simulate Claim WU
363
+ let doc = parseYAML(readFileSync(wuPath, 'utf-8'));
364
+ expect(doc.status).toBe(WU_STATUS.READY);
365
+ doc.status = WU_STATUS.IN_PROGRESS;
366
+ doc.claimed_at = new Date().toISOString();
367
+ doc.worktree_path = `worktrees/framework-cli-${TEST_WU_ID.toLowerCase()}`;
368
+ writeFileSync(wuPath, stringifyYAML(doc));
369
+ // Step 3: Verify Prep is valid (status check)
370
+ doc = parseYAML(readFileSync(wuPath, 'utf-8'));
371
+ expect(doc.status).toBe(WU_STATUS.IN_PROGRESS);
372
+ const nextCommand = `cd ${tempDir} && pnpm wu:done --id ${TEST_WU_ID}`;
373
+ expect(nextCommand).toContain('wu:done');
374
+ // Step 4: Complete WU
375
+ doc.status = WU_STATUS.DONE;
376
+ doc.completed_at = new Date().toISOString();
377
+ writeFileSync(wuPath, stringifyYAML(doc));
378
+ // Create stamp
379
+ const stampPath = join(tempDir, '.lumenflow/stamps', `${TEST_WU_ID}.done`);
380
+ writeFileSync(stampPath, JSON.stringify({ completed_at: new Date().toISOString() }));
381
+ // Verify final state
382
+ expect(existsSync(stampPath)).toBe(true);
383
+ doc = parseYAML(readFileSync(wuPath, 'utf-8'));
384
+ expect(doc.status).toBe(WU_STATUS.DONE);
385
+ });
386
+ });
387
+ });
388
+ });
@@ -17,7 +17,6 @@ import path from 'node:path';
17
17
  import { readWURaw, writeWU, appendNote } from '@lumenflow/core/dist/wu-yaml.js';
18
18
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
19
19
  import { CLI_FLAGS, EXIT_CODES, EMOJI, WU_STATUS, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
20
- /* eslint-disable security/detect-non-literal-fs-filename */
21
20
  /** Log prefix for consistent output */
22
21
  const LOG_PREFIX = '[backlog-prune]';
23
22
  /**
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-console -- CLI entry point uses console for error output */
2
1
  /**
3
2
  * Shared CLI entry point wrapper
4
3
  *