@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
@@ -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
  *
@@ -13,33 +13,60 @@
13
13
  *
14
14
  * Usage:
15
15
  * pnpm wu:cleanup --id WU-703
16
+ * pnpm wu:cleanup --artifacts
16
17
  */
17
18
  import { execSync } from 'node:child_process';
18
- import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
19
+ import { createGitForPath, getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
19
20
  import { existsSync } from 'node:fs';
20
21
  import path from 'node:path';
21
22
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
23
+ import { die } from '@lumenflow/core/dist/error-handler.js';
24
+ import { cleanupWorktreeBuildArtifacts } from '@lumenflow/core/dist/rebase-artifact-cleanup.js';
25
+ import { detectCurrentWorktree } from '@lumenflow/core/dist/wu-done-validators.js';
22
26
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
23
27
  import { readWU } from '@lumenflow/core/dist/wu-yaml.js';
24
- import { BRANCHES, EXIT_CODES, FILE_SYSTEM, 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';
25
30
  // WU-2278: Import ownership validation for cross-agent protection
26
31
  import { validateWorktreeOwnership } from '@lumenflow/core/dist/worktree-ownership.js';
27
32
  /* eslint-disable security/detect-non-literal-fs-filename */
28
- // Box drawing characters for consistent output
29
- const BOX = {
30
- TOP: '╔═══════════════════════════════════════════════════════════════════╗',
31
- MID: '╠═══════════════════════════════════════════════════════════════════╣',
32
- BOT: '╚═══════════════════════════════════════════════════════════════════╝',
33
+ const CLEANUP_OPTIONS = {
34
+ artifacts: {
35
+ name: 'artifacts',
36
+ flags: '--artifacts',
37
+ description: 'Remove build artifacts (dist, tsbuildinfo) in current worktree',
38
+ },
33
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
+ }
34
59
  // Help text is now auto-generated by commander via createWUParser
35
60
  async function verifyPRMerged(laneBranch) {
36
- // Try gh API first (most reliable)
61
+ if (!isGhCliAvailable()) {
62
+ return { merged: null, method: 'gh_unavailable' };
63
+ }
37
64
  let ghResult;
38
65
  try {
39
66
  ghResult = execSync(`gh api repos/:owner/:repo/pulls -q '.[] | select(.head.ref == "${laneBranch}") | .merged'`, { encoding: FILE_SYSTEM.UTF8 }).trim();
40
67
  }
41
68
  catch {
42
- ghResult = '';
69
+ ghResult = STRING_LITERALS.EMPTY;
43
70
  }
44
71
  if (ghResult === 'true') {
45
72
  return { merged: true, method: 'gh_api' };
@@ -47,37 +74,7 @@ async function verifyPRMerged(laneBranch) {
47
74
  if (ghResult === 'false') {
48
75
  return { merged: false, method: 'gh_api' };
49
76
  }
50
- // Fallback: git merge-base --is-ancestor
51
- // Always fetch origin/main first for accurate merge-base check
52
- await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
53
- const localBranchExists = await getGitForCwd().branchExists(laneBranch);
54
- if (!localBranchExists) {
55
- // Branch doesn't exist locally - check if it exists remotely
56
- const remoteBranchExists = await getGitForCwd().raw([
57
- 'ls-remote',
58
- '--heads',
59
- REMOTES.ORIGIN,
60
- laneBranch,
61
- ]);
62
- if (!remoteBranchExists) {
63
- // Branch is gone both locally and remotely - assume merged
64
- return { merged: true, method: 'branch_deleted' };
65
- }
66
- // Branch exists remotely but not locally - need to fetch
67
- await getGitForCwd().fetch(REMOTES.ORIGIN, laneBranch);
68
- }
69
- let isAncestor;
70
- try {
71
- await getGitForCwd().raw(['merge-base', '--is-ancestor', laneBranch, GIT_REFS.ORIGIN_MAIN]);
72
- isAncestor = true;
73
- }
74
- catch {
75
- isAncestor = false;
76
- }
77
- if (isAncestor) {
78
- return { merged: true, method: 'git_merge_base' };
79
- }
80
- return { merged: false, method: 'git_merge_base' };
77
+ return { merged: null, method: 'gh_api' };
81
78
  }
82
79
  async function removeWorktree(worktreePath) {
83
80
  if (!existsSync(worktreePath)) {
@@ -115,14 +112,74 @@ async function deleteBranch(laneBranch) {
115
112
  console.log(`[wu-cleanup] ✓ Remote branch already deleted: ${laneBranch}`);
116
113
  }
117
114
  }
115
+ async function cleanupArtifactsInWorktree() {
116
+ const worktreePath = detectCurrentWorktree();
117
+ if (!worktreePath) {
118
+ die(`${LOG_PREFIX.CLEANUP} Not in a worktree.\n\n` +
119
+ 'Run this command from inside a worktree:\n' +
120
+ ' cd worktrees/<lane>-wu-xxx\n' +
121
+ ' pnpm wu:cleanup --artifacts\n');
122
+ }
123
+ console.log(`${LOG_PREFIX.CLEANUP} Cleaning build artifacts in ${worktreePath}`);
124
+ const result = await cleanupWorktreeBuildArtifacts(worktreePath);
125
+ if (result.removedCount === 0) {
126
+ console.log(`${LOG_PREFIX.CLEANUP} ✓ No build artifacts found`);
127
+ return;
128
+ }
129
+ if (result.distDirectories.length > 0) {
130
+ console.log(`${LOG_PREFIX.CLEANUP} Removed dist directories:`);
131
+ for (const dir of result.distDirectories) {
132
+ console.log(` - ${dir}`);
133
+ }
134
+ }
135
+ if (result.tsbuildinfoFiles.length > 0) {
136
+ console.log(`${LOG_PREFIX.CLEANUP} Removed tsbuildinfo files:`);
137
+ for (const file of result.tsbuildinfoFiles) {
138
+ console.log(` - ${file}`);
139
+ }
140
+ }
141
+ console.log(`${LOG_PREFIX.CLEANUP} ✓ Build artifact cleanup complete`);
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
+ }
118
168
  async function main() {
119
169
  const args = createWUParser({
120
170
  name: 'wu-cleanup',
121
171
  description: 'Clean up worktree and branch after PR merge (PR-based completion workflow)',
122
- options: [WU_OPTIONS.id],
123
- required: ['id'],
172
+ options: [WU_OPTIONS.id, CLEANUP_OPTIONS.artifacts],
173
+ required: [],
124
174
  allowPositionalId: true,
125
175
  });
176
+ if (args.artifacts) {
177
+ await cleanupArtifactsInWorktree();
178
+ return;
179
+ }
180
+ if (!args.id) {
181
+ die(`${LOG_PREFIX.CLEANUP} Missing required --id (or use --artifacts).`);
182
+ }
126
183
  const id = args.id.toUpperCase();
127
184
  const wu = readWU(WU_PATHS.WU(id), id);
128
185
  // Use kebab-case lane naming (match wu-claim.mjs logic)
@@ -135,6 +192,7 @@ async function main() {
135
192
  const idK = args.id.toLowerCase();
136
193
  const laneBranch = `lane/${laneK}/${idK}`;
137
194
  const worktreePath = path.join('worktrees', `${laneK}-${idK}`);
195
+ const absoluteWorktreePath = path.resolve(worktreePath);
138
196
  console.log(`[wu-cleanup] Cleaning up ${args.id} (${wu.title})`);
139
197
  console.log(`[wu-cleanup] Lane: ${wu.lane}`);
140
198
  console.log(`[wu-cleanup] Branch: ${laneBranch}`);
@@ -154,25 +212,38 @@ async function main() {
154
212
  console.error(BOX.BOT);
155
213
  process.exit(EXIT_CODES.ERROR);
156
214
  }
157
- // 1. Verify PR is merged
158
- console.log('[wu-cleanup] Verifying PR merge status...');
159
- const { merged, method } = await verifyPRMerged(laneBranch);
160
- 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) {
161
232
  console.error();
162
233
  console.error(BOX.TOP);
163
- console.error('║ PR NOT MERGED');
234
+ console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.BLOCKED}`);
164
235
  console.error(BOX.MID);
165
- console.error(`║ Branch ${laneBranch} has not been merged to main yet.`);
166
- console.error('║');
167
- console.error('║ Next steps:');
168
- console.error('║ 1. Review the PR in GitHub UI');
169
- console.error('║ 2. Merge the PR');
170
- 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
+ }
171
244
  console.error(BOX.BOT);
172
245
  process.exit(EXIT_CODES.ERROR);
173
246
  }
174
- console.log(`[wu-cleanup] ✓ PR merged (verified via ${method})`);
175
- console.log();
176
247
  // 2. Remove worktree
177
248
  await removeWorktree(worktreePath);
178
249
  console.log();
@@ -189,6 +260,7 @@ async function main() {
189
260
  }
190
261
  // Guard main() for testability (WU-1366)
191
262
  import { fileURLToPath } from 'node:url';
263
+ import { runCLI } from './cli-entry-point.js';
192
264
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
193
- main();
265
+ runCLI(main);
194
266
  }
package/dist/wu-create.js CHANGED
@@ -40,7 +40,7 @@ import { inferSubLane } from '@lumenflow/core/dist/lane-inference.js';
40
40
  import { parseBacklogFrontmatter } from '@lumenflow/core/dist/backlog-parser.js';
41
41
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
42
42
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
43
- import { PLACEHOLDER_SENTINEL, validateReadyWU } from '@lumenflow/core/dist/wu-schema.js';
43
+ import { validateWU } from '@lumenflow/core/dist/wu-schema.js';
44
44
  import { COMMIT_FORMATS, FILE_SYSTEM, STRING_LITERALS, READINESS_UI, } from '@lumenflow/core/dist/wu-constants.js';
45
45
  // WU-1593: Use centralized validateWUIDFormat (DRY)
46
46
  import { ensureOnMain, validateWUIDFormat } from '@lumenflow/core/dist/wu-helpers.js';
@@ -52,6 +52,8 @@ import { validateSpecCompleteness } from '@lumenflow/core/dist/wu-done-validator
52
52
  import { readWU } from '@lumenflow/core/dist/wu-yaml.js';
53
53
  // WU-2253: Import WU spec linter for acceptance/code_paths validation
54
54
  import { lintWUSpec, formatLintErrors } from '@lumenflow/core/dist/wu-lint.js';
55
+ // WU-1025: Import placeholder validator for inline content validation
56
+ import { validateNoPlaceholders, buildPlaceholderErrorMessage, } from '@lumenflow/core/dist/wu-validator.js';
55
57
  /** Log prefix for console output */
56
58
  const LOG_PREFIX = '[wu:create]';
57
59
  /** Micro-worktree operation name */
@@ -175,67 +177,29 @@ function displayReadinessSummary(id) {
175
177
  console.warn(`${LOG_PREFIX} ⚠️ Could not validate readiness: ${err.message}`);
176
178
  }
177
179
  }
178
- /**
179
- * Create WU YAML file in micro-worktree
180
- *
181
- * @param {string} worktreePath - Path to micro-worktree
182
- * @param {string} id - WU ID
183
- * @param {string} lane - WU lane
184
- * @param {string} title - WU title
185
- * @param {string} priority - WU priority
186
- * @param {string} type - WU type
187
- * @param {Object} opts - Additional options
188
- * @returns {string} Relative path to created YAML file
189
- */
190
- function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, opts = {}) {
191
- const wuRelativePath = WU_PATHS.WU(id);
192
- const wuAbsolutePath = join(worktreePath, wuRelativePath);
193
- const wuDir = join(worktreePath, WU_PATHS.WU_DIR());
194
- mkdirSync(wuDir, { recursive: true });
195
- // WU-1428: Use todayISO() for consistent YYYY-MM-DD format (library-first)
196
- const today = todayISO();
197
- // Parse initiative system fields from opts (WU-1247) and assigned_to (WU-1368)
198
- const { initiative, phase, blockedBy, blocks, labels, assignedTo } = opts;
199
- // WU-1364: Parse full spec inline options
200
- const { description: inlineDescription, acceptance: inlineAcceptance, codePaths, testPathsManual, testPathsUnit, testPathsE2e, } = opts;
201
- // WU-1998: Parse exposure field options
202
- const { exposure, userJourney, uiPairingWus } = opts;
203
- // WU-2320: Parse spec_refs option
204
- const { specRefs } = opts;
205
- // Helper to parse comma-separated strings into arrays (DRY)
206
- const parseCommaSeparated = (value) => value
207
- ? value
208
- .split(',')
209
- .map((s) => s.trim())
210
- .filter(Boolean)
211
- : [];
212
- // WU-1364: Build description (inline or placeholder)
213
- const description = inlineDescription
214
- ? inlineDescription
215
- : `${PLACEHOLDER_SENTINEL} Describe the work to be done.\n\nContext: ...\nProblem: ...\nSolution: ...\n`;
216
- // WU-1364: Build acceptance (inline array or placeholder)
217
- const acceptance = inlineAcceptance && inlineAcceptance.length > 0
218
- ? inlineAcceptance
219
- : [
220
- `${PLACEHOLDER_SENTINEL} Define acceptance criteria`,
221
- 'pnpm format, lint, typecheck → PASS',
222
- ];
223
- // WU-1364: Build code_paths from inline flag
180
+ // Helper to parse comma-separated strings into arrays (DRY)
181
+ const parseCommaSeparated = (value) => value
182
+ ? value
183
+ .split(',')
184
+ .map((s) => s.trim())
185
+ .filter(Boolean)
186
+ : [];
187
+ function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
188
+ const { description, acceptance, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
224
189
  const code_paths = parseCommaSeparated(codePaths);
225
- // WU-1364: Build tests object from inline flags
226
190
  const tests = {
227
191
  manual: parseCommaSeparated(testPathsManual),
228
192
  unit: parseCommaSeparated(testPathsUnit),
229
193
  e2e: parseCommaSeparated(testPathsE2e),
230
194
  };
231
- const wuContent = {
195
+ return {
232
196
  id,
233
197
  title,
234
198
  lane,
235
199
  type,
236
200
  status: 'ready',
237
201
  priority,
238
- created: today,
202
+ created,
239
203
  description,
240
204
  acceptance,
241
205
  code_paths,
@@ -245,24 +209,107 @@ function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, o
245
209
  risks: [],
246
210
  notes: '',
247
211
  requires_review: false,
248
- // Initiative system fields - only include if provided (WU-1247)
249
212
  ...(initiative && { initiative }),
250
213
  ...(phase && { phase: parseInt(phase, 10) }),
251
214
  ...(blockedBy && { blocked_by: blockedBy.split(',').map((s) => s.trim()) }),
252
215
  ...(blocks && { blocks: blocks.split(',').map((s) => s.trim()) }),
253
216
  ...(labels && { labels: labels.split(',').map((s) => s.trim()) }),
254
- // WU-1368: Default assigned_to from git config user.email
255
217
  ...(assignedTo && { assigned_to: assignedTo }),
256
- // WU-1998: Exposure field options - only include if provided
257
218
  ...(exposure && { exposure }),
258
219
  ...(userJourney && { user_journey: userJourney }),
259
220
  ...(uiPairingWus && { ui_pairing_wus: parseCommaSeparated(uiPairingWus) }),
260
- // WU-2320: Spec references - only include if provided
261
221
  ...(specRefs && { spec_refs: parseCommaSeparated(specRefs) }),
262
222
  };
263
- // WU-1539: Validate WU structure before writing (fail-fast, allows placeholders)
223
+ }
224
+ export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
225
+ const errors = [];
226
+ const effectiveType = type || DEFAULT_TYPE;
227
+ if (!opts.description) {
228
+ errors.push('--description is required');
229
+ }
230
+ if (!opts.acceptance || opts.acceptance.length === 0) {
231
+ errors.push('--acceptance is required (repeatable)');
232
+ }
233
+ if (!opts.exposure) {
234
+ errors.push('--exposure is required');
235
+ }
236
+ const hasTestPaths = opts.testPathsManual || opts.testPathsUnit || opts.testPathsE2e;
237
+ if (effectiveType !== 'documentation' && effectiveType !== 'process') {
238
+ if (!opts.codePaths) {
239
+ errors.push('--code-paths is required for non-documentation WUs');
240
+ }
241
+ if (!hasTestPaths) {
242
+ errors.push('At least one test path flag is required (--test-paths-manual, --test-paths-unit, or --test-paths-e2e)');
243
+ }
244
+ }
245
+ if (effectiveType === 'feature' && !opts.specRefs) {
246
+ errors.push('--spec-refs is required for type: feature WUs');
247
+ }
248
+ if (errors.length > 0) {
249
+ return { valid: false, errors };
250
+ }
251
+ const placeholderResult = validateNoPlaceholders({
252
+ description: opts.description,
253
+ acceptance: opts.acceptance,
254
+ });
255
+ if (!placeholderResult.valid) {
256
+ return {
257
+ valid: false,
258
+ errors: [buildPlaceholderErrorMessage('wu:create', placeholderResult)],
259
+ };
260
+ }
261
+ const today = todayISO();
262
+ const wuContent = buildWUContent({
263
+ id,
264
+ lane,
265
+ title,
266
+ priority,
267
+ type: effectiveType,
268
+ created: today,
269
+ opts,
270
+ });
271
+ const schemaResult = validateWU(wuContent);
272
+ if (!schemaResult.success) {
273
+ const schemaErrors = schemaResult.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
274
+ return { valid: false, errors: schemaErrors };
275
+ }
276
+ const completeness = validateSpecCompleteness(wuContent, id);
277
+ if (!completeness.valid) {
278
+ return { valid: false, errors: completeness.errors };
279
+ }
280
+ return { valid: true, errors: [] };
281
+ }
282
+ /**
283
+ * Create WU YAML file in micro-worktree
284
+ *
285
+ * @param {string} worktreePath - Path to micro-worktree
286
+ * @param {string} id - WU ID
287
+ * @param {string} lane - WU lane
288
+ * @param {string} title - WU title
289
+ * @param {string} priority - WU priority
290
+ * @param {string} type - WU type
291
+ * @param {Object} opts - Additional options
292
+ * @returns {string} Relative path to created YAML file
293
+ */
294
+ function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, opts = {}) {
295
+ const wuRelativePath = WU_PATHS.WU(id);
296
+ const wuAbsolutePath = join(worktreePath, wuRelativePath);
297
+ const wuDir = join(worktreePath, WU_PATHS.WU_DIR());
298
+ mkdirSync(wuDir, { recursive: true });
299
+ // WU-1428: Use todayISO() for consistent YYYY-MM-DD format (library-first)
300
+ const today = todayISO();
301
+ const wuContent = buildWUContent({
302
+ id,
303
+ lane,
304
+ title,
305
+ priority,
306
+ type,
307
+ created: today,
308
+ opts,
309
+ });
310
+ // WU-1539: Validate WU structure before writing (fail-fast, no placeholders)
264
311
  // WU-1750: Zod transforms normalize embedded newlines in arrays and strings
265
- const validationResult = validateReadyWU(wuContent);
312
+ const validationResult = validateWU(wuContent);
266
313
  if (!validationResult.success) {
267
314
  const errors = validationResult.error.issues
268
315
  .map((issue) => ` • ${issue.path.join('.')}: ${issue.message}`)
@@ -270,6 +317,14 @@ function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, o
270
317
  die(`${LOG_PREFIX} ❌ WU YAML validation failed:\n\n${errors}\n\n` +
271
318
  `Fix the issues above and retry.`);
272
319
  }
320
+ const completenessResult = validateSpecCompleteness(wuContent, id);
321
+ if (!completenessResult.valid) {
322
+ const errorList = completenessResult.errors
323
+ .map((error) => ` • ${error}`)
324
+ .join(STRING_LITERALS.NEWLINE);
325
+ die(`${LOG_PREFIX} ❌ WU SPEC INCOMPLETE:\n\n${errorList}\n\n` +
326
+ `Provide the missing fields and retry.`);
327
+ }
273
328
  // WU-2253: Validate acceptance/code_paths consistency and invariants compliance
274
329
  // This blocks WU creation if acceptance references paths not in code_paths
275
330
  // or if code_paths conflicts with tools/invariants.yml
@@ -429,76 +484,95 @@ async function main() {
429
484
  if (!assignedTo) {
430
485
  console.warn(`${LOG_PREFIX} ⚠️ No assigned_to set - WU will need manual assignment`);
431
486
  }
432
- // WU-1364: Validate spec completeness when --validate is set
433
- if (args.validate) {
434
- const validationErrors = [];
435
- if (!args.description) {
436
- validationErrors.push('--description is required when --validate is set');
437
- }
438
- // acceptance is an array from repeatable flag (empty array if not provided)
439
- if (!args.acceptance || args.acceptance.length === 0) {
440
- validationErrors.push('--acceptance is required when --validate is set (use multiple times)');
441
- }
442
- const hasTestPaths = args.testPathsManual || args.testPathsUnit || args.testPathsE2e;
443
- if (!hasTestPaths) {
444
- validationErrors.push('--validate requires at least one test path flag (--test-paths-manual, --test-paths-unit, or --test-paths-e2e)');
445
- }
446
- // WU-2320: spec_refs required for feature WUs (type defaults to feature)
447
- const effectiveType = args.type || DEFAULT_TYPE;
448
- if (effectiveType === 'feature' && !args.specRefs) {
449
- validationErrors.push('--spec-refs is required for type: feature WUs (link to plan file in docs/04-operations/plans/)');
450
- }
451
- if (validationErrors.length > 0) {
452
- const errorList = validationErrors.map((e) => ` • ${e}`).join(STRING_LITERALS.NEWLINE);
453
- die(`Spec validation failed:\n\n${errorList}\n\nTo create without validation, omit the --validate flag.`);
454
- }
455
- console.log(`${LOG_PREFIX} ✅ Spec validation passed`);
487
+ const createSpecValidation = validateCreateSpec({
488
+ id: args.id,
489
+ lane: args.lane,
490
+ title: args.title,
491
+ priority: args.priority || DEFAULT_PRIORITY,
492
+ type: args.type || DEFAULT_TYPE,
493
+ opts: {
494
+ description: args.description,
495
+ acceptance: args.acceptance,
496
+ codePaths: args.codePaths,
497
+ testPathsManual: args.testPathsManual,
498
+ testPathsUnit: args.testPathsUnit,
499
+ testPathsE2e: args.testPathsE2e,
500
+ exposure: args.exposure,
501
+ userJourney: args.userJourney,
502
+ uiPairingWus: args.uiPairingWus,
503
+ specRefs: args.specRefs,
504
+ initiative: args.initiative,
505
+ phase: args.phase,
506
+ blockedBy: args.blockedBy,
507
+ blocks: args.blocks,
508
+ labels: args.labels,
509
+ assignedTo,
510
+ },
511
+ });
512
+ if (!createSpecValidation.valid) {
513
+ const errorList = createSpecValidation.errors
514
+ .map((error) => ` • ${error}`)
515
+ .join(STRING_LITERALS.NEWLINE);
516
+ die(`${LOG_PREFIX} ❌ Spec validation failed:\n\n${errorList}`);
456
517
  }
518
+ console.log(`${LOG_PREFIX} ✅ Spec validation passed`);
457
519
  // Transaction: micro-worktree isolation (WU-1439)
458
520
  try {
459
521
  const priority = args.priority || DEFAULT_PRIORITY;
460
522
  const type = args.type || DEFAULT_TYPE;
461
- await withMicroWorktree({
462
- operation: OPERATION_NAME,
463
- id: args.id,
464
- logPrefix: LOG_PREFIX,
465
- execute: async ({ worktreePath }) => {
466
- // Create WU YAML in micro-worktree
467
- const wuPath = createWUYamlInWorktree(worktreePath, args.id, args.lane, args.title, priority, type, {
468
- // Initiative system fields (WU-1247)
469
- initiative: args.initiative,
470
- phase: args.phase,
471
- blockedBy: args.blockedBy,
472
- blocks: args.blocks,
473
- labels: args.labels,
474
- // WU-1368: Assigned to
475
- assignedTo,
476
- // WU-1364: Full spec inline options
477
- description: args.description,
478
- acceptance: args.acceptance,
479
- codePaths: args.codePaths,
480
- testPathsManual: args.testPathsManual,
481
- testPathsUnit: args.testPathsUnit,
482
- testPathsE2e: args.testPathsE2e,
483
- // WU-1998: Exposure field options
484
- exposure: args.exposure,
485
- userJourney: args.userJourney,
486
- uiPairingWus: args.uiPairingWus,
487
- // WU-2320: Spec references
488
- specRefs: args.specRefs,
489
- });
490
- // Update backlog.md in micro-worktree
491
- const backlogPath = updateBacklogInWorktree(worktreePath, args.id, args.lane, args.title);
492
- // Build commit message
493
- const shortTitle = truncateTitle(args.title);
494
- const commitMessage = COMMIT_FORMATS.CREATE(args.id, shortTitle);
495
- // Return commit message and files to commit
496
- return {
497
- commitMessage,
498
- files: [wuPath, backlogPath],
499
- };
500
- },
501
- });
523
+ const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
524
+ process.env.LUMENFLOW_WU_TOOL = OPERATION_NAME;
525
+ try {
526
+ await withMicroWorktree({
527
+ operation: OPERATION_NAME,
528
+ id: args.id,
529
+ logPrefix: LOG_PREFIX,
530
+ execute: async ({ worktreePath }) => {
531
+ // Create WU YAML in micro-worktree
532
+ const wuPath = createWUYamlInWorktree(worktreePath, args.id, args.lane, args.title, priority, type, {
533
+ // Initiative system fields (WU-1247)
534
+ initiative: args.initiative,
535
+ phase: args.phase,
536
+ blockedBy: args.blockedBy,
537
+ blocks: args.blocks,
538
+ labels: args.labels,
539
+ // WU-1368: Assigned to
540
+ assignedTo,
541
+ // WU-1364: Full spec inline options
542
+ description: args.description,
543
+ acceptance: args.acceptance,
544
+ codePaths: args.codePaths,
545
+ testPathsManual: args.testPathsManual,
546
+ testPathsUnit: args.testPathsUnit,
547
+ testPathsE2e: args.testPathsE2e,
548
+ // WU-1998: Exposure field options
549
+ exposure: args.exposure,
550
+ userJourney: args.userJourney,
551
+ uiPairingWus: args.uiPairingWus,
552
+ // WU-2320: Spec references
553
+ specRefs: args.specRefs,
554
+ });
555
+ // Update backlog.md in micro-worktree
556
+ const backlogPath = updateBacklogInWorktree(worktreePath, args.id, args.lane, args.title);
557
+ // Build commit message
558
+ const shortTitle = truncateTitle(args.title);
559
+ const commitMessage = COMMIT_FORMATS.CREATE(args.id, shortTitle);
560
+ // Return commit message and files to commit
561
+ return {
562
+ commitMessage,
563
+ files: [wuPath, backlogPath],
564
+ };
565
+ },
566
+ });
567
+ }
568
+ finally {
569
+ if (previousWuTool === undefined) {
570
+ delete process.env.LUMENFLOW_WU_TOOL;
571
+ }
572
+ else {
573
+ process.env.LUMENFLOW_WU_TOOL = previousWuTool;
574
+ }
575
+ }
502
576
  console.log(`\n${LOG_PREFIX} ✅ Transaction complete!`);
503
577
  console.log(`\nWU ${args.id} created successfully:`);
504
578
  console.log(` File: ${WU_PATHS.WU(args.id)}`);
@@ -515,6 +589,7 @@ async function main() {
515
589
  }
516
590
  // Guard main() for testability (WU-1366)
517
591
  import { fileURLToPath } from 'node:url';
592
+ import { runCLI } from './cli-entry-point.js';
518
593
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
519
- main();
594
+ runCLI(main);
520
595
  }