@lumenflow/cli 2.2.2 → 2.3.1

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 (118) hide show
  1. package/README.md +147 -57
  2. package/dist/__tests__/agent-log-issue.test.js +56 -0
  3. package/dist/__tests__/cli-entry-point.test.js +66 -17
  4. package/dist/__tests__/cli-subprocess.test.js +25 -0
  5. package/dist/__tests__/init.test.js +298 -0
  6. package/dist/__tests__/initiative-plan.test.js +340 -0
  7. package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
  8. package/dist/__tests__/merge-block.test.js +220 -0
  9. package/dist/__tests__/safe-git.test.js +191 -0
  10. package/dist/__tests__/state-doctor.test.js +274 -0
  11. package/dist/__tests__/wu-done.test.js +36 -0
  12. package/dist/__tests__/wu-edit.test.js +119 -0
  13. package/dist/__tests__/wu-prep.test.js +108 -0
  14. package/dist/agent-issues-query.js +4 -3
  15. package/dist/agent-log-issue.js +25 -4
  16. package/dist/backlog-prune.js +5 -4
  17. package/dist/cli-entry-point.js +11 -1
  18. package/dist/doctor.js +368 -0
  19. package/dist/flow-bottlenecks.js +6 -5
  20. package/dist/flow-report.js +4 -3
  21. package/dist/gates.js +356 -101
  22. package/dist/guard-locked.js +4 -3
  23. package/dist/guard-worktree-commit.js +4 -3
  24. package/dist/init.js +508 -86
  25. package/dist/initiative-add-wu.js +4 -3
  26. package/dist/initiative-bulk-assign-wus.js +8 -5
  27. package/dist/initiative-create.js +73 -37
  28. package/dist/initiative-edit.js +37 -21
  29. package/dist/initiative-list.js +4 -3
  30. package/dist/initiative-plan.js +337 -0
  31. package/dist/initiative-status.js +4 -3
  32. package/dist/lane-health.js +377 -0
  33. package/dist/lane-suggest.js +382 -0
  34. package/dist/mem-checkpoint.js +2 -2
  35. package/dist/mem-cleanup.js +2 -2
  36. package/dist/mem-context.js +306 -0
  37. package/dist/mem-create.js +2 -2
  38. package/dist/mem-delete.js +293 -0
  39. package/dist/mem-inbox.js +2 -2
  40. package/dist/mem-index.js +211 -0
  41. package/dist/mem-init.js +1 -1
  42. package/dist/mem-profile.js +207 -0
  43. package/dist/mem-promote.js +254 -0
  44. package/dist/mem-ready.js +2 -2
  45. package/dist/mem-signal.js +2 -2
  46. package/dist/mem-start.js +2 -2
  47. package/dist/mem-summarize.js +2 -2
  48. package/dist/mem-triage.js +2 -2
  49. package/dist/merge-block.js +222 -0
  50. package/dist/metrics-cli.js +7 -4
  51. package/dist/metrics-snapshot.js +4 -3
  52. package/dist/orchestrate-initiative.js +10 -4
  53. package/dist/orchestrate-monitor.js +379 -31
  54. package/dist/signal-cleanup.js +296 -0
  55. package/dist/spawn-list.js +6 -5
  56. package/dist/state-bootstrap.js +5 -4
  57. package/dist/state-cleanup.js +360 -0
  58. package/dist/state-doctor-fix.js +196 -0
  59. package/dist/state-doctor.js +501 -0
  60. package/dist/validate-agent-skills.js +4 -3
  61. package/dist/validate-agent-sync.js +4 -3
  62. package/dist/validate-backlog-sync.js +4 -3
  63. package/dist/validate-skills-spec.js +4 -3
  64. package/dist/validate.js +4 -3
  65. package/dist/wu-block.js +3 -3
  66. package/dist/wu-claim.js +208 -98
  67. package/dist/wu-cleanup.js +5 -4
  68. package/dist/wu-create.js +71 -46
  69. package/dist/wu-delete.js +88 -60
  70. package/dist/wu-deps.js +6 -5
  71. package/dist/wu-done-check.js +34 -0
  72. package/dist/wu-done.js +39 -12
  73. package/dist/wu-edit.js +63 -28
  74. package/dist/wu-infer-lane.js +7 -6
  75. package/dist/wu-preflight.js +23 -81
  76. package/dist/wu-prep.js +125 -0
  77. package/dist/wu-prune.js +4 -3
  78. package/dist/wu-recover.js +88 -22
  79. package/dist/wu-repair.js +7 -6
  80. package/dist/wu-spawn.js +226 -270
  81. package/dist/wu-status.js +4 -3
  82. package/dist/wu-unblock.js +5 -5
  83. package/dist/wu-unlock-lane.js +4 -3
  84. package/dist/wu-validate.js +5 -4
  85. package/package.json +16 -7
  86. package/templates/core/.lumenflow/constraints.md.template +192 -0
  87. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  88. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  89. package/templates/core/AGENTS.md.template +60 -0
  90. package/templates/core/LUMENFLOW.md.template +255 -0
  91. package/templates/core/UPGRADING.md.template +121 -0
  92. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  93. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  94. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  95. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  96. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  97. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  98. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  99. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  100. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  101. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  102. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  103. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  104. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  105. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  106. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  107. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  108. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  109. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  110. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  111. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  112. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  113. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  114. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  115. package/templates/vendors/cline/.clinerules.template +53 -0
  116. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  117. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  118. package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ensureCleanWorktree } from '../wu-done-check.js';
3
+ import * as gitAdapter from '@lumenflow/core/git-adapter';
4
+ import * as errorHandler from '@lumenflow/core/error-handler';
5
+ // Mock dependencies
6
+ vi.mock('@lumenflow/core/git-adapter');
7
+ vi.mock('@lumenflow/core/error-handler');
8
+ describe('wu-done', () => {
9
+ describe('ensureCleanWorktree', () => {
10
+ let mockGit;
11
+ beforeEach(() => {
12
+ vi.resetAllMocks();
13
+ mockGit = {
14
+ getStatus: vi.fn(),
15
+ };
16
+ vi.mocked(gitAdapter.createGitForPath).mockReturnValue(mockGit);
17
+ });
18
+ it('should pass if worktree is clean', async () => {
19
+ mockGit.getStatus.mockResolvedValue(''); // Clean status
20
+ await ensureCleanWorktree('/path/to/worktree');
21
+ expect(mockGit.getStatus).toHaveBeenCalled();
22
+ expect(errorHandler.die).not.toHaveBeenCalled();
23
+ });
24
+ it('should die if worktree has uncommitted changes', async () => {
25
+ mockGit.getStatus.mockResolvedValue('M file.ts\n?? new-file.ts'); // Dirty status
26
+ await ensureCleanWorktree('/path/to/worktree');
27
+ expect(mockGit.getStatus).toHaveBeenCalled();
28
+ expect(errorHandler.die).toHaveBeenCalledWith(expect.stringContaining('Worktree has uncommitted changes'));
29
+ });
30
+ it('should use the correct worktree path', async () => {
31
+ mockGit.getStatus.mockResolvedValue('');
32
+ await ensureCleanWorktree('/custom/worktree/path');
33
+ expect(gitAdapter.createGitForPath).toHaveBeenCalledWith('/custom/worktree/path');
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * WU-1225: Tests for wu-edit append-by-default behavior
3
+ *
4
+ * Validates that array fields (code_paths, risks, acceptance, etc.)
5
+ * now append by default instead of replacing, making behavior consistent
6
+ * across all array options.
7
+ */
8
+ import { describe, it, expect } from 'vitest';
9
+ import { applyEdits, mergeStringField } from '../wu-edit.js';
10
+ describe('wu-edit applyEdits', () => {
11
+ describe('WU-1225: code_paths append-by-default', () => {
12
+ const baseWU = {
13
+ id: 'WU-1225',
14
+ status: 'ready',
15
+ code_paths: ['existing/path.ts'],
16
+ };
17
+ it('appends code_paths by default (no flags)', () => {
18
+ const opts = { codePaths: ['new/path.ts'] };
19
+ const result = applyEdits(baseWU, opts);
20
+ expect(result.code_paths).toEqual(['existing/path.ts', 'new/path.ts']);
21
+ });
22
+ it('appends code_paths when --append is set (backwards compat)', () => {
23
+ const opts = { codePaths: ['new/path.ts'], append: true };
24
+ const result = applyEdits(baseWU, opts);
25
+ expect(result.code_paths).toEqual(['existing/path.ts', 'new/path.ts']);
26
+ });
27
+ it('replaces code_paths when --replace-code-paths is set', () => {
28
+ const opts = { codePaths: ['new/path.ts'], replaceCodePaths: true };
29
+ const result = applyEdits(baseWU, opts);
30
+ expect(result.code_paths).toEqual(['new/path.ts']);
31
+ });
32
+ });
33
+ describe('WU-1225: risks append-by-default', () => {
34
+ const baseWU = {
35
+ id: 'WU-1225',
36
+ status: 'ready',
37
+ risks: ['existing risk'],
38
+ };
39
+ it('appends risks by default', () => {
40
+ const opts = { risks: ['new risk'] };
41
+ const result = applyEdits(baseWU, opts);
42
+ expect(result.risks).toEqual(['existing risk', 'new risk']);
43
+ });
44
+ it('replaces risks when --replace-risks is set', () => {
45
+ const opts = { risks: ['new risk'], replaceRisks: true };
46
+ const result = applyEdits(baseWU, opts);
47
+ expect(result.risks).toEqual(['new risk']);
48
+ });
49
+ });
50
+ describe('WU-1225: blocked_by append-by-default', () => {
51
+ const baseWU = {
52
+ id: 'WU-1225',
53
+ status: 'ready',
54
+ blocked_by: ['WU-100'],
55
+ };
56
+ it('appends blocked_by by default', () => {
57
+ const opts = { blockedBy: 'WU-200' };
58
+ const result = applyEdits(baseWU, opts);
59
+ expect(result.blocked_by).toEqual(['WU-100', 'WU-200']);
60
+ });
61
+ it('replaces blocked_by when --replace-blocked-by is set', () => {
62
+ const opts = { blockedBy: 'WU-200', replaceBlockedBy: true };
63
+ const result = applyEdits(baseWU, opts);
64
+ expect(result.blocked_by).toEqual(['WU-200']);
65
+ });
66
+ });
67
+ describe('WU-1225: dependencies append-by-default', () => {
68
+ const baseWU = {
69
+ id: 'WU-1225',
70
+ status: 'ready',
71
+ dependencies: ['WU-50'],
72
+ };
73
+ it('appends dependencies by default', () => {
74
+ const opts = { addDep: 'WU-60' };
75
+ const result = applyEdits(baseWU, opts);
76
+ expect(result.dependencies).toEqual(['WU-50', 'WU-60']);
77
+ });
78
+ it('replaces dependencies when --replace-dependencies is set', () => {
79
+ const opts = { addDep: 'WU-60', replaceDependencies: true };
80
+ const result = applyEdits(baseWU, opts);
81
+ expect(result.dependencies).toEqual(['WU-60']);
82
+ });
83
+ });
84
+ describe('WU-1144: acceptance already appends by default', () => {
85
+ const baseWU = {
86
+ id: 'WU-1225',
87
+ status: 'ready',
88
+ acceptance: ['existing criterion'],
89
+ };
90
+ it('appends acceptance by default', () => {
91
+ const opts = { acceptance: ['new criterion'] };
92
+ const result = applyEdits(baseWU, opts);
93
+ expect(result.acceptance).toEqual(['existing criterion', 'new criterion']);
94
+ });
95
+ it('replaces acceptance when --replace-acceptance is set', () => {
96
+ const opts = { acceptance: ['new criterion'], replaceAcceptance: true };
97
+ const result = applyEdits(baseWU, opts);
98
+ expect(result.acceptance).toEqual(['new criterion']);
99
+ });
100
+ });
101
+ });
102
+ describe('wu-edit mergeStringField', () => {
103
+ it('appends by default', () => {
104
+ const result = mergeStringField('existing', 'new', false);
105
+ expect(result).toBe('existing\n\nnew');
106
+ });
107
+ it('replaces when shouldReplace is true', () => {
108
+ const result = mergeStringField('existing', 'new', true);
109
+ expect(result).toBe('new');
110
+ });
111
+ it('returns new value if existing is empty', () => {
112
+ const result = mergeStringField('', 'new', false);
113
+ expect(result).toBe('new');
114
+ });
115
+ it('returns new value if existing is undefined', () => {
116
+ const result = mergeStringField(undefined, 'new', false);
117
+ expect(result).toBe('new');
118
+ });
119
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as locationResolver from '@lumenflow/core/dist/context/location-resolver.js';
3
+ import * as wuYaml from '@lumenflow/core/dist/wu-yaml.js';
4
+ import { CONTEXT_VALIDATION, WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
5
+ const { LOCATION_TYPES } = CONTEXT_VALIDATION;
6
+ // Mock dependencies
7
+ vi.mock('@lumenflow/core/dist/context/location-resolver.js');
8
+ vi.mock('@lumenflow/core/dist/error-handler.js');
9
+ vi.mock('@lumenflow/core/dist/wu-yaml.js');
10
+ vi.mock('../gates.js', () => ({
11
+ runGates: vi.fn().mockResolvedValue(true),
12
+ }));
13
+ describe('wu-prep (WU-1223)', () => {
14
+ beforeEach(() => {
15
+ vi.resetAllMocks();
16
+ });
17
+ describe('location validation', () => {
18
+ it('should error when run from main checkout', async () => {
19
+ // Mock location as main checkout
20
+ vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
21
+ type: LOCATION_TYPES.MAIN,
22
+ cwd: '/repo',
23
+ gitRoot: '/repo',
24
+ mainCheckout: '/repo',
25
+ worktreeName: null,
26
+ worktreeWuId: null,
27
+ });
28
+ // Import after mocks are set up
29
+ const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
30
+ const location = await resolveLocation();
31
+ // Verify the mock returns main
32
+ expect(location.type).toBe(LOCATION_TYPES.MAIN);
33
+ });
34
+ it('should proceed when run from worktree', async () => {
35
+ // Mock location as worktree
36
+ vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
37
+ type: LOCATION_TYPES.WORKTREE,
38
+ cwd: '/repo/worktrees/framework-cli-wu-1223',
39
+ gitRoot: '/repo/worktrees/framework-cli-wu-1223',
40
+ mainCheckout: '/repo',
41
+ worktreeName: 'framework-cli-wu-1223',
42
+ worktreeWuId: 'WU-1223',
43
+ });
44
+ const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
45
+ const location = await resolveLocation();
46
+ // Verify the mock returns worktree
47
+ expect(location.type).toBe(LOCATION_TYPES.WORKTREE);
48
+ expect(location.mainCheckout).toBe('/repo');
49
+ });
50
+ });
51
+ describe('WU status validation', () => {
52
+ it('should only allow in_progress WUs', async () => {
53
+ // Mock WU YAML with wrong status
54
+ const mockDoc = {
55
+ id: 'WU-1223',
56
+ status: WU_STATUS.DONE,
57
+ title: 'Test WU',
58
+ };
59
+ vi.mocked(wuYaml.readWU).mockReturnValue(mockDoc);
60
+ const { readWU } = await import('@lumenflow/core/dist/wu-yaml.js');
61
+ const doc = readWU('path/to/wu.yaml', 'WU-1223');
62
+ expect(doc.status).toBe(WU_STATUS.DONE);
63
+ expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
64
+ });
65
+ });
66
+ describe('success message', () => {
67
+ it('should include copy-paste instruction with main path', async () => {
68
+ // The success message should include:
69
+ // 1. Main checkout path
70
+ // 2. WU ID
71
+ // 3. Copy-paste command: cd <main> && pnpm wu:done --id <WU-ID>
72
+ const mainCheckout = '/repo';
73
+ const wuId = 'WU-1223';
74
+ // Build expected command that would be in the success message
75
+ const expectedCommand = `cd ${mainCheckout} && pnpm wu:done --id ${wuId}`;
76
+ expect(expectedCommand).toBe('cd /repo && pnpm wu:done --id WU-1223');
77
+ });
78
+ });
79
+ });
80
+ describe('wu:done worktree check (WU-1223)', () => {
81
+ beforeEach(() => {
82
+ vi.resetAllMocks();
83
+ });
84
+ it('should error when run from worktree with guidance to use wu:prep', async () => {
85
+ // Mock location as worktree
86
+ vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
87
+ type: LOCATION_TYPES.WORKTREE,
88
+ cwd: '/repo/worktrees/framework-cli-wu-1223',
89
+ gitRoot: '/repo/worktrees/framework-cli-wu-1223',
90
+ mainCheckout: '/repo',
91
+ worktreeName: 'framework-cli-wu-1223',
92
+ worktreeWuId: 'WU-1223',
93
+ });
94
+ const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
95
+ const location = await resolveLocation();
96
+ // The error message should guide user to wu:prep workflow
97
+ expect(location.type).toBe(LOCATION_TYPES.WORKTREE);
98
+ // Error message should contain:
99
+ const errorShouldContain = [
100
+ 'wu:prep', // Mention the new command
101
+ 'main checkout', // Explain where wu:done should run
102
+ '/repo', // Main checkout path
103
+ ];
104
+ // Build the expected error content
105
+ const expectedGuidance = `pnpm wu:prep --id WU-1223`;
106
+ expect(expectedGuidance).toContain('wu:prep');
107
+ });
108
+ });
@@ -243,9 +243,10 @@ async function main() {
243
243
  const issues = await readIssues(baseDir, sinceDate, opts.category, opts.severity);
244
244
  displaySummary(issues, opts.since);
245
245
  }
246
- // Guard main() for testability (WU-1366)
247
- import { fileURLToPath } from 'node:url';
248
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
246
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
247
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
248
+ // path but import.meta.url resolves to the real path - they never match
249
+ if (import.meta.main) {
249
250
  main().catch((err) => {
250
251
  die(`Issues query failed: ${err.message}`);
251
252
  });
@@ -6,11 +6,32 @@
6
6
  *
7
7
  * Usage:
8
8
  * pnpm agent:log-issue --category workflow --severity minor --title "..." --description "..."
9
+ * pnpm agent:log-issue --category tooling --severity major --title "..." --description "..." \
10
+ * --tag worktree --tag gates --file src/main.ts --file src/utils.ts
11
+ *
12
+ * WU-1182: Uses Commander.js repeatable options pattern for --tag and --file.
13
+ * Use --tag multiple times instead of comma-separated --tags.
9
14
  */
10
15
  import { Command } from 'commander';
11
16
  import { logIncident, getCurrentSession } from '@lumenflow/agent';
12
17
  import { EXIT_CODES, INCIDENT_SEVERITY, LUMENFLOW_PATHS, } from '@lumenflow/core/dist/wu-constants.js';
13
18
  import chalk from 'chalk';
19
+ /**
20
+ * WU-1182: Collector function for Commander.js repeatable options.
21
+ * Accumulates multiple flag values into an array.
22
+ *
23
+ * Usage: --tag a --tag b → ['a', 'b']
24
+ *
25
+ * This follows Commander.js best practices - use repeatable pattern for
26
+ * multi-value options instead of comma-separated splits.
27
+ *
28
+ * @param value - New value from CLI
29
+ * @param previous - Previously accumulated values
30
+ * @returns Updated array with new value appended
31
+ */
32
+ function collectRepeatable(value, previous) {
33
+ return previous.concat([value]);
34
+ }
14
35
  const program = new Command()
15
36
  .name('agent:log-issue')
16
37
  .description('Log a workflow issue or incident')
@@ -19,9 +40,9 @@ const program = new Command()
19
40
  .requiredOption('--title <title>', 'Short description (5-100 chars)')
20
41
  .requiredOption('--description <desc>', 'Detailed context (10-2000 chars)')
21
42
  .option('--resolution <res>', 'How the issue was resolved')
22
- .option('--tags <tags>', 'Comma-separated tags (e.g., worktree,gates)')
43
+ .option('--tag <tag>', 'Tag for categorization (repeatable)', collectRepeatable, [])
23
44
  .option('--step <step>', 'Current workflow step (e.g., wu:done, gates)')
24
- .option('--files <files>', 'Comma-separated related files')
45
+ .option('--file <file>', 'Related file path (repeatable)', collectRepeatable, [])
25
46
  .action(async (opts) => {
26
47
  try {
27
48
  const session = await getCurrentSession();
@@ -36,10 +57,10 @@ const program = new Command()
36
57
  title: opts.title,
37
58
  description: opts.description,
38
59
  resolution: opts.resolution,
39
- tags: opts.tags ? opts.tags.split(',').map((t) => t.trim()) : [],
60
+ tags: opts.tag,
40
61
  context: {
41
62
  current_step: opts.step,
42
- related_files: opts.files ? opts.files.split(',').map((f) => f.trim()) : [],
63
+ related_files: opts.file,
43
64
  },
44
65
  };
45
66
  await logIncident(incident);
@@ -6,7 +6,7 @@
6
6
  * - Auto-tagging stale WUs (in_progress/ready too long without activity)
7
7
  * - Archiving old completed WUs (done for > N days)
8
8
  *
9
- * WU-1106: INIT-003 Phase 3b - Migrate from PatientPath tools/backlog-prune.mjs
9
+ * WU-1106: INIT-003 Phase 3b - Migrate from PatientPath tools/backlog-prune.ts
10
10
  *
11
11
  * Usage:
12
12
  * pnpm backlog:prune # Dry-run mode (shows what would be done)
@@ -291,9 +291,10 @@ async function main() {
291
291
  }
292
292
  process.exit(EXIT_CODES.SUCCESS);
293
293
  }
294
- // Guard main() for testability
295
- import { fileURLToPath } from 'node:url';
294
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
295
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
296
+ // path but import.meta.url resolves to the real path - they never match
296
297
  import { runCLI } from './cli-entry-point.js';
297
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
298
+ if (import.meta.main) {
298
299
  runCLI(main);
299
300
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console -- CLI entry point uses console for error output */
1
2
  /**
2
3
  * Shared CLI entry point wrapper
3
4
  *
@@ -12,6 +13,10 @@
12
13
  *
13
14
  * WU-1085: Initializes color support respecting NO_COLOR/FORCE_COLOR/--no-color
14
15
  *
16
+ * WU-1233: Adds EPIPE protection for pipe resilience. When CLI output is piped
17
+ * through head/tail, the pipe may close before all output is written. Without
18
+ * this protection, Node.js throws unhandled EPIPE errors crashing the process.
19
+ *
15
20
  * @example
16
21
  * ```typescript
17
22
  * // At the bottom of each CLI file:
@@ -23,15 +28,20 @@
23
28
  * ```
24
29
  */
25
30
  import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
26
- import { initColorSupport } from '@lumenflow/core';
31
+ import { initColorSupport, StreamErrorHandler } from '@lumenflow/core';
27
32
  /**
28
33
  * Wraps an async main function with proper error handling.
29
34
  * WU-1085: Also initializes color support based on NO_COLOR/FORCE_COLOR/--no-color
35
+ * WU-1233: Attaches EPIPE handler for graceful pipe closure
30
36
  *
31
37
  * @param main - The async main function to execute
32
38
  * @returns Promise that resolves when main completes (or after error handling)
33
39
  */
34
40
  export async function runCLI(main) {
41
+ // WU-1233: Attach EPIPE handler before running command
42
+ // This must be done early to catch any EPIPE errors during execution
43
+ const streamErrorHandler = StreamErrorHandler.createWithDefaults();
44
+ streamErrorHandler.attach();
35
45
  // WU-1085: Initialize color support before running command
36
46
  initColorSupport();
37
47
  try {