@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.
Files changed (50) hide show
  1. package/dist/__tests__/cli-entry-point.test.js +111 -0
  2. package/dist/__tests__/cli-subprocess.test.js +64 -0
  3. package/dist/__tests__/wu-done-staging-whitelist.test.js +35 -0
  4. package/dist/agent-issues-query.js +0 -0
  5. package/dist/agent-log-issue.js +0 -0
  6. package/dist/agent-session-end.js +0 -0
  7. package/dist/agent-session.js +0 -0
  8. package/dist/cli-entry-point.js +51 -0
  9. package/dist/flow-bottlenecks.js +0 -0
  10. package/dist/flow-report.js +0 -0
  11. package/dist/gates.js +4 -2
  12. package/dist/init.js +39 -3
  13. package/dist/initiative-add-wu.js +2 -1
  14. package/dist/initiative-bulk-assign-wus.js +0 -0
  15. package/dist/initiative-create.js +2 -1
  16. package/dist/initiative-edit.js +0 -0
  17. package/dist/initiative-list.js +2 -1
  18. package/dist/initiative-status.js +2 -1
  19. package/dist/mem-checkpoint.js +0 -0
  20. package/dist/mem-cleanup.js +0 -0
  21. package/dist/mem-create.js +0 -0
  22. package/dist/mem-inbox.js +0 -0
  23. package/dist/mem-init.js +0 -0
  24. package/dist/mem-ready.js +0 -0
  25. package/dist/mem-signal.js +0 -0
  26. package/dist/mem-start.js +0 -0
  27. package/dist/mem-summarize.js +0 -0
  28. package/dist/mem-triage.js +0 -0
  29. package/dist/metrics-snapshot.js +0 -0
  30. package/dist/orchestrate-init-status.js +0 -0
  31. package/dist/orchestrate-initiative.js +0 -0
  32. package/dist/orchestrate-monitor.js +0 -0
  33. package/dist/spawn-list.js +0 -0
  34. package/dist/wu-block.js +0 -0
  35. package/dist/wu-claim.js +6 -3
  36. package/dist/wu-cleanup.js +82 -56
  37. package/dist/wu-create.js +63 -8
  38. package/dist/wu-delete.js +0 -0
  39. package/dist/wu-deps.js +2 -1
  40. package/dist/wu-done.js +9 -3
  41. package/dist/wu-edit.js +0 -0
  42. package/dist/wu-infer-lane.js +3 -2
  43. package/dist/wu-preflight.js +2 -1
  44. package/dist/wu-prune.js +12 -3
  45. package/dist/wu-repair.js +2 -1
  46. package/dist/wu-spawn.js +8 -5
  47. package/dist/wu-unblock.js +0 -0
  48. package/dist/wu-unlock-lane.js +6 -1
  49. package/dist/wu-validate.js +2 -1
  50. 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
File without changes
File without changes
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
+ }
File without changes
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
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
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
  }
File without changes
@@ -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
  }
File without changes
File without changes
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
File without changes
package/dist/mem-start.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
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 { fileURLToPath } from 'node:url';
1301
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
1302
- main();
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
  }
@@ -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 or git merge-base fallback)
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 { BRANCHES, EXIT_CODES, FILE_SYSTEM, LOG_PREFIX, REMOTES, GIT_REFS, } from '@lumenflow/core/dist/wu-constants.js';
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
- // Try gh API first (most reliable)
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
- // Fallback: git merge-base --is-ancestor
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
- // 1. Verify PR is merged
204
- console.log('[wu-cleanup] Verifying PR merge status...');
205
- const { merged, method } = await verifyPRMerged(laneBranch);
206
- if (!merged) {
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('║ PR NOT MERGED');
234
+ console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.BLOCKED}`);
210
235
  console.error(BOX.MID);
211
- console.error(`║ Branch ${laneBranch} has not been merged to main yet.`);
212
- console.error('║');
213
- console.error('║ Next steps:');
214
- console.error('║ 1. Review the PR in GitHub UI');
215
- console.error('║ 2. Merge the PR');
216
- console.error(`║ 3. Re-run: pnpm wu:cleanup --id ${args.id}`);
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 { COMMIT_FORMATS, FILE_SYSTEM, STRING_LITERALS, READINESS_UI, } from '@lumenflow/core/dist/wu-constants.js';
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: args.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: args.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 { fileURLToPath } from 'node:url';
592
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
593
- main();
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 { fileURLToPath } from 'node:url';
2217
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
2218
- main();
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
@@ -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
  }
@@ -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 packages/@patientpath/design-system
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 { fileURLToPath } from 'node:url';
1244
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
1245
- main();
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
  }
File without changes
@@ -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
+ }
@@ -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.0",
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
+ }