@lumenflow/cli 1.1.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 (118) 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/gates.js +102 -39
  5. package/dist/init.js +241 -195
  6. package/dist/initiative-add-wu.js +2 -1
  7. package/dist/initiative-create.js +5 -8
  8. package/dist/initiative-edit.js +3 -3
  9. package/dist/initiative-list.js +2 -1
  10. package/dist/initiative-status.js +2 -1
  11. package/dist/wu-claim.js +297 -110
  12. package/dist/wu-cleanup.js +129 -57
  13. package/dist/wu-create.js +197 -122
  14. package/dist/wu-deps.js +2 -1
  15. package/dist/wu-done.js +46 -14
  16. package/dist/wu-edit.js +152 -61
  17. package/dist/wu-infer-lane.js +5 -4
  18. package/dist/wu-preflight.js +2 -1
  19. package/dist/wu-prune.js +12 -3
  20. package/dist/wu-repair.js +2 -1
  21. package/dist/wu-spawn.js +79 -159
  22. package/dist/wu-unlock-lane.js +6 -1
  23. package/dist/wu-validate.js +2 -1
  24. package/package.json +14 -14
  25. package/dist/gates.d.ts +0 -41
  26. package/dist/gates.d.ts.map +0 -1
  27. package/dist/gates.js.map +0 -1
  28. package/dist/initiative-add-wu.d.ts +0 -22
  29. package/dist/initiative-add-wu.d.ts.map +0 -1
  30. package/dist/initiative-add-wu.js.map +0 -1
  31. package/dist/initiative-create.d.ts +0 -28
  32. package/dist/initiative-create.d.ts.map +0 -1
  33. package/dist/initiative-create.js.map +0 -1
  34. package/dist/initiative-edit.d.ts +0 -34
  35. package/dist/initiative-edit.d.ts.map +0 -1
  36. package/dist/initiative-edit.js.map +0 -1
  37. package/dist/initiative-list.d.ts +0 -12
  38. package/dist/initiative-list.d.ts.map +0 -1
  39. package/dist/initiative-list.js.map +0 -1
  40. package/dist/initiative-status.d.ts +0 -11
  41. package/dist/initiative-status.d.ts.map +0 -1
  42. package/dist/initiative-status.js.map +0 -1
  43. package/dist/mem-checkpoint.d.ts +0 -16
  44. package/dist/mem-checkpoint.d.ts.map +0 -1
  45. package/dist/mem-checkpoint.js.map +0 -1
  46. package/dist/mem-cleanup.d.ts +0 -29
  47. package/dist/mem-cleanup.d.ts.map +0 -1
  48. package/dist/mem-cleanup.js.map +0 -1
  49. package/dist/mem-create.d.ts +0 -17
  50. package/dist/mem-create.d.ts.map +0 -1
  51. package/dist/mem-create.js.map +0 -1
  52. package/dist/mem-inbox.d.ts +0 -35
  53. package/dist/mem-inbox.d.ts.map +0 -1
  54. package/dist/mem-inbox.js.map +0 -1
  55. package/dist/mem-init.d.ts +0 -15
  56. package/dist/mem-init.d.ts.map +0 -1
  57. package/dist/mem-init.js.map +0 -1
  58. package/dist/mem-ready.d.ts +0 -16
  59. package/dist/mem-ready.d.ts.map +0 -1
  60. package/dist/mem-ready.js.map +0 -1
  61. package/dist/mem-signal.d.ts +0 -16
  62. package/dist/mem-signal.d.ts.map +0 -1
  63. package/dist/mem-signal.js.map +0 -1
  64. package/dist/mem-start.d.ts +0 -16
  65. package/dist/mem-start.d.ts.map +0 -1
  66. package/dist/mem-start.js.map +0 -1
  67. package/dist/mem-summarize.d.ts +0 -22
  68. package/dist/mem-summarize.d.ts.map +0 -1
  69. package/dist/mem-summarize.js.map +0 -1
  70. package/dist/mem-triage.d.ts +0 -22
  71. package/dist/mem-triage.d.ts.map +0 -1
  72. package/dist/mem-triage.js.map +0 -1
  73. package/dist/spawn-list.d.ts +0 -16
  74. package/dist/spawn-list.d.ts.map +0 -1
  75. package/dist/spawn-list.js.map +0 -1
  76. package/dist/wu-block.d.ts +0 -16
  77. package/dist/wu-block.d.ts.map +0 -1
  78. package/dist/wu-block.js.map +0 -1
  79. package/dist/wu-claim.d.ts +0 -32
  80. package/dist/wu-claim.d.ts.map +0 -1
  81. package/dist/wu-claim.js.map +0 -1
  82. package/dist/wu-cleanup.d.ts +0 -17
  83. package/dist/wu-cleanup.d.ts.map +0 -1
  84. package/dist/wu-cleanup.js.map +0 -1
  85. package/dist/wu-create.d.ts +0 -38
  86. package/dist/wu-create.d.ts.map +0 -1
  87. package/dist/wu-create.js.map +0 -1
  88. package/dist/wu-deps.d.ts +0 -13
  89. package/dist/wu-deps.d.ts.map +0 -1
  90. package/dist/wu-deps.js.map +0 -1
  91. package/dist/wu-done.d.ts +0 -153
  92. package/dist/wu-done.d.ts.map +0 -1
  93. package/dist/wu-done.js.map +0 -1
  94. package/dist/wu-edit.d.ts +0 -29
  95. package/dist/wu-edit.d.ts.map +0 -1
  96. package/dist/wu-edit.js.map +0 -1
  97. package/dist/wu-infer-lane.d.ts +0 -17
  98. package/dist/wu-infer-lane.d.ts.map +0 -1
  99. package/dist/wu-infer-lane.js.map +0 -1
  100. package/dist/wu-preflight.d.ts +0 -47
  101. package/dist/wu-preflight.d.ts.map +0 -1
  102. package/dist/wu-preflight.js.map +0 -1
  103. package/dist/wu-prune.d.ts +0 -16
  104. package/dist/wu-prune.d.ts.map +0 -1
  105. package/dist/wu-prune.js.map +0 -1
  106. package/dist/wu-repair.d.ts +0 -60
  107. package/dist/wu-repair.d.ts.map +0 -1
  108. package/dist/wu-repair.js.map +0 -1
  109. package/dist/wu-spawn-completion.d.ts +0 -10
  110. package/dist/wu-spawn.d.ts +0 -168
  111. package/dist/wu-spawn.d.ts.map +0 -1
  112. package/dist/wu-spawn.js.map +0 -1
  113. package/dist/wu-unblock.d.ts +0 -16
  114. package/dist/wu-unblock.d.ts.map +0 -1
  115. package/dist/wu-unblock.js.map +0 -1
  116. package/dist/wu-validate.d.ts +0 -16
  117. package/dist/wu-validate.d.ts.map +0 -1
  118. package/dist/wu-validate.js.map +0 -1
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
@@ -147,7 +147,7 @@ async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
147
147
  ` pnpm wu:repair-claim --id ${id}\n\n` +
148
148
  `After repair, retry:\n` +
149
149
  ` pnpm wu:done --id ${id}\n\n` +
150
- `See: ai/onboarding/troubleshooting-wu-done.md for more recovery options.`);
150
+ `See: docs/04-operations/_frameworks/lumenflow/agent/onboarding/troubleshooting-wu-done.md for more recovery options.`);
151
151
  }
152
152
  export function printExposureWarnings(wu, options = {}) {
153
153
  // Validate exposure
@@ -1793,7 +1793,8 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1793
1793
  }
1794
1794
  else {
1795
1795
  die(`Worktree not found (${worktreePath || 'unknown'}). Gates must run in the lane worktree.\n` +
1796
- `If the worktree was removed, recreate it and retry, or use --skip-gates with justification.`);
1796
+ `If the worktree was removed, recreate it and retry, or rerun with --branch-only when the lane branch exists.\n` +
1797
+ `Use --skip-gates only with justification.`);
1797
1798
  }
1798
1799
  // Step 0.75: Run COS governance gates (WU-614, COS v1.3 §7)
1799
1800
  if (!args.skipCosGates) {
@@ -1858,6 +1859,13 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1858
1859
  * @param {string|null} params.derivedWorktree - Derived worktree path
1859
1860
  * @param {string} params.STAMPS_DIR - Stamps directory path
1860
1861
  */
1862
+ export function computeBranchOnlyFallback({ isBranchOnly, branchOnlyRequested, worktreeExists, derivedWorktree, }) {
1863
+ const allowFallback = Boolean(branchOnlyRequested) && !isBranchOnly && !worktreeExists && Boolean(derivedWorktree);
1864
+ return {
1865
+ allowFallback,
1866
+ effectiveBranchOnly: isBranchOnly || allowFallback,
1867
+ };
1868
+ }
1861
1869
  function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR }) {
1862
1870
  const stampExists = existsSync(path.join(STAMPS_DIR, `${id}.done`)) ? 'yes' : 'no';
1863
1871
  const yamlStatus = docMain.status || 'unknown';
@@ -1869,6 +1877,8 @@ function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree,
1869
1877
  }
1870
1878
  // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1871
1879
  async function main() {
1880
+ // Allow pre-push hook to recognize wu:done automation (WU-1030)
1881
+ process.env.LUMENFLOW_WU_TOOL = 'wu-done';
1872
1882
  // Validate CLI arguments and WU ID format (extracted to wu-done-validators.mjs)
1873
1883
  const { args, id } = validateInputs(process.argv);
1874
1884
  // Detect workspace mode and calculate paths (WU-1215: extracted to validators module)
@@ -1876,25 +1886,39 @@ async function main() {
1876
1886
  const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain, isBranchOnly, derivedWorktree, docForValidation: initialDocForValidation, isDocsOnly, } = pathInfo;
1877
1887
  // Capture main checkout path once. process.cwd() may drift later during recovery flows.
1878
1888
  const mainCheckoutPath = process.cwd();
1889
+ // Resolve worktree path early so we can detect missing worktree before pre-flight checks
1890
+ const resolvedWorktreePath = derivedWorktree && !isBranchOnly
1891
+ ? path.isAbsolute(derivedWorktree)
1892
+ ? derivedWorktree
1893
+ : path.resolve(mainCheckoutPath, derivedWorktree)
1894
+ : null;
1895
+ const worktreeExists = resolvedWorktreePath ? existsSync(resolvedWorktreePath) : false;
1896
+ const { allowFallback: allowBranchOnlyFallback, effectiveBranchOnly } = computeBranchOnlyFallback({
1897
+ isBranchOnly,
1898
+ branchOnlyRequested: args.branchOnly,
1899
+ worktreeExists,
1900
+ derivedWorktree,
1901
+ });
1902
+ if (allowBranchOnlyFallback) {
1903
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Worktree missing (${resolvedWorktreePath}). Proceeding in branch-only mode because --branch-only was provided.`);
1904
+ }
1905
+ const effectiveDerivedWorktree = effectiveBranchOnly ? null : derivedWorktree;
1906
+ const effectiveWorktreePath = effectiveBranchOnly ? null : resolvedWorktreePath;
1879
1907
  // Pre-flight checks (WU-1215: extracted to executePreFlightChecks function)
1880
1908
  const preFlightResult = await executePreFlightChecks({
1881
1909
  id,
1882
1910
  args,
1883
- isBranchOnly,
1911
+ isBranchOnly: effectiveBranchOnly,
1884
1912
  isDocsOnly,
1885
1913
  docMain,
1886
1914
  docForValidation: initialDocForValidation,
1887
- derivedWorktree,
1915
+ derivedWorktree: effectiveDerivedWorktree,
1888
1916
  });
1889
1917
  const title = preFlightResult.title;
1890
1918
  // Note: docForValidation is returned but not used after pre-flight checks
1891
1919
  // The metadata transaction uses docForUpdate instead
1892
1920
  // Step 0: Run gates (WU-1215: extracted to executeGates function)
1893
- const worktreePath = derivedWorktree && !isBranchOnly
1894
- ? path.isAbsolute(derivedWorktree)
1895
- ? derivedWorktree
1896
- : path.resolve(mainCheckoutPath, derivedWorktree)
1897
- : null;
1921
+ const worktreePath = effectiveWorktreePath;
1898
1922
  // WU-1943: Check if any checkpoints exist for this WU session
1899
1923
  // Warn (don't block) if no checkpoints - agent should have been checkpointing periodically
1900
1924
  try {
@@ -1908,9 +1932,16 @@ async function main() {
1908
1932
  catch {
1909
1933
  // Non-blocking: checkpoint check failure should not block wu:done
1910
1934
  }
1911
- await executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath });
1935
+ await executeGates({ id, args, isBranchOnly: effectiveBranchOnly, isDocsOnly, worktreePath });
1912
1936
  // Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
1913
- printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR });
1937
+ printStateHUD({
1938
+ id,
1939
+ docMain,
1940
+ isBranchOnly: effectiveBranchOnly,
1941
+ isDocsOnly,
1942
+ derivedWorktree: effectiveDerivedWorktree,
1943
+ STAMPS_DIR,
1944
+ });
1914
1945
  // Step 0.5: Pre-flight validation - run ALL pre-commit hooks BEFORE merge
1915
1946
  // This prevents partial completion states where merge succeeds but commit fails
1916
1947
  // Validates all 8 gates: secrets, file size, ESLint, Prettier, TypeScript, audit, architecture, tasks
@@ -1947,7 +1978,7 @@ async function main() {
1947
1978
  validateStagedFiles,
1948
1979
  };
1949
1980
  try {
1950
- if (isBranchOnly) {
1981
+ if (effectiveBranchOnly) {
1951
1982
  // Branch-Only mode: merge first, then update metadata on main
1952
1983
  // NOTE: Branch-only still uses old rollback mechanism
1953
1984
  const branchOnlyContext = {
@@ -2165,7 +2196,7 @@ async function detectChangedDocPaths(worktreePath, baseBranch) {
2165
2196
  // Get files changed in this branch vs base
2166
2197
  const diff = await git.raw(['diff', '--name-only', baseBranch]);
2167
2198
  const changedFiles = diff.split('\n').filter(Boolean);
2168
- // Filter to docs: ai/onboarding/, docs/, CLAUDE.md, README.md, *.md in root
2199
+ // Filter to docs: docs/04-operations/_frameworks/lumenflow/agent/onboarding/, docs/, CLAUDE.md, README.md, *.md in root
2169
2200
  const docPatterns = [
2170
2201
  /^ai\/onboarding\//,
2171
2202
  /^docs\//,
@@ -2183,6 +2214,7 @@ async function detectChangedDocPaths(worktreePath, baseBranch) {
2183
2214
  // Guard main() execution for testability (WU-1366)
2184
2215
  // When imported as a module for testing, main() should not auto-run
2185
2216
  import { fileURLToPath } from 'node:url';
2217
+ import { runCLI } from './cli-entry-point.js';
2186
2218
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
2187
- main();
2219
+ runCLI(main);
2188
2220
  }
package/dist/wu-edit.js CHANGED
@@ -26,6 +26,7 @@
26
26
  * Part of WU-1274: Add wu:edit command for spec-only changes
27
27
  * @see {@link tools/lib/micro-worktree.mjs} - Shared micro-worktree logic
28
28
  */
29
+ import { fileURLToPath } from 'node:url';
29
30
  import { getGitForCwd, createGitForPath } from '@lumenflow/core/dist/git-adapter.js';
30
31
  import { die } from '@lumenflow/core/dist/error-handler.js';
31
32
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
@@ -35,7 +36,9 @@ import { join, resolve } from 'node:path';
35
36
  import { parseYAML, stringifyYAML, readWU } from '@lumenflow/core/dist/wu-yaml.js';
36
37
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
37
38
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
38
- import { FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, COMMIT_FORMATS, WU_STATUS, CLAIMED_MODES, getLaneBranch, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, READINESS_UI, } from '@lumenflow/core/dist/wu-constants.js';
39
+ import { FILE_SYSTEM, EXIT_CODES, MICRO_WORKTREE_OPERATIONS, LOG_PREFIX, COMMIT_FORMATS, WU_STATUS, CLAIMED_MODES, getLaneBranch, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, READINESS_UI,
40
+ // WU-1039: Import exposure values for validation (Library-First, no magic strings)
41
+ WU_EXPOSURE_VALUES, } from '@lumenflow/core/dist/wu-constants.js';
39
42
  // WU-1593: Use centralized validateWUIDFormat (DRY)
40
43
  import { ensureOnMain, ensureMainUpToDate, validateWUIDFormat, } from '@lumenflow/core/dist/wu-helpers.js';
41
44
  import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
@@ -57,6 +60,86 @@ import { normalizeWUSchema } from '@lumenflow/core/dist/wu-schema-normalization.
57
60
  import { lintWUSpec, formatLintErrors } from '@lumenflow/core/dist/wu-lint.js';
58
61
  /* eslint-disable security/detect-object-injection */
59
62
  const PREFIX = LOG_PREFIX.EDIT;
63
+ /**
64
+ * WU-1039: Validate which edits are allowed on done WUs
65
+ *
66
+ * Done WUs only allow metadata reassignment: initiative, phase, and exposure.
67
+ * All other edits are blocked to preserve WU immutability after completion.
68
+ *
69
+ * @param opts - Parsed CLI options
70
+ * @returns { valid: boolean, disallowedEdits: string[] }
71
+ */
72
+ export function validateDoneWUEdits(opts) {
73
+ const disallowedEdits = [];
74
+ // Check for disallowed edits on done WUs
75
+ if (opts.specFile)
76
+ disallowedEdits.push('--spec-file');
77
+ if (opts.description)
78
+ disallowedEdits.push('--description');
79
+ if (opts.acceptance && Array.isArray(opts.acceptance) && opts.acceptance.length > 0) {
80
+ disallowedEdits.push('--acceptance');
81
+ }
82
+ if (opts.notes)
83
+ disallowedEdits.push('--notes');
84
+ if (opts.codePaths && Array.isArray(opts.codePaths) && opts.codePaths.length > 0) {
85
+ disallowedEdits.push('--code-paths');
86
+ }
87
+ if (opts.lane)
88
+ disallowedEdits.push('--lane');
89
+ if (opts.type)
90
+ disallowedEdits.push('--type');
91
+ if (opts.priority)
92
+ disallowedEdits.push('--priority');
93
+ if (opts.testPathsManual &&
94
+ Array.isArray(opts.testPathsManual) &&
95
+ opts.testPathsManual.length > 0) {
96
+ disallowedEdits.push('--test-paths-manual');
97
+ }
98
+ if (opts.testPathsUnit && Array.isArray(opts.testPathsUnit) && opts.testPathsUnit.length > 0) {
99
+ disallowedEdits.push('--test-paths-unit');
100
+ }
101
+ if (opts.testPathsE2e && Array.isArray(opts.testPathsE2e) && opts.testPathsE2e.length > 0) {
102
+ disallowedEdits.push('--test-paths-e2e');
103
+ }
104
+ return {
105
+ valid: disallowedEdits.length === 0,
106
+ disallowedEdits,
107
+ };
108
+ }
109
+ /**
110
+ * WU-1039: Validate exposure value against schema
111
+ *
112
+ * Uses WU_EXPOSURE_VALUES from core constants (Library-First, no magic strings).
113
+ *
114
+ * @param exposure - Exposure value to validate
115
+ * @returns { valid: boolean, error?: string }
116
+ */
117
+ export function validateExposureValue(exposure) {
118
+ // WU_EXPOSURE_VALUES is readonly array, need to cast for includes check
119
+ const validValues = WU_EXPOSURE_VALUES;
120
+ if (!validValues.includes(exposure)) {
121
+ return {
122
+ valid: false,
123
+ error: `Invalid exposure value: "${exposure}"\n\nValid values: ${WU_EXPOSURE_VALUES.join(', ')}`,
124
+ };
125
+ }
126
+ return { valid: true };
127
+ }
128
+ /**
129
+ * WU-1039: Apply exposure edit to WU object
130
+ *
131
+ * Returns a new WU object with updated exposure (immutable pattern).
132
+ *
133
+ * @param wu - Original WU object
134
+ * @param exposure - New exposure value
135
+ * @returns Updated WU object (does not mutate original)
136
+ */
137
+ export function applyExposureEdit(wu, exposure) {
138
+ return {
139
+ ...wu,
140
+ exposure,
141
+ };
142
+ }
60
143
  /**
61
144
  * Custom options for wu-edit (not in shared WU_OPTIONS)
62
145
  */
@@ -244,6 +327,8 @@ function parseArgs() {
244
327
  // WU-2564: Add blocked_by and dependencies
245
328
  EDIT_OPTIONS.blockedBy,
246
329
  EDIT_OPTIONS.addDep,
330
+ // WU-1039: Add exposure for done WU metadata updates
331
+ WU_OPTIONS.exposure,
247
332
  ],
248
333
  required: ['id'],
249
334
  allowPositionalId: true,
@@ -629,6 +714,14 @@ function applyEdits(wu, opts) {
629
714
  .filter(Boolean);
630
715
  updated.dependencies = mergeArrayField(wu.dependencies, depIds, opts.append);
631
716
  }
717
+ // WU-1039: Handle --exposure flag with validation
718
+ if (opts.exposure) {
719
+ const exposureResult = validateExposureValue(opts.exposure);
720
+ if (!exposureResult.valid) {
721
+ die(exposureResult.error);
722
+ }
723
+ updated.exposure = opts.exposure;
724
+ }
632
725
  return updated;
633
726
  }
634
727
  /**
@@ -642,38 +735,18 @@ async function main() {
642
735
  // Validate inputs
643
736
  validateWUIDFormat(id);
644
737
  const { wu: originalWU, editMode, isDone } = validateWUEditable(id);
645
- // WU-1929: Done WUs only allow initiative/phase edits (metadata reassignment)
738
+ // WU-1039: Done WUs allow initiative/phase/exposure edits only (metadata reassignment)
739
+ // Uses validateDoneWUEdits (DRY - centralized validation logic)
646
740
  if (isDone) {
647
- const disallowedEdits = [];
648
- if (opts.specFile)
649
- disallowedEdits.push('--spec-file');
650
- if (opts.description)
651
- disallowedEdits.push('--description');
652
- if (opts.acceptance && opts.acceptance.length > 0)
653
- disallowedEdits.push('--acceptance');
654
- if (opts.notes)
655
- disallowedEdits.push('--notes');
656
- if (opts.codePaths && opts.codePaths.length > 0)
657
- disallowedEdits.push('--code-paths');
658
- if (opts.lane)
659
- disallowedEdits.push('--lane');
660
- if (opts.type)
661
- disallowedEdits.push('--type');
662
- if (opts.priority)
663
- disallowedEdits.push('--priority');
664
- if (opts.testPathsManual && opts.testPathsManual.length > 0)
665
- disallowedEdits.push('--test-paths-manual');
666
- if (opts.testPathsUnit && opts.testPathsUnit.length > 0)
667
- disallowedEdits.push('--test-paths-unit');
668
- if (opts.testPathsE2e && opts.testPathsE2e.length > 0)
669
- disallowedEdits.push('--test-paths-e2e');
670
- if (disallowedEdits.length > 0) {
741
+ const doneValidation = validateDoneWUEdits(opts);
742
+ if (!doneValidation.valid) {
671
743
  die(`Cannot edit WU ${id}: WU is done/immutable.\n\n` +
672
- `Completed WUs only allow initiative/phase reassignment.\n` +
673
- `Disallowed edits: ${disallowedEdits.join(', ')}\n\n` +
744
+ `Completed WUs only allow initiative/phase/exposure reassignment.\n` +
745
+ `Disallowed edits: ${doneValidation.disallowedEdits.join(', ')}\n\n` +
674
746
  `Allowed for done WUs:\n` +
675
747
  ` --initiative <initId> Reassign to different initiative\n` +
676
- ` --phase <number> Update phase within initiative`);
748
+ ` --phase <number> Update phase within initiative\n` +
749
+ ` --exposure <type> Update exposure level`);
677
750
  }
678
751
  }
679
752
  // Check we have something to edit
@@ -698,7 +771,9 @@ async function main() {
698
771
  opts.phase ||
699
772
  // WU-2564: Add blocked_by and add_dep to hasEdits check
700
773
  opts.blockedBy ||
701
- opts.addDep;
774
+ opts.addDep ||
775
+ // WU-1039: Add exposure to hasEdits check
776
+ opts.exposure;
702
777
  if (!hasEdits) {
703
778
  die('No edits specified.\n\n' +
704
779
  'Provide one of:\n' +
@@ -716,7 +791,8 @@ async function main() {
716
791
  ' --test-paths-unit <path> Add unit test paths (repeatable; use --append to add)\n' +
717
792
  ' --test-paths-e2e <path> Add e2e test paths (repeatable; use --append to add)\n' +
718
793
  ' --blocked-by <wuIds> WU IDs that block this WU (comma-separated; use --append to add)\n' +
719
- ' --add-dep <wuIds> Add WU IDs to dependencies (comma-separated; use --append to add)');
794
+ ' --add-dep <wuIds> Add WU IDs to dependencies (comma-separated; use --append to add)\n' +
795
+ ' --exposure <type> Update exposure level (ui, api, backend-only, documentation)');
720
796
  }
721
797
  // Apply edits to get updated WU
722
798
  const updatedWU = applyEdits(originalWU, opts);
@@ -814,39 +890,54 @@ async function main() {
814
890
  const oldInitiative = originalWU.initiative;
815
891
  const newInitiative = opts.initiative;
816
892
  const initiativeChanged = newInitiative && newInitiative !== oldInitiative;
817
- await withMicroWorktree({
818
- operation: MICRO_WORKTREE_OPERATIONS.WU_EDIT,
819
- id: id,
820
- logPrefix: PREFIX,
821
- execute: async ({ worktreePath }) => {
822
- const files = [WU_PATHS.WU(id)];
823
- // Write updated WU to micro-worktree (WU-1750: use normalized data)
824
- const wuPath = join(worktreePath, WU_PATHS.WU(id));
825
- // WU-1442: Normalize dates before dumping to prevent ISO timestamp corruption
826
- normalizeWUDates(normalizedWU);
827
- // Emergency fix Session 2: Use centralized stringifyYAML helper
828
- const yamlContent = stringifyYAML(normalizedWU);
829
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
830
- writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
831
- console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
832
- // WU-1929: Handle bidirectional initiative updates
833
- if (initiativeChanged) {
834
- const initiativeFiles = updateInitiativeWusArrays(worktreePath, id, oldInitiative, newInitiative);
835
- files.push(...initiativeFiles);
836
- }
837
- return {
838
- commitMessage: COMMIT_FORMATS.EDIT(id),
839
- files,
840
- };
841
- },
842
- });
893
+ const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
894
+ process.env.LUMENFLOW_WU_TOOL = MICRO_WORKTREE_OPERATIONS.WU_EDIT;
895
+ try {
896
+ await withMicroWorktree({
897
+ operation: MICRO_WORKTREE_OPERATIONS.WU_EDIT,
898
+ id: id,
899
+ logPrefix: PREFIX,
900
+ execute: async ({ worktreePath }) => {
901
+ const files = [WU_PATHS.WU(id)];
902
+ // Write updated WU to micro-worktree (WU-1750: use normalized data)
903
+ const wuPath = join(worktreePath, WU_PATHS.WU(id));
904
+ // WU-1442: Normalize dates before dumping to prevent ISO timestamp corruption
905
+ normalizeWUDates(normalizedWU);
906
+ // Emergency fix Session 2: Use centralized stringifyYAML helper
907
+ const yamlContent = stringifyYAML(normalizedWU);
908
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
909
+ writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
910
+ console.log(`${PREFIX} Updated ${id}.yaml in micro-worktree`);
911
+ // WU-1929: Handle bidirectional initiative updates
912
+ if (initiativeChanged) {
913
+ const initiativeFiles = updateInitiativeWusArrays(worktreePath, id, oldInitiative, newInitiative);
914
+ files.push(...initiativeFiles);
915
+ }
916
+ return {
917
+ commitMessage: COMMIT_FORMATS.EDIT(id),
918
+ files,
919
+ };
920
+ },
921
+ });
922
+ }
923
+ finally {
924
+ if (previousWuTool === undefined) {
925
+ delete process.env.LUMENFLOW_WU_TOOL;
926
+ }
927
+ else {
928
+ process.env.LUMENFLOW_WU_TOOL = previousWuTool;
929
+ }
930
+ }
843
931
  console.log(`${PREFIX} ✅ Successfully edited ${id}`);
844
932
  console.log(`${PREFIX} Changes pushed to origin/main`);
845
933
  // WU-1620: Display readiness summary
846
934
  displayReadinessSummary(id);
847
935
  }
848
936
  }
849
- main().catch((err) => {
850
- console.error(`${PREFIX} ${err.message}`);
851
- process.exit(EXIT_CODES.ERROR);
852
- });
937
+ // Guard main() execution for testability (WU-1366)
938
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
939
+ main().catch((err) => {
940
+ console.error(`${PREFIX} ❌ ${err.message}`);
941
+ process.exit(EXIT_CODES.ERROR);
942
+ });
943
+ }
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { readFileSync, existsSync } from 'node:fs';
18
18
  import path from 'node:path';
19
- import yaml from 'js-yaml';
19
+ import { parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
20
20
  import { inferSubLane } from '@lumenflow/core/dist/lane-inference.js';
21
21
  import { die } from '@lumenflow/core/dist/error-handler.js';
22
22
  import { FILE_SYSTEM, EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
@@ -81,7 +81,7 @@ function loadWuYaml(id) {
81
81
  ` 2. Ensure you have read access to the repository`);
82
82
  }
83
83
  try {
84
- const doc = yaml.load(content);
84
+ const doc = parseYAML(content);
85
85
  return doc;
86
86
  }
87
87
  catch (err) {
@@ -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 };