@lumenflow/cli 1.3.0 → 1.3.3
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/dist/__tests__/cli-entry-point.test.js +111 -0
- package/dist/__tests__/cli-subprocess.test.js +64 -0
- package/dist/__tests__/wu-done-staging-whitelist.test.js +35 -0
- package/dist/agent-issues-query.js +0 -0
- package/dist/agent-log-issue.js +0 -0
- package/dist/agent-session-end.js +0 -0
- package/dist/agent-session.js +0 -0
- package/dist/cli-entry-point.js +51 -0
- package/dist/flow-bottlenecks.js +0 -0
- package/dist/flow-report.js +0 -0
- package/dist/gates.js +4 -2
- package/dist/init.js +39 -3
- package/dist/initiative-add-wu.js +2 -1
- package/dist/initiative-bulk-assign-wus.js +0 -0
- package/dist/initiative-create.js +2 -1
- package/dist/initiative-edit.js +0 -0
- package/dist/initiative-list.js +2 -1
- package/dist/initiative-status.js +2 -1
- package/dist/mem-checkpoint.js +0 -0
- package/dist/mem-cleanup.js +0 -0
- package/dist/mem-create.js +0 -0
- package/dist/mem-inbox.js +0 -0
- package/dist/mem-init.js +0 -0
- package/dist/mem-ready.js +0 -0
- package/dist/mem-signal.js +0 -0
- package/dist/mem-start.js +0 -0
- package/dist/mem-summarize.js +0 -0
- package/dist/mem-triage.js +0 -0
- package/dist/metrics-snapshot.js +0 -0
- package/dist/orchestrate-init-status.js +0 -0
- package/dist/orchestrate-initiative.js +0 -0
- package/dist/orchestrate-monitor.js +0 -0
- package/dist/spawn-list.js +0 -0
- package/dist/wu-block.js +0 -0
- package/dist/wu-claim.js +6 -3
- package/dist/wu-cleanup.js +82 -56
- package/dist/wu-create.js +63 -8
- package/dist/wu-delete.js +0 -0
- package/dist/wu-deps.js +2 -1
- package/dist/wu-done.js +9 -3
- package/dist/wu-edit.js +0 -0
- package/dist/wu-infer-lane.js +3 -2
- package/dist/wu-preflight.js +2 -1
- package/dist/wu-prune.js +12 -3
- package/dist/wu-repair.js +2 -1
- package/dist/wu-spawn.js +8 -5
- package/dist/wu-unblock.js +0 -0
- package/dist/wu-unlock-lane.js +6 -1
- package/dist/wu-validate.js +2 -1
- package/package.json +15 -15
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI entry point error handling
|
|
3
|
+
*
|
|
4
|
+
* Verifies that runCLI wrapper properly:
|
|
5
|
+
* - Catches async errors from main()
|
|
6
|
+
* - Logs error messages to stderr
|
|
7
|
+
* - Exits with EXIT_CODES.ERROR on failure
|
|
8
|
+
*
|
|
9
|
+
* WU-1071: Also verifies that CLI entry points use import.meta.main pattern
|
|
10
|
+
* instead of the broken process.argv[1] === fileURLToPath(import.meta.url) pattern.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { runCLI } from '../cli-entry-point.js';
|
|
16
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
17
|
+
describe('runCLI', () => {
|
|
18
|
+
let mockExit;
|
|
19
|
+
let mockConsoleError;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
22
|
+
mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
mockExit.mockRestore();
|
|
26
|
+
mockConsoleError.mockRestore();
|
|
27
|
+
});
|
|
28
|
+
it('should call main() and do nothing on success', async () => {
|
|
29
|
+
const main = vi.fn().mockResolvedValue(undefined);
|
|
30
|
+
await runCLI(main);
|
|
31
|
+
expect(main).toHaveBeenCalledOnce();
|
|
32
|
+
expect(mockExit).not.toHaveBeenCalled();
|
|
33
|
+
expect(mockConsoleError).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
it('should catch errors and exit with ERROR code', async () => {
|
|
36
|
+
const error = new Error('Test error message');
|
|
37
|
+
const main = vi.fn().mockRejectedValue(error);
|
|
38
|
+
await runCLI(main);
|
|
39
|
+
expect(main).toHaveBeenCalledOnce();
|
|
40
|
+
expect(mockConsoleError).toHaveBeenCalledWith('Test error message');
|
|
41
|
+
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
42
|
+
});
|
|
43
|
+
it('should handle errors without message property', async () => {
|
|
44
|
+
const main = vi.fn().mockRejectedValue('string error');
|
|
45
|
+
await runCLI(main);
|
|
46
|
+
expect(mockConsoleError).toHaveBeenCalledWith('string error');
|
|
47
|
+
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
48
|
+
});
|
|
49
|
+
it('should handle null/undefined errors', async () => {
|
|
50
|
+
const main = vi.fn().mockRejectedValue(null);
|
|
51
|
+
await runCLI(main);
|
|
52
|
+
expect(mockConsoleError).toHaveBeenCalledWith('Unknown error');
|
|
53
|
+
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* WU-1071: Verify CLI entry points use import.meta.main pattern
|
|
58
|
+
*
|
|
59
|
+
* The old pattern `process.argv[1] === fileURLToPath(import.meta.url)` fails with
|
|
60
|
+
* pnpm symlinks because process.argv[1] is the symlink path but import.meta.url
|
|
61
|
+
* resolves to the real path - they never match so main() is never called.
|
|
62
|
+
*
|
|
63
|
+
* The fix is to use `import.meta.main` (Node.js 22.16.0+ built-in) which correctly
|
|
64
|
+
* handles symlinks.
|
|
65
|
+
*/
|
|
66
|
+
describe('WU-1071: CLI entry point patterns', () => {
|
|
67
|
+
// CLI files that should have the main() entry guard
|
|
68
|
+
const CLI_FILES_WITH_ENTRY_GUARD = [
|
|
69
|
+
'gates.ts',
|
|
70
|
+
'wu-spawn.ts',
|
|
71
|
+
'wu-create.ts',
|
|
72
|
+
'wu-claim.ts',
|
|
73
|
+
'wu-done.ts',
|
|
74
|
+
];
|
|
75
|
+
// Old broken pattern that fails with pnpm symlinks
|
|
76
|
+
const OLD_BROKEN_PATTERN = /if\s*\(\s*process\.argv\[1\]\s*===\s*fileURLToPath\(import\.meta\.url\)\s*\)/;
|
|
77
|
+
// New working pattern using import.meta.main
|
|
78
|
+
const NEW_WORKING_PATTERN = /if\s*\(\s*import\.meta\.main\s*\)/;
|
|
79
|
+
it('should use import.meta.main instead of process.argv[1] comparison', () => {
|
|
80
|
+
const srcDir = path.resolve(__dirname, '..');
|
|
81
|
+
for (const file of CLI_FILES_WITH_ENTRY_GUARD) {
|
|
82
|
+
const filePath = path.join(srcDir, file);
|
|
83
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
84
|
+
// Should NOT have old broken pattern
|
|
85
|
+
expect(OLD_BROKEN_PATTERN.test(content), `${file} should not use the old broken pattern (process.argv[1] === fileURLToPath)`).toBe(false);
|
|
86
|
+
// Should have new working pattern
|
|
87
|
+
expect(NEW_WORKING_PATTERN.test(content), `${file} should use import.meta.main pattern`).toBe(true);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it('should not have unused fileURLToPath imports in CLI files with entry guards', () => {
|
|
91
|
+
const srcDir = path.resolve(__dirname, '..');
|
|
92
|
+
for (const file of CLI_FILES_WITH_ENTRY_GUARD) {
|
|
93
|
+
const filePath = path.join(srcDir, file);
|
|
94
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
95
|
+
// If the file imports fileURLToPath, it should actually use it somewhere
|
|
96
|
+
// (not just for the now-removed entry guard pattern)
|
|
97
|
+
const hasFileURLToPathImport = /import\s*{[^}]*fileURLToPath[^}]*}\s*from\s*['"]node:url['"]/.test(content);
|
|
98
|
+
const usesFileURLToPath = /fileURLToPath\(/.test(content);
|
|
99
|
+
if (hasFileURLToPathImport && !usesFileURLToPath) {
|
|
100
|
+
expect.fail(`${file} imports fileURLToPath but does not use it - remove unused import`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
it('cli-entry-point.ts JSDoc should document import.meta.main pattern', () => {
|
|
105
|
+
const srcDir = path.resolve(__dirname, '..');
|
|
106
|
+
const cliEntryPointPath = path.join(srcDir, 'cli-entry-point.ts');
|
|
107
|
+
const content = readFileSync(cliEntryPointPath, 'utf-8');
|
|
108
|
+
// JSDoc example should show import.meta.main pattern, not the old one
|
|
109
|
+
expect(content.includes('import.meta.main'), 'cli-entry-point.ts JSDoc should mention import.meta.main').toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for CLI subprocess execution
|
|
3
|
+
*
|
|
4
|
+
* Tests that CLI commands properly:
|
|
5
|
+
* - Exit with non-zero code on errors
|
|
6
|
+
* - Output error messages to stderr
|
|
7
|
+
* - Don't silently fail
|
|
8
|
+
*
|
|
9
|
+
* These tests run CLI commands as subprocesses to verify
|
|
10
|
+
* the entry point error handling works end-to-end.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
const CLI_DIR = resolve(__dirname, '../../dist');
|
|
16
|
+
/**
|
|
17
|
+
* Helper to run a CLI command as subprocess
|
|
18
|
+
*/
|
|
19
|
+
function runCLI(command, args = []) {
|
|
20
|
+
const result = spawnSync('node', [resolve(CLI_DIR, `${command}.js`), ...args], {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
timeout: 10000,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
code: result.status ?? -1,
|
|
26
|
+
stdout: result.stdout ?? '',
|
|
27
|
+
stderr: result.stderr ?? '',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe('CLI subprocess error handling', () => {
|
|
31
|
+
describe('wu-claim', () => {
|
|
32
|
+
it('should exit with non-zero code when required options are missing', () => {
|
|
33
|
+
const result = runCLI('wu-claim', ['--id', 'WU-TEST']);
|
|
34
|
+
// Should NOT exit 0 (silent failure)
|
|
35
|
+
expect(result.code).not.toBe(0);
|
|
36
|
+
// Should have some error output
|
|
37
|
+
expect(result.stderr.length + result.stdout.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
it('should output help when --help is passed', () => {
|
|
40
|
+
const result = runCLI('wu-claim', ['--help']);
|
|
41
|
+
// Help should work
|
|
42
|
+
expect(result.code).toBe(0);
|
|
43
|
+
expect(result.stdout).toContain('Usage');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('wu-done', () => {
|
|
47
|
+
it('should exit with non-zero code for non-existent WU', () => {
|
|
48
|
+
const result = runCLI('wu-done', ['--id', 'WU-NONEXISTENT-99999']);
|
|
49
|
+
// Should NOT exit 0 (silent failure)
|
|
50
|
+
expect(result.code).not.toBe(0);
|
|
51
|
+
// Should have some error output
|
|
52
|
+
expect(result.stderr.length + result.stdout.length).toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('wu-create', () => {
|
|
56
|
+
it('should exit with non-zero code when validation fails', () => {
|
|
57
|
+
const result = runCLI('wu-create', ['--id', 'WU-TEST', '--lane', 'Invalid Lane']);
|
|
58
|
+
// Should NOT exit 0 (silent failure)
|
|
59
|
+
expect(result.code).not.toBe(0);
|
|
60
|
+
// Should have some error output
|
|
61
|
+
expect(result.stderr.length + result.stdout.length).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for wu-done staging whitelist patterns
|
|
3
|
+
* @see WU-1072: Fix staging whitelist for auto-generated docs
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
/**
|
|
7
|
+
* Pattern matching logic extracted from validateStagedFiles in wu-done.ts
|
|
8
|
+
* This tests the MDX whitelist pattern added in WU-1072
|
|
9
|
+
*/
|
|
10
|
+
function isWhitelistedAutoGeneratedDoc(file) {
|
|
11
|
+
return file.startsWith('apps/docs/') && file.endsWith('.mdx');
|
|
12
|
+
}
|
|
13
|
+
describe('wu-done staging whitelist', () => {
|
|
14
|
+
describe('auto-generated docs pattern (WU-1072)', () => {
|
|
15
|
+
it('should whitelist MDX files in apps/docs/', () => {
|
|
16
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/api/wu-create.mdx')).toBe(true);
|
|
17
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/commands/gates.mdx')).toBe(true);
|
|
18
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/nested/deep/file.mdx')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it('should not whitelist MDX files outside apps/docs/', () => {
|
|
21
|
+
expect(isWhitelistedAutoGeneratedDoc('docs/something.mdx')).toBe(false);
|
|
22
|
+
expect(isWhitelistedAutoGeneratedDoc('packages/cli/README.mdx')).toBe(false);
|
|
23
|
+
expect(isWhitelistedAutoGeneratedDoc('src/docs/file.mdx')).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it('should not whitelist non-MDX files in apps/docs/', () => {
|
|
26
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/config.ts')).toBe(false);
|
|
27
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/package.json')).toBe(false);
|
|
28
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/styles.css')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it('should not whitelist files with mdx in the path but not as extension', () => {
|
|
31
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/mdx-utils/helper.ts')).toBe(false);
|
|
32
|
+
expect(isWhitelistedAutoGeneratedDoc('apps/docs/mdx')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
File without changes
|
package/dist/agent-log-issue.js
CHANGED
|
File without changes
|
|
File without changes
|
package/dist/agent-session.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI entry point wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error handling for all CLI commands.
|
|
5
|
+
* Catches async errors, logs them, and exits with proper code.
|
|
6
|
+
*
|
|
7
|
+
* WU-1071: Use import.meta.main (Node.js 22.16.0+) instead of the old
|
|
8
|
+
* process.argv[1] === fileURLToPath(import.meta.url) pattern. The old
|
|
9
|
+
* pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
10
|
+
* path but import.meta.url resolves to the real path - they never match
|
|
11
|
+
* so main() is never called.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // At the bottom of each CLI file:
|
|
16
|
+
* import { runCLI } from './cli-entry-point.js';
|
|
17
|
+
*
|
|
18
|
+
* if (import.meta.main) {
|
|
19
|
+
* runCLI(main);
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
24
|
+
/**
|
|
25
|
+
* Wraps an async main function with proper error handling.
|
|
26
|
+
*
|
|
27
|
+
* @param main - The async main function to execute
|
|
28
|
+
* @returns Promise that resolves when main completes (or after error handling)
|
|
29
|
+
*/
|
|
30
|
+
export async function runCLI(main) {
|
|
31
|
+
try {
|
|
32
|
+
await main();
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const message = getErrorMessage(err);
|
|
36
|
+
console.error(message);
|
|
37
|
+
process.exit(EXIT_CODES.ERROR);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extracts error message from unknown error type.
|
|
42
|
+
*/
|
|
43
|
+
function getErrorMessage(err) {
|
|
44
|
+
if (err === null || err === undefined) {
|
|
45
|
+
return 'Unknown error';
|
|
46
|
+
}
|
|
47
|
+
if (err instanceof Error) {
|
|
48
|
+
return err.message;
|
|
49
|
+
}
|
|
50
|
+
return String(err);
|
|
51
|
+
}
|
package/dist/flow-bottlenecks.js
CHANGED
|
File without changes
|
package/dist/flow-report.js
CHANGED
|
File without changes
|
package/dist/gates.js
CHANGED
|
@@ -42,7 +42,6 @@ import { execSync, spawnSync } from 'node:child_process';
|
|
|
42
42
|
import { closeSync, mkdirSync, openSync, readSync, statSync, writeSync } from 'node:fs';
|
|
43
43
|
import { access } from 'node:fs/promises';
|
|
44
44
|
import path from 'node:path';
|
|
45
|
-
import { fileURLToPath } from 'node:url';
|
|
46
45
|
import { emitGateEvent, getCurrentWU, getCurrentLane } from '@lumenflow/core/dist/telemetry.js';
|
|
47
46
|
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
48
47
|
import { getChangedLintableFiles, convertToPackageRelativePaths, } from '@lumenflow/core/dist/incremental-lint.js';
|
|
@@ -753,7 +752,10 @@ async function main() {
|
|
|
753
752
|
}
|
|
754
753
|
process.exit(EXIT_CODES.SUCCESS);
|
|
755
754
|
}
|
|
756
|
-
|
|
755
|
+
// WU-1071: Use import.meta.main instead of process.argv[1] comparison
|
|
756
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
757
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
758
|
+
if (import.meta.main) {
|
|
757
759
|
main().catch((error) => {
|
|
758
760
|
console.error('Gates failed:', error);
|
|
759
761
|
process.exit(EXIT_CODES.ERROR);
|
package/dist/init.js
CHANGED
|
@@ -8,6 +8,8 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import * as yaml from 'yaml';
|
|
10
10
|
import { getDefaultConfig } from '@lumenflow/core';
|
|
11
|
+
// WU-1067: Import GATE_PRESETS for --preset support
|
|
12
|
+
import { GATE_PRESETS } from '@lumenflow/core/dist/gates-config.js';
|
|
11
13
|
const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
|
|
12
14
|
const FRAMEWORK_HINT_FILE = '.lumenflow.framework.yaml';
|
|
13
15
|
const LUMENFLOW_DIR = '.lumenflow';
|
|
@@ -16,11 +18,20 @@ const CLAUDE_DIR = '.claude';
|
|
|
16
18
|
const CLAUDE_AGENTS_DIR = path.join(CLAUDE_DIR, 'agents');
|
|
17
19
|
/**
|
|
18
20
|
* Generate YAML configuration with header comment
|
|
21
|
+
* WU-1067: Supports --preset option for config-driven gates
|
|
19
22
|
*/
|
|
20
|
-
function generateLumenflowConfigYaml() {
|
|
23
|
+
function generateLumenflowConfigYaml(gatePreset) {
|
|
21
24
|
const header = `# LumenFlow Configuration\n# Generated by: lumenflow init\n# Customize paths based on your project structure\n\n`;
|
|
22
25
|
const config = getDefaultConfig();
|
|
23
26
|
config.directories.agentsDir = LUMENFLOW_AGENTS_DIR;
|
|
27
|
+
// WU-1067: Add gates.execution section with preset if specified
|
|
28
|
+
if (gatePreset && GATE_PRESETS[gatePreset]) {
|
|
29
|
+
const presetConfig = GATE_PRESETS[gatePreset];
|
|
30
|
+
config.gates.execution = {
|
|
31
|
+
preset: gatePreset,
|
|
32
|
+
...presetConfig,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
24
35
|
return header + yaml.stringify(config);
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
@@ -144,6 +155,28 @@ function parseFrameworkArg(args) {
|
|
|
144
155
|
}
|
|
145
156
|
return undefined;
|
|
146
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* WU-1067: Parse --preset flag from arguments for config-driven gates
|
|
160
|
+
*/
|
|
161
|
+
function parsePresetArg(args) {
|
|
162
|
+
const presetArg = args.find((arg) => arg.startsWith('--preset='));
|
|
163
|
+
if (presetArg) {
|
|
164
|
+
const [, value] = presetArg.split('=', 2);
|
|
165
|
+
const preset = value?.trim().toLowerCase();
|
|
166
|
+
if (preset && ['node', 'python', 'go', 'rust', 'dotnet'].includes(preset)) {
|
|
167
|
+
return preset;
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const presetIndex = args.findIndex((arg) => arg === '--preset');
|
|
172
|
+
if (presetIndex !== -1 && args[presetIndex + 1]) {
|
|
173
|
+
const preset = args[presetIndex + 1].toLowerCase();
|
|
174
|
+
if (['node', 'python', 'go', 'rust', 'dotnet'].includes(preset)) {
|
|
175
|
+
return preset;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
147
180
|
function shouldUseVendor(vendor, defaultClient) {
|
|
148
181
|
if (vendor) {
|
|
149
182
|
return vendor;
|
|
@@ -168,8 +201,8 @@ export async function scaffoldProject(targetDir, options) {
|
|
|
168
201
|
DATE: getCurrentDate(),
|
|
169
202
|
PROJECT_ROOT: targetDir,
|
|
170
203
|
};
|
|
171
|
-
// Create .lumenflow.config.yaml
|
|
172
|
-
await createFile(path.join(targetDir, CONFIG_FILE_NAME), generateLumenflowConfigYaml(), options.force, result, targetDir);
|
|
204
|
+
// Create .lumenflow.config.yaml (WU-1067: includes gate preset if specified)
|
|
205
|
+
await createFile(path.join(targetDir, CONFIG_FILE_NAME), generateLumenflowConfigYaml(options.gatePreset), options.force, result, targetDir);
|
|
173
206
|
// Create LUMENFLOW.md (main entry point)
|
|
174
207
|
await createFile(path.join(targetDir, 'LUMENFLOW.md'), processTemplate(LUMENFLOW_MD_TEMPLATE, tokenDefaults), options.force, result, targetDir);
|
|
175
208
|
// Create .lumenflow/constraints.md
|
|
@@ -271,16 +304,19 @@ export async function main() {
|
|
|
271
304
|
const full = args.includes('--full');
|
|
272
305
|
const vendor = parseVendorArg(args);
|
|
273
306
|
const framework = parseFrameworkArg(args);
|
|
307
|
+
const gatePreset = parsePresetArg(args); // WU-1067
|
|
274
308
|
const targetDir = process.cwd();
|
|
275
309
|
console.log('[lumenflow init] Scaffolding LumenFlow project...');
|
|
276
310
|
console.log(` Mode: ${full ? 'full' : 'minimal'}`);
|
|
277
311
|
console.log(` Framework: ${framework ?? 'none'}`);
|
|
278
312
|
console.log(` Vendor overlays: ${vendor ?? 'auto'}`);
|
|
313
|
+
console.log(` Gate preset: ${gatePreset ?? 'none (manual config)'}`);
|
|
279
314
|
const result = await scaffoldProject(targetDir, {
|
|
280
315
|
force,
|
|
281
316
|
full,
|
|
282
317
|
vendor,
|
|
283
318
|
framework,
|
|
319
|
+
gatePreset,
|
|
284
320
|
});
|
|
285
321
|
if (result.created.length > 0) {
|
|
286
322
|
console.log('\nCreated:');
|
|
@@ -229,6 +229,7 @@ async function main() {
|
|
|
229
229
|
}
|
|
230
230
|
// Guard main() for testability
|
|
231
231
|
import { fileURLToPath } from 'node:url';
|
|
232
|
+
import { runCLI } from './cli-entry-point.js';
|
|
232
233
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
233
|
-
main
|
|
234
|
+
runCLI(main);
|
|
234
235
|
}
|
|
File without changes
|
|
@@ -163,6 +163,7 @@ async function main() {
|
|
|
163
163
|
}
|
|
164
164
|
// Guard main() for testability
|
|
165
165
|
import { fileURLToPath } from 'node:url';
|
|
166
|
+
import { runCLI } from './cli-entry-point.js';
|
|
166
167
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
167
|
-
main
|
|
168
|
+
runCLI(main);
|
|
168
169
|
}
|
package/dist/initiative-edit.js
CHANGED
|
File without changes
|
package/dist/initiative-list.js
CHANGED
|
@@ -96,6 +96,7 @@ async function main() {
|
|
|
96
96
|
}
|
|
97
97
|
// Guard main() for testability (WU-1366)
|
|
98
98
|
import { fileURLToPath } from 'node:url';
|
|
99
|
+
import { runCLI } from './cli-entry-point.js';
|
|
99
100
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
100
|
-
main
|
|
101
|
+
runCLI(main);
|
|
101
102
|
}
|
|
@@ -216,6 +216,7 @@ async function main() {
|
|
|
216
216
|
}
|
|
217
217
|
// Guard main() for testability (WU-1366)
|
|
218
218
|
import { fileURLToPath } from 'node:url';
|
|
219
|
+
import { runCLI } from './cli-entry-point.js';
|
|
219
220
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
220
|
-
main
|
|
221
|
+
runCLI(main);
|
|
221
222
|
}
|
package/dist/mem-checkpoint.js
CHANGED
|
File without changes
|
package/dist/mem-cleanup.js
CHANGED
|
File without changes
|
package/dist/mem-create.js
CHANGED
|
File without changes
|
package/dist/mem-inbox.js
CHANGED
|
File without changes
|
package/dist/mem-init.js
CHANGED
|
File without changes
|
package/dist/mem-ready.js
CHANGED
|
File without changes
|
package/dist/mem-signal.js
CHANGED
|
File without changes
|
package/dist/mem-start.js
CHANGED
|
File without changes
|
package/dist/mem-summarize.js
CHANGED
|
File without changes
|
package/dist/mem-triage.js
CHANGED
|
File without changes
|
package/dist/metrics-snapshot.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dist/spawn-list.js
CHANGED
|
File without changes
|
package/dist/wu-block.js
CHANGED
|
File without changes
|
package/dist/wu-claim.js
CHANGED
|
@@ -1297,7 +1297,10 @@ async function main() {
|
|
|
1297
1297
|
}
|
|
1298
1298
|
}
|
|
1299
1299
|
// Guard main() for testability (WU-1366)
|
|
1300
|
-
import
|
|
1301
|
-
|
|
1302
|
-
|
|
1300
|
+
// WU-1071: Use import.meta.main instead of process.argv[1] comparison
|
|
1301
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
1302
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
1303
|
+
import { runCLI } from './cli-entry-point.js';
|
|
1304
|
+
if (import.meta.main) {
|
|
1305
|
+
runCLI(main);
|
|
1303
1306
|
}
|
package/dist/wu-cleanup.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Cleans up worktree and branch after PR merge (PR-based completion workflow).
|
|
6
6
|
*
|
|
7
7
|
* Sequence:
|
|
8
|
-
* 1) Verify PR is merged (via gh API
|
|
8
|
+
* 1) Verify PR is merged (via gh API; no merge-base fallback)
|
|
9
9
|
* 2) Remove worktree (if exists)
|
|
10
10
|
* 3) Delete lane branch (local + remote)
|
|
11
11
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* pnpm wu:cleanup --artifacts
|
|
17
17
|
*/
|
|
18
18
|
import { execSync } from 'node:child_process';
|
|
19
|
-
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
19
|
+
import { createGitForPath, getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
20
20
|
import { existsSync } from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
@@ -25,16 +25,11 @@ import { cleanupWorktreeBuildArtifacts } from '@lumenflow/core/dist/rebase-artif
|
|
|
25
25
|
import { detectCurrentWorktree } from '@lumenflow/core/dist/wu-done-validators.js';
|
|
26
26
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
27
27
|
import { readWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
28
|
-
import {
|
|
28
|
+
import { isGhCliAvailable } from '@lumenflow/core/dist/wu-done-pr.js';
|
|
29
|
+
import { BOX, CLEANUP_GUARD, EXIT_CODES, FILE_SYSTEM, LOG_PREFIX, REMOTES, STRING_LITERALS, WU_STATUS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
29
30
|
// WU-2278: Import ownership validation for cross-agent protection
|
|
30
31
|
import { validateWorktreeOwnership } from '@lumenflow/core/dist/worktree-ownership.js';
|
|
31
32
|
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
32
|
-
// Box drawing characters for consistent output
|
|
33
|
-
const BOX = {
|
|
34
|
-
TOP: '╔═══════════════════════════════════════════════════════════════════╗',
|
|
35
|
-
MID: '╠═══════════════════════════════════════════════════════════════════╣',
|
|
36
|
-
BOT: '╚═══════════════════════════════════════════════════════════════════╝',
|
|
37
|
-
};
|
|
38
33
|
const CLEANUP_OPTIONS = {
|
|
39
34
|
artifacts: {
|
|
40
35
|
name: 'artifacts',
|
|
@@ -42,15 +37,36 @@ const CLEANUP_OPTIONS = {
|
|
|
42
37
|
description: 'Remove build artifacts (dist, tsbuildinfo) in current worktree',
|
|
43
38
|
},
|
|
44
39
|
};
|
|
40
|
+
export const CLEANUP_GUARD_REASONS = CLEANUP_GUARD.REASONS;
|
|
41
|
+
export function evaluateCleanupGuards({ hasUncommittedChanges, hasUnpushedCommits, hasStamp, yamlStatus, ghAvailable, prMerged, }) {
|
|
42
|
+
if (hasUncommittedChanges) {
|
|
43
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.UNCOMMITTED_CHANGES };
|
|
44
|
+
}
|
|
45
|
+
if (hasUnpushedCommits) {
|
|
46
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.UNPUSHED_COMMITS };
|
|
47
|
+
}
|
|
48
|
+
if (yamlStatus !== WU_STATUS.DONE) {
|
|
49
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.STATUS_NOT_DONE };
|
|
50
|
+
}
|
|
51
|
+
if (!hasStamp) {
|
|
52
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.MISSING_STAMP };
|
|
53
|
+
}
|
|
54
|
+
if (ghAvailable && prMerged !== true) {
|
|
55
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.PR_NOT_MERGED };
|
|
56
|
+
}
|
|
57
|
+
return { allowed: true, reason: null };
|
|
58
|
+
}
|
|
45
59
|
// Help text is now auto-generated by commander via createWUParser
|
|
46
60
|
async function verifyPRMerged(laneBranch) {
|
|
47
|
-
|
|
61
|
+
if (!isGhCliAvailable()) {
|
|
62
|
+
return { merged: null, method: 'gh_unavailable' };
|
|
63
|
+
}
|
|
48
64
|
let ghResult;
|
|
49
65
|
try {
|
|
50
66
|
ghResult = execSync(`gh api repos/:owner/:repo/pulls -q '.[] | select(.head.ref == "${laneBranch}") | .merged'`, { encoding: FILE_SYSTEM.UTF8 }).trim();
|
|
51
67
|
}
|
|
52
68
|
catch {
|
|
53
|
-
ghResult =
|
|
69
|
+
ghResult = STRING_LITERALS.EMPTY;
|
|
54
70
|
}
|
|
55
71
|
if (ghResult === 'true') {
|
|
56
72
|
return { merged: true, method: 'gh_api' };
|
|
@@ -58,37 +74,7 @@ async function verifyPRMerged(laneBranch) {
|
|
|
58
74
|
if (ghResult === 'false') {
|
|
59
75
|
return { merged: false, method: 'gh_api' };
|
|
60
76
|
}
|
|
61
|
-
|
|
62
|
-
// Always fetch origin/main first for accurate merge-base check
|
|
63
|
-
await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
64
|
-
const localBranchExists = await getGitForCwd().branchExists(laneBranch);
|
|
65
|
-
if (!localBranchExists) {
|
|
66
|
-
// Branch doesn't exist locally - check if it exists remotely
|
|
67
|
-
const remoteBranchExists = await getGitForCwd().raw([
|
|
68
|
-
'ls-remote',
|
|
69
|
-
'--heads',
|
|
70
|
-
REMOTES.ORIGIN,
|
|
71
|
-
laneBranch,
|
|
72
|
-
]);
|
|
73
|
-
if (!remoteBranchExists) {
|
|
74
|
-
// Branch is gone both locally and remotely - assume merged
|
|
75
|
-
return { merged: true, method: 'branch_deleted' };
|
|
76
|
-
}
|
|
77
|
-
// Branch exists remotely but not locally - need to fetch
|
|
78
|
-
await getGitForCwd().fetch(REMOTES.ORIGIN, laneBranch);
|
|
79
|
-
}
|
|
80
|
-
let isAncestor;
|
|
81
|
-
try {
|
|
82
|
-
await getGitForCwd().raw(['merge-base', '--is-ancestor', laneBranch, GIT_REFS.ORIGIN_MAIN]);
|
|
83
|
-
isAncestor = true;
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
isAncestor = false;
|
|
87
|
-
}
|
|
88
|
-
if (isAncestor) {
|
|
89
|
-
return { merged: true, method: 'git_merge_base' };
|
|
90
|
-
}
|
|
91
|
-
return { merged: false, method: 'git_merge_base' };
|
|
77
|
+
return { merged: null, method: 'gh_api' };
|
|
92
78
|
}
|
|
93
79
|
async function removeWorktree(worktreePath) {
|
|
94
80
|
if (!existsSync(worktreePath)) {
|
|
@@ -154,6 +140,31 @@ async function cleanupArtifactsInWorktree() {
|
|
|
154
140
|
}
|
|
155
141
|
console.log(`${LOG_PREFIX.CLEANUP} ✓ Build artifact cleanup complete`);
|
|
156
142
|
}
|
|
143
|
+
async function hasUncommittedChanges(worktreePath) {
|
|
144
|
+
if (!existsSync(worktreePath)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const git = createGitForPath(worktreePath);
|
|
148
|
+
const status = await git.getStatus();
|
|
149
|
+
return status.length > 0;
|
|
150
|
+
}
|
|
151
|
+
async function hasUnpushedCommits(worktreePath) {
|
|
152
|
+
if (!existsSync(worktreePath)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const git = createGitForPath(worktreePath);
|
|
156
|
+
try {
|
|
157
|
+
const unpushed = await git.getUnpushedCommits();
|
|
158
|
+
return unpushed.length > 0;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function hasStampFile(wuId) {
|
|
165
|
+
const stampPath = path.join(process.cwd(), WU_PATHS.STAMP(wuId));
|
|
166
|
+
return existsSync(stampPath);
|
|
167
|
+
}
|
|
157
168
|
async function main() {
|
|
158
169
|
const args = createWUParser({
|
|
159
170
|
name: 'wu-cleanup',
|
|
@@ -181,6 +192,7 @@ async function main() {
|
|
|
181
192
|
const idK = args.id.toLowerCase();
|
|
182
193
|
const laneBranch = `lane/${laneK}/${idK}`;
|
|
183
194
|
const worktreePath = path.join('worktrees', `${laneK}-${idK}`);
|
|
195
|
+
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
184
196
|
console.log(`[wu-cleanup] Cleaning up ${args.id} (${wu.title})`);
|
|
185
197
|
console.log(`[wu-cleanup] Lane: ${wu.lane}`);
|
|
186
198
|
console.log(`[wu-cleanup] Branch: ${laneBranch}`);
|
|
@@ -200,25 +212,38 @@ async function main() {
|
|
|
200
212
|
console.error(BOX.BOT);
|
|
201
213
|
process.exit(EXIT_CODES.ERROR);
|
|
202
214
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
const cleanupCheck = {
|
|
216
|
+
hasUncommittedChanges: await hasUncommittedChanges(absoluteWorktreePath),
|
|
217
|
+
hasUnpushedCommits: await hasUnpushedCommits(absoluteWorktreePath),
|
|
218
|
+
hasStamp: hasStampFile(id),
|
|
219
|
+
yamlStatus: wu.status,
|
|
220
|
+
ghAvailable: isGhCliAvailable(),
|
|
221
|
+
prMerged: null,
|
|
222
|
+
};
|
|
223
|
+
if (cleanupCheck.ghAvailable) {
|
|
224
|
+
console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.START}`);
|
|
225
|
+
const { merged, method } = await verifyPRMerged(laneBranch);
|
|
226
|
+
cleanupCheck.prMerged = merged;
|
|
227
|
+
console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.RESULT} ${method}`);
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
230
|
+
const guardResult = evaluateCleanupGuards(cleanupCheck);
|
|
231
|
+
if (!guardResult.allowed) {
|
|
207
232
|
console.error();
|
|
208
233
|
console.error(BOX.TOP);
|
|
209
|
-
console.error(
|
|
234
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.BLOCKED}`);
|
|
210
235
|
console.error(BOX.MID);
|
|
211
|
-
console.error(
|
|
212
|
-
console.error(
|
|
213
|
-
console.error(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.MESSAGES[guardResult.reason]}`);
|
|
237
|
+
console.error(`${BOX.SIDE}`);
|
|
238
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.NEXT_STEPS}`);
|
|
239
|
+
const steps = CLEANUP_GUARD.NEXT_STEPS[guardResult.reason] || CLEANUP_GUARD.NEXT_STEPS.DEFAULT;
|
|
240
|
+
for (const step of steps) {
|
|
241
|
+
const line = step.appendId ? `${step.text} ${args.id}` : step.text;
|
|
242
|
+
console.error(`${BOX.SIDE} ${line}`);
|
|
243
|
+
}
|
|
217
244
|
console.error(BOX.BOT);
|
|
218
245
|
process.exit(EXIT_CODES.ERROR);
|
|
219
246
|
}
|
|
220
|
-
console.log(`[wu-cleanup] ✓ PR merged (verified via ${method})`);
|
|
221
|
-
console.log();
|
|
222
247
|
// 2. Remove worktree
|
|
223
248
|
await removeWorktree(worktreePath);
|
|
224
249
|
console.log();
|
|
@@ -235,6 +260,7 @@ async function main() {
|
|
|
235
260
|
}
|
|
236
261
|
// Guard main() for testability (WU-1366)
|
|
237
262
|
import { fileURLToPath } from 'node:url';
|
|
263
|
+
import { runCLI } from './cli-entry-point.js';
|
|
238
264
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
239
|
-
main
|
|
265
|
+
runCLI(main);
|
|
240
266
|
}
|
package/dist/wu-create.js
CHANGED
|
@@ -38,10 +38,12 @@ import { validateLaneFormat, extractParent } from '@lumenflow/core/dist/lane-che
|
|
|
38
38
|
// WU-2330: Import lane inference for sub-lane suggestions
|
|
39
39
|
import { inferSubLane } from '@lumenflow/core/dist/lane-inference.js';
|
|
40
40
|
import { parseBacklogFrontmatter } from '@lumenflow/core/dist/backlog-parser.js';
|
|
41
|
-
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
41
|
+
import { createWUParser, WU_CREATE_OPTIONS, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
42
42
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
43
43
|
import { validateWU } from '@lumenflow/core/dist/wu-schema.js';
|
|
44
|
-
import {
|
|
44
|
+
import { getPlanPath, getPlanProtocolRef, getPlansDir, } from '@lumenflow/core/dist/lumenflow-home.js';
|
|
45
|
+
import { validateSpecRefs } from '@lumenflow/core/dist/wu-create-validators.js';
|
|
46
|
+
import { COMMIT_FORMATS, FILE_SYSTEM, READINESS_UI, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
45
47
|
// WU-1593: Use centralized validateWUIDFormat (DRY)
|
|
46
48
|
import { ensureOnMain, validateWUIDFormat } from '@lumenflow/core/dist/wu-helpers.js';
|
|
47
49
|
// WU-1439: Use shared micro-worktree helper
|
|
@@ -184,6 +186,36 @@ const parseCommaSeparated = (value) => value
|
|
|
184
186
|
.map((s) => s.trim())
|
|
185
187
|
.filter(Boolean)
|
|
186
188
|
: [];
|
|
189
|
+
function mergeSpecRefs(specRefs, extraRef) {
|
|
190
|
+
const refs = parseCommaSeparated(specRefs);
|
|
191
|
+
if (extraRef && !refs.includes(extraRef)) {
|
|
192
|
+
refs.push(extraRef);
|
|
193
|
+
}
|
|
194
|
+
return refs.length > 0 ? refs.join(',') : undefined;
|
|
195
|
+
}
|
|
196
|
+
function createPlanTemplate(wuId, title) {
|
|
197
|
+
const plansDir = getPlansDir();
|
|
198
|
+
mkdirSync(plansDir, { recursive: true });
|
|
199
|
+
const planPath = getPlanPath(wuId);
|
|
200
|
+
if (existsSync(planPath)) {
|
|
201
|
+
die(`Plan already exists: ${planPath}\n\n` +
|
|
202
|
+
`Options:\n` +
|
|
203
|
+
` 1. Open the existing plan and continue editing\n` +
|
|
204
|
+
` 2. Delete or rename the existing plan before retrying\n` +
|
|
205
|
+
` 3. Run wu:create without --plan`);
|
|
206
|
+
}
|
|
207
|
+
const today = todayISO();
|
|
208
|
+
const content = `# ${wuId} Plan — ${title}\n\n` +
|
|
209
|
+
`Created: ${today}\n\n` +
|
|
210
|
+
`## Goal\n\n` +
|
|
211
|
+
`## Scope\n\n` +
|
|
212
|
+
`## Approach\n\n` +
|
|
213
|
+
`## Risks\n\n` +
|
|
214
|
+
`## Open Questions\n`;
|
|
215
|
+
writeFileSync(planPath, content, { encoding: FILE_SYSTEM.UTF8 });
|
|
216
|
+
console.log(`${LOG_PREFIX} ✅ Created plan template: ${planPath}`);
|
|
217
|
+
return planPath;
|
|
218
|
+
}
|
|
187
219
|
function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
|
|
188
220
|
const { description, acceptance, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
|
|
189
221
|
const code_paths = parseCommaSeparated(codePaths);
|
|
@@ -453,6 +485,8 @@ async function main() {
|
|
|
453
485
|
WU_OPTIONS.exposure,
|
|
454
486
|
WU_OPTIONS.userJourney,
|
|
455
487
|
WU_OPTIONS.uiPairingWus,
|
|
488
|
+
// WU-1062: External plan options for wu:create
|
|
489
|
+
WU_CREATE_OPTIONS.plan,
|
|
456
490
|
],
|
|
457
491
|
required: ['id', 'lane', 'title'],
|
|
458
492
|
allowPositionalId: false,
|
|
@@ -477,13 +511,15 @@ async function main() {
|
|
|
477
511
|
}
|
|
478
512
|
// WU-2330: Warn if a more specific sub-lane matches code_paths or description
|
|
479
513
|
warnIfBetterLaneExists(args.lane, args.codePaths, args.title, args.description);
|
|
480
|
-
checkWUExists(args.id);
|
|
481
514
|
await ensureOnMain(getGitForCwd());
|
|
515
|
+
checkWUExists(args.id);
|
|
482
516
|
// WU-1368: Get assigned_to from flag or git config user.email
|
|
483
517
|
const assignedTo = args.assignedTo || (await getDefaultAssignedTo());
|
|
484
518
|
if (!assignedTo) {
|
|
485
519
|
console.warn(`${LOG_PREFIX} ⚠️ No assigned_to set - WU will need manual assignment`);
|
|
486
520
|
}
|
|
521
|
+
const planSpecRef = args.plan ? getPlanProtocolRef(args.id) : undefined;
|
|
522
|
+
const mergedSpecRefs = mergeSpecRefs(args.specRefs, planSpecRef);
|
|
487
523
|
const createSpecValidation = validateCreateSpec({
|
|
488
524
|
id: args.id,
|
|
489
525
|
lane: args.lane,
|
|
@@ -500,7 +536,7 @@ async function main() {
|
|
|
500
536
|
exposure: args.exposure,
|
|
501
537
|
userJourney: args.userJourney,
|
|
502
538
|
uiPairingWus: args.uiPairingWus,
|
|
503
|
-
specRefs:
|
|
539
|
+
specRefs: mergedSpecRefs,
|
|
504
540
|
initiative: args.initiative,
|
|
505
541
|
phase: args.phase,
|
|
506
542
|
blockedBy: args.blockedBy,
|
|
@@ -516,6 +552,22 @@ async function main() {
|
|
|
516
552
|
die(`${LOG_PREFIX} ❌ Spec validation failed:\n\n${errorList}`);
|
|
517
553
|
}
|
|
518
554
|
console.log(`${LOG_PREFIX} ✅ Spec validation passed`);
|
|
555
|
+
const specRefsList = parseCommaSeparated(mergedSpecRefs);
|
|
556
|
+
const specRefsValidation = validateSpecRefs(specRefsList);
|
|
557
|
+
if (!specRefsValidation.valid) {
|
|
558
|
+
const errorList = specRefsValidation.errors
|
|
559
|
+
.map((error) => ` • ${error}`)
|
|
560
|
+
.join(STRING_LITERALS.NEWLINE);
|
|
561
|
+
die(`${LOG_PREFIX} ❌ Spec reference validation failed:\n\n${errorList}`);
|
|
562
|
+
}
|
|
563
|
+
if (specRefsValidation.warnings.length > 0) {
|
|
564
|
+
for (const warning of specRefsValidation.warnings) {
|
|
565
|
+
console.warn(`${LOG_PREFIX} ⚠️ ${warning}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (args.plan) {
|
|
569
|
+
createPlanTemplate(args.id, args.title);
|
|
570
|
+
}
|
|
519
571
|
// Transaction: micro-worktree isolation (WU-1439)
|
|
520
572
|
try {
|
|
521
573
|
const priority = args.priority || DEFAULT_PRIORITY;
|
|
@@ -550,7 +602,7 @@ async function main() {
|
|
|
550
602
|
userJourney: args.userJourney,
|
|
551
603
|
uiPairingWus: args.uiPairingWus,
|
|
552
604
|
// WU-2320: Spec references
|
|
553
|
-
specRefs:
|
|
605
|
+
specRefs: mergedSpecRefs,
|
|
554
606
|
});
|
|
555
607
|
// Update backlog.md in micro-worktree
|
|
556
608
|
const backlogPath = updateBacklogInWorktree(worktreePath, args.id, args.lane, args.title);
|
|
@@ -588,7 +640,10 @@ async function main() {
|
|
|
588
640
|
}
|
|
589
641
|
}
|
|
590
642
|
// Guard main() for testability (WU-1366)
|
|
591
|
-
import
|
|
592
|
-
|
|
593
|
-
|
|
643
|
+
// WU-1071: Use import.meta.main instead of process.argv[1] comparison
|
|
644
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
645
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
646
|
+
import { runCLI } from './cli-entry-point.js';
|
|
647
|
+
if (import.meta.main) {
|
|
648
|
+
runCLI(main);
|
|
594
649
|
}
|
package/dist/wu-delete.js
CHANGED
|
File without changes
|
package/dist/wu-deps.js
CHANGED
|
@@ -114,6 +114,7 @@ function renderGraphJSON(graph, rootId, depth, direction) {
|
|
|
114
114
|
}
|
|
115
115
|
// Guard main() for testability (WU-1366)
|
|
116
116
|
import { fileURLToPath } from 'node:url';
|
|
117
|
+
import { runCLI } from './cli-entry-point.js';
|
|
117
118
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
118
|
-
main
|
|
119
|
+
runCLI(main);
|
|
119
120
|
}
|
package/dist/wu-done.js
CHANGED
|
@@ -956,6 +956,9 @@ async function validateStagedFiles(id, isDocsOnly = false) {
|
|
|
956
956
|
// Whitelist .beacon/stamps/** pattern
|
|
957
957
|
if (file.startsWith('.beacon/stamps/'))
|
|
958
958
|
return false;
|
|
959
|
+
// WU-1072: Whitelist apps/docs/**/*.mdx for auto-generated docs from turbo docs:generate
|
|
960
|
+
if (file.startsWith('apps/docs/') && file.endsWith('.mdx'))
|
|
961
|
+
return false;
|
|
959
962
|
return true;
|
|
960
963
|
});
|
|
961
964
|
if (unexpected.length > 0) {
|
|
@@ -2213,7 +2216,10 @@ async function detectChangedDocPaths(worktreePath, baseBranch) {
|
|
|
2213
2216
|
}
|
|
2214
2217
|
// Guard main() execution for testability (WU-1366)
|
|
2215
2218
|
// When imported as a module for testing, main() should not auto-run
|
|
2216
|
-
import
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
+
// WU-1071: Use import.meta.main instead of process.argv[1] comparison
|
|
2220
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
2221
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
2222
|
+
import { runCLI } from './cli-entry-point.js';
|
|
2223
|
+
if (import.meta.main) {
|
|
2224
|
+
runCLI(main);
|
|
2219
2225
|
}
|
package/dist/wu-edit.js
CHANGED
|
File without changes
|
package/dist/wu-infer-lane.js
CHANGED
|
@@ -93,7 +93,7 @@ function loadWuYaml(id) {
|
|
|
93
93
|
` 2. Fix YAML errors manually and retry`);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
function main() {
|
|
96
|
+
async function main() {
|
|
97
97
|
const args = parseArgs(process.argv);
|
|
98
98
|
let codePaths = [];
|
|
99
99
|
let description = '';
|
|
@@ -130,6 +130,7 @@ function main() {
|
|
|
130
130
|
}
|
|
131
131
|
// Guard main() for testability (WU-1366)
|
|
132
132
|
import { fileURLToPath } from 'node:url';
|
|
133
|
+
import { runCLI } from './cli-entry-point.js';
|
|
133
134
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
134
|
-
main
|
|
135
|
+
runCLI(main);
|
|
135
136
|
}
|
package/dist/wu-preflight.js
CHANGED
|
@@ -160,8 +160,9 @@ async function main() {
|
|
|
160
160
|
process.exit(EXIT_CODES.SUCCESS);
|
|
161
161
|
}
|
|
162
162
|
// Guard main() for testability
|
|
163
|
+
import { runCLI } from './cli-entry-point.js';
|
|
163
164
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
164
|
-
main
|
|
165
|
+
runCLI(main);
|
|
165
166
|
}
|
|
166
167
|
// Export for testing
|
|
167
168
|
export { parseArgs, detectWorktreePath };
|
package/dist/wu-prune.js
CHANGED
|
@@ -19,8 +19,8 @@ import { readWUYaml, validateBranchName, extractWUFromBranch, } from '@lumenflow
|
|
|
19
19
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
20
20
|
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
21
21
|
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
22
|
-
import { detectOrphanWorktrees, removeOrphanDirectory, } from '@lumenflow/core/dist/orphan-detector.js';
|
|
23
|
-
import { BRANCHES, WU_STATUS, CLI_FLAGS, EXIT_CODES, STRING_LITERALS, EMOJI, LOG_PREFIX, } from '@lumenflow/core/dist/wu-constants.js';
|
|
22
|
+
import { detectOrphanWorktrees, detectMissingTrackedWorktrees, removeOrphanDirectory, } from '@lumenflow/core/dist/orphan-detector.js';
|
|
23
|
+
import { BRANCHES, WU_STATUS, CLI_FLAGS, EXIT_CODES, STRING_LITERALS, EMOJI, LOG_PREFIX, WORKTREE_WARNINGS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
24
24
|
function parseArgs(argv) {
|
|
25
25
|
const args = { dryRun: true }; // Default to dry-run for safety
|
|
26
26
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -145,6 +145,14 @@ This tool:
|
|
|
145
145
|
}
|
|
146
146
|
console.log(`${PREFIX} Worktree Hygiene Check`);
|
|
147
147
|
console.log(`${PREFIX} =====================\n`);
|
|
148
|
+
const missingTracked = await detectMissingTrackedWorktrees(process.cwd());
|
|
149
|
+
if (missingTracked.length > 0) {
|
|
150
|
+
console.warn(`${PREFIX} ${EMOJI.WARNING} ${WORKTREE_WARNINGS.MISSING_TRACKED_HEADER}`);
|
|
151
|
+
for (const missingPath of missingTracked) {
|
|
152
|
+
console.warn(`${PREFIX} ${WORKTREE_WARNINGS.MISSING_TRACKED_LINE(missingPath)}`);
|
|
153
|
+
}
|
|
154
|
+
console.warn('');
|
|
155
|
+
}
|
|
148
156
|
if (args.dryRun) {
|
|
149
157
|
console.log(`${PREFIX} ${EMOJI.INFO} DRY-RUN MODE (use --execute to apply changes)\n`);
|
|
150
158
|
}
|
|
@@ -254,6 +262,7 @@ This tool:
|
|
|
254
262
|
}
|
|
255
263
|
// Guard main() for testability (WU-1366)
|
|
256
264
|
import { fileURLToPath } from 'node:url';
|
|
265
|
+
import { runCLI } from './cli-entry-point.js';
|
|
257
266
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
258
|
-
main
|
|
267
|
+
runCLI(main);
|
|
259
268
|
}
|
package/dist/wu-repair.js
CHANGED
|
@@ -219,8 +219,9 @@ async function main() {
|
|
|
219
219
|
}
|
|
220
220
|
// Guard main() for testability (WU-1366)
|
|
221
221
|
import { fileURLToPath } from 'node:url';
|
|
222
|
+
import { runCLI } from './cli-entry-point.js';
|
|
222
223
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
223
|
-
main
|
|
224
|
+
runCLI(main);
|
|
224
225
|
}
|
|
225
226
|
// Export for testing
|
|
226
227
|
export { normaliseWUId, isValidWUId };
|
package/dist/wu-spawn.js
CHANGED
|
@@ -731,7 +731,7 @@ function generateLaneGuidance(lane) {
|
|
|
731
731
|
- Follow prompt versioning guidelines in ai/prompts/README.md`,
|
|
732
732
|
Experience: `## Lane-Specific: Experience
|
|
733
733
|
|
|
734
|
-
- Follow design system tokens in
|
|
734
|
+
- Follow design system tokens defined in the project
|
|
735
735
|
- Ensure accessibility compliance (WCAG 2.1 AA)`,
|
|
736
736
|
Core: `## Lane-Specific: Core
|
|
737
737
|
|
|
@@ -1239,8 +1239,11 @@ async function main() {
|
|
|
1239
1239
|
console.log(`\n${registryMessage}`);
|
|
1240
1240
|
}
|
|
1241
1241
|
}
|
|
1242
|
-
// Guard main() for testability
|
|
1243
|
-
import
|
|
1244
|
-
|
|
1245
|
-
|
|
1242
|
+
// Guard main() for testability (WU-1366)
|
|
1243
|
+
// WU-1071: Use import.meta.main instead of process.argv[1] comparison
|
|
1244
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
1245
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
1246
|
+
import { runCLI } from './cli-entry-point.js';
|
|
1247
|
+
if (import.meta.main) {
|
|
1248
|
+
runCLI(main);
|
|
1246
1249
|
}
|
package/dist/wu-unblock.js
CHANGED
|
File without changes
|
package/dist/wu-unlock-lane.js
CHANGED
|
@@ -155,4 +155,9 @@ async function main() {
|
|
|
155
155
|
console.log(`${PREFIX} Previous owner: ${result.previousLock?.wuId || 'unknown'}`);
|
|
156
156
|
console.log(`${PREFIX} Reason: ${result.reason}`);
|
|
157
157
|
}
|
|
158
|
-
main()
|
|
158
|
+
// Guard main() for testability (WU-1064)
|
|
159
|
+
import { fileURLToPath } from 'node:url';
|
|
160
|
+
import { runCLI } from './cli-entry-point.js';
|
|
161
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
162
|
+
runCLI(main);
|
|
163
|
+
}
|
package/dist/wu-validate.js
CHANGED
|
@@ -188,6 +188,7 @@ async function main() {
|
|
|
188
188
|
}
|
|
189
189
|
// Guard main() for testability
|
|
190
190
|
import { fileURLToPath } from 'node:url';
|
|
191
|
+
import { runCLI } from './cli-entry-point.js';
|
|
191
192
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
192
|
-
main
|
|
193
|
+
runCLI(main);
|
|
193
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumenflow/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Command-line interface for LumenFlow workflow framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lumenflow",
|
|
@@ -75,7 +75,19 @@
|
|
|
75
75
|
"LICENSE",
|
|
76
76
|
"README.md"
|
|
77
77
|
],
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsc",
|
|
80
|
+
"build:dist": "tsc -p tsconfig.build.json",
|
|
81
|
+
"pack:dist": "pnpm pack",
|
|
82
|
+
"clean": "rm -rf dist *.tgz",
|
|
83
|
+
"test": "vitest run"
|
|
84
|
+
},
|
|
78
85
|
"dependencies": {
|
|
86
|
+
"@lumenflow/core": "workspace:*",
|
|
87
|
+
"@lumenflow/metrics": "workspace:*",
|
|
88
|
+
"@lumenflow/memory": "workspace:*",
|
|
89
|
+
"@lumenflow/initiatives": "workspace:*",
|
|
90
|
+
"@lumenflow/agent": "workspace:*",
|
|
79
91
|
"chalk": "^5.6.2",
|
|
80
92
|
"cli-table3": "^0.6.5",
|
|
81
93
|
"commander": "^14.0.2",
|
|
@@ -84,12 +96,7 @@
|
|
|
84
96
|
"ms": "^2.1.3",
|
|
85
97
|
"pretty-ms": "^9.2.0",
|
|
86
98
|
"simple-git": "^3.30.0",
|
|
87
|
-
"yaml": "^2.8.2"
|
|
88
|
-
"@lumenflow/memory": "1.3.0",
|
|
89
|
-
"@lumenflow/agent": "1.3.0",
|
|
90
|
-
"@lumenflow/initiatives": "1.3.0",
|
|
91
|
-
"@lumenflow/metrics": "1.3.0",
|
|
92
|
-
"@lumenflow/core": "1.3.0"
|
|
99
|
+
"yaml": "^2.8.2"
|
|
93
100
|
},
|
|
94
101
|
"devDependencies": {
|
|
95
102
|
"@vitest/coverage-v8": "^4.0.17",
|
|
@@ -101,12 +108,5 @@
|
|
|
101
108
|
},
|
|
102
109
|
"publishConfig": {
|
|
103
110
|
"access": "public"
|
|
104
|
-
},
|
|
105
|
-
"scripts": {
|
|
106
|
-
"build": "tsc",
|
|
107
|
-
"build:dist": "tsc -p tsconfig.build.json",
|
|
108
|
-
"pack:dist": "pnpm pack",
|
|
109
|
-
"clean": "rm -rf dist *.tgz",
|
|
110
|
-
"test": "vitest run"
|
|
111
111
|
}
|
|
112
|
-
}
|
|
112
|
+
}
|