@lumenflow/cli 1.3.0 → 1.3.2

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 (114) hide show
  1. package/dist/__tests__/cli-entry-point.test.js +50 -0
  2. package/dist/__tests__/cli-subprocess.test.js +64 -0
  3. package/dist/cli-entry-point.js +46 -0
  4. package/dist/initiative-add-wu.js +2 -1
  5. package/dist/initiative-create.js +2 -1
  6. package/dist/initiative-list.js +2 -1
  7. package/dist/initiative-status.js +2 -1
  8. package/dist/wu-claim.js +2 -1
  9. package/dist/wu-cleanup.js +82 -56
  10. package/dist/wu-create.js +2 -1
  11. package/dist/wu-deps.js +2 -1
  12. package/dist/wu-done.js +2 -1
  13. package/dist/wu-infer-lane.js +3 -2
  14. package/dist/wu-preflight.js +2 -1
  15. package/dist/wu-prune.js +12 -3
  16. package/dist/wu-repair.js +2 -1
  17. package/dist/wu-spawn.js +2 -1
  18. package/dist/wu-unlock-lane.js +6 -1
  19. package/dist/wu-validate.js +2 -1
  20. package/package.json +6 -6
  21. package/dist/gates.d.ts +0 -41
  22. package/dist/gates.d.ts.map +0 -1
  23. package/dist/gates.js.map +0 -1
  24. package/dist/initiative-add-wu.d.ts +0 -22
  25. package/dist/initiative-add-wu.d.ts.map +0 -1
  26. package/dist/initiative-add-wu.js.map +0 -1
  27. package/dist/initiative-create.d.ts +0 -28
  28. package/dist/initiative-create.d.ts.map +0 -1
  29. package/dist/initiative-create.js.map +0 -1
  30. package/dist/initiative-edit.d.ts +0 -34
  31. package/dist/initiative-edit.d.ts.map +0 -1
  32. package/dist/initiative-edit.js.map +0 -1
  33. package/dist/initiative-list.d.ts +0 -12
  34. package/dist/initiative-list.d.ts.map +0 -1
  35. package/dist/initiative-list.js.map +0 -1
  36. package/dist/initiative-status.d.ts +0 -11
  37. package/dist/initiative-status.d.ts.map +0 -1
  38. package/dist/initiative-status.js.map +0 -1
  39. package/dist/mem-checkpoint.d.ts +0 -16
  40. package/dist/mem-checkpoint.d.ts.map +0 -1
  41. package/dist/mem-checkpoint.js.map +0 -1
  42. package/dist/mem-cleanup.d.ts +0 -29
  43. package/dist/mem-cleanup.d.ts.map +0 -1
  44. package/dist/mem-cleanup.js.map +0 -1
  45. package/dist/mem-create.d.ts +0 -17
  46. package/dist/mem-create.d.ts.map +0 -1
  47. package/dist/mem-create.js.map +0 -1
  48. package/dist/mem-inbox.d.ts +0 -35
  49. package/dist/mem-inbox.d.ts.map +0 -1
  50. package/dist/mem-inbox.js.map +0 -1
  51. package/dist/mem-init.d.ts +0 -15
  52. package/dist/mem-init.d.ts.map +0 -1
  53. package/dist/mem-init.js.map +0 -1
  54. package/dist/mem-ready.d.ts +0 -16
  55. package/dist/mem-ready.d.ts.map +0 -1
  56. package/dist/mem-ready.js.map +0 -1
  57. package/dist/mem-signal.d.ts +0 -16
  58. package/dist/mem-signal.d.ts.map +0 -1
  59. package/dist/mem-signal.js.map +0 -1
  60. package/dist/mem-start.d.ts +0 -16
  61. package/dist/mem-start.d.ts.map +0 -1
  62. package/dist/mem-start.js.map +0 -1
  63. package/dist/mem-summarize.d.ts +0 -22
  64. package/dist/mem-summarize.d.ts.map +0 -1
  65. package/dist/mem-summarize.js.map +0 -1
  66. package/dist/mem-triage.d.ts +0 -22
  67. package/dist/mem-triage.d.ts.map +0 -1
  68. package/dist/mem-triage.js.map +0 -1
  69. package/dist/spawn-list.d.ts +0 -16
  70. package/dist/spawn-list.d.ts.map +0 -1
  71. package/dist/spawn-list.js.map +0 -1
  72. package/dist/wu-block.d.ts +0 -16
  73. package/dist/wu-block.d.ts.map +0 -1
  74. package/dist/wu-block.js.map +0 -1
  75. package/dist/wu-claim.d.ts +0 -32
  76. package/dist/wu-claim.d.ts.map +0 -1
  77. package/dist/wu-claim.js.map +0 -1
  78. package/dist/wu-cleanup.d.ts +0 -17
  79. package/dist/wu-cleanup.d.ts.map +0 -1
  80. package/dist/wu-cleanup.js.map +0 -1
  81. package/dist/wu-create.d.ts +0 -38
  82. package/dist/wu-create.d.ts.map +0 -1
  83. package/dist/wu-create.js.map +0 -1
  84. package/dist/wu-deps.d.ts +0 -13
  85. package/dist/wu-deps.d.ts.map +0 -1
  86. package/dist/wu-deps.js.map +0 -1
  87. package/dist/wu-done.d.ts +0 -153
  88. package/dist/wu-done.d.ts.map +0 -1
  89. package/dist/wu-done.js.map +0 -1
  90. package/dist/wu-edit.d.ts +0 -29
  91. package/dist/wu-edit.d.ts.map +0 -1
  92. package/dist/wu-edit.js.map +0 -1
  93. package/dist/wu-infer-lane.d.ts +0 -17
  94. package/dist/wu-infer-lane.d.ts.map +0 -1
  95. package/dist/wu-infer-lane.js.map +0 -1
  96. package/dist/wu-preflight.d.ts +0 -47
  97. package/dist/wu-preflight.d.ts.map +0 -1
  98. package/dist/wu-preflight.js.map +0 -1
  99. package/dist/wu-prune.d.ts +0 -16
  100. package/dist/wu-prune.d.ts.map +0 -1
  101. package/dist/wu-prune.js.map +0 -1
  102. package/dist/wu-repair.d.ts +0 -60
  103. package/dist/wu-repair.d.ts.map +0 -1
  104. package/dist/wu-repair.js.map +0 -1
  105. package/dist/wu-spawn-completion.d.ts +0 -10
  106. package/dist/wu-spawn.d.ts +0 -168
  107. package/dist/wu-spawn.d.ts.map +0 -1
  108. package/dist/wu-spawn.js.map +0 -1
  109. package/dist/wu-unblock.d.ts +0 -16
  110. package/dist/wu-unblock.d.ts.map +0 -1
  111. package/dist/wu-unblock.js.map +0 -1
  112. package/dist/wu-validate.d.ts +0 -16
  113. package/dist/wu-validate.d.ts.map +0 -1
  114. package/dist/wu-validate.js.map +0 -1
@@ -0,0 +1,50 @@
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
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import { runCLI } from '../cli-entry-point.js';
11
+ import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
12
+ describe('runCLI', () => {
13
+ let mockExit;
14
+ let mockConsoleError;
15
+ beforeEach(() => {
16
+ mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
17
+ mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
18
+ });
19
+ afterEach(() => {
20
+ mockExit.mockRestore();
21
+ mockConsoleError.mockRestore();
22
+ });
23
+ it('should call main() and do nothing on success', async () => {
24
+ const main = vi.fn().mockResolvedValue(undefined);
25
+ await runCLI(main);
26
+ expect(main).toHaveBeenCalledOnce();
27
+ expect(mockExit).not.toHaveBeenCalled();
28
+ expect(mockConsoleError).not.toHaveBeenCalled();
29
+ });
30
+ it('should catch errors and exit with ERROR code', async () => {
31
+ const error = new Error('Test error message');
32
+ const main = vi.fn().mockRejectedValue(error);
33
+ await runCLI(main);
34
+ expect(main).toHaveBeenCalledOnce();
35
+ expect(mockConsoleError).toHaveBeenCalledWith('Test error message');
36
+ expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
37
+ });
38
+ it('should handle errors without message property', async () => {
39
+ const main = vi.fn().mockRejectedValue('string error');
40
+ await runCLI(main);
41
+ expect(mockConsoleError).toHaveBeenCalledWith('string error');
42
+ expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
43
+ });
44
+ it('should handle null/undefined errors', async () => {
45
+ const main = vi.fn().mockRejectedValue(null);
46
+ await runCLI(main);
47
+ expect(mockConsoleError).toHaveBeenCalledWith('Unknown error');
48
+ expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
49
+ });
50
+ });
@@ -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,46 @@
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
+ * @example
8
+ * ```typescript
9
+ * // At the bottom of each CLI file:
10
+ * import { runCLI } from './cli-entry-point.js';
11
+ * import { fileURLToPath } from 'node:url';
12
+ *
13
+ * if (process.argv[1] === fileURLToPath(import.meta.url)) {
14
+ * runCLI(main);
15
+ * }
16
+ * ```
17
+ */
18
+ import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
19
+ /**
20
+ * Wraps an async main function with proper error handling.
21
+ *
22
+ * @param main - The async main function to execute
23
+ * @returns Promise that resolves when main completes (or after error handling)
24
+ */
25
+ export async function runCLI(main) {
26
+ try {
27
+ await main();
28
+ }
29
+ catch (err) {
30
+ const message = getErrorMessage(err);
31
+ console.error(message);
32
+ process.exit(EXIT_CODES.ERROR);
33
+ }
34
+ }
35
+ /**
36
+ * Extracts error message from unknown error type.
37
+ */
38
+ function getErrorMessage(err) {
39
+ if (err === null || err === undefined) {
40
+ return 'Unknown error';
41
+ }
42
+ if (err instanceof Error) {
43
+ return err.message;
44
+ }
45
+ return String(err);
46
+ }
@@ -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
  }
@@ -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
  }
@@ -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/wu-claim.js CHANGED
@@ -1298,6 +1298,7 @@ async function main() {
1298
1298
  }
1299
1299
  // Guard main() for testability (WU-1366)
1300
1300
  import { fileURLToPath } from 'node:url';
1301
+ import { runCLI } from './cli-entry-point.js';
1301
1302
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
1302
- main();
1303
+ runCLI(main);
1303
1304
  }
@@ -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
@@ -589,6 +589,7 @@ async function main() {
589
589
  }
590
590
  // Guard main() for testability (WU-1366)
591
591
  import { fileURLToPath } from 'node:url';
592
+ import { runCLI } from './cli-entry-point.js';
592
593
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
593
- main();
594
+ runCLI(main);
594
595
  }
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
@@ -2214,6 +2214,7 @@ async function detectChangedDocPaths(worktreePath, baseBranch) {
2214
2214
  // Guard main() execution for testability (WU-1366)
2215
2215
  // When imported as a module for testing, main() should not auto-run
2216
2216
  import { fileURLToPath } from 'node:url';
2217
+ import { runCLI } from './cli-entry-point.js';
2217
2218
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
2218
- main();
2219
+ runCLI(main);
2219
2220
  }
@@ -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
@@ -1241,6 +1241,7 @@ async function main() {
1241
1241
  }
1242
1242
  // Guard main() for testability
1243
1243
  import { fileURLToPath } from 'node:url';
1244
+ import { runCLI } from './cli-entry-point.js';
1244
1245
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
1245
- main();
1246
+ runCLI(main);
1246
1247
  }
@@ -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.2",
4
4
  "description": "Command-line interface for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -85,11 +85,11 @@
85
85
  "pretty-ms": "^9.2.0",
86
86
  "simple-git": "^3.30.0",
87
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"
88
+ "@lumenflow/core": "1.3.2",
89
+ "@lumenflow/metrics": "1.3.2",
90
+ "@lumenflow/memory": "1.3.2",
91
+ "@lumenflow/initiatives": "1.3.2",
92
+ "@lumenflow/agent": "1.3.2"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@vitest/coverage-v8": "^4.0.17",
package/dist/gates.d.ts DELETED
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Quality Gates Runner
4
- *
5
- * Runs quality gates with support for docs-only mode and incremental linting.
6
- *
7
- * WU-1304: Optimise ESLint gates performance
8
- * - Uses incremental linting (only files changed since branching from main)
9
- * - Full lint coverage maintained via CI workflow
10
- *
11
- * WU-1433: Coverage gate with mode flag
12
- * - Checks coverage thresholds for hex core files (≥90% for application layer)
13
- * - Mode: block (default) fails the gate, warn logs warnings only
14
- * WU-2334: Changed default from warn to block for TDD enforcement
15
- *
16
- * WU-1610: Supabase docs linter
17
- * - Verifies every table in migrations is documented in schema.md
18
- * - Fails if any table is missing documentation
19
- *
20
- * For type:documentation WUs:
21
- * - ✅ Run: format:check, spec:linter, prompts:lint, backlog-sync
22
- * - ❌ Skip: lint, typecheck, supabase-docs:linter, tests, coverage (no code changed)
23
- *
24
- * WU-1920: Incremental test execution
25
- * - Uses Vitest's --changed flag to run only tests for changed files
26
- * - Full test suite maintained via CI workflow and --full-tests flag
27
- *
28
- * WU-2062: Tiered test execution for faster wu:done
29
- * - Safety-critical tests (PHI, escalation, red-flag) ALWAYS run
30
- * - Docs-only WUs: lint/typecheck only (auto-detected or --docs-only flag)
31
- * - High-risk WUs (auth, PHI, RLS, migrations): run integration tests
32
- * - Standard WUs: changed tests + safety-critical tests
33
- *
34
- * Usage:
35
- * node tools/gates.mjs # Tiered gates (default)
36
- * node tools/gates.mjs --docs-only # Docs-only gates
37
- * node tools/gates.mjs --full-lint # Full lint (bypass incremental)
38
- * node tools/gates.mjs --full-tests # Full tests (bypass incremental)
39
- * node tools/gates.mjs --coverage-mode=block # Coverage gate in block mode
40
- */
41
- export {};
@@ -1 +0,0 @@
1
- {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG"}