@lumenflow/core 1.0.0 → 1.3.0

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 (65) hide show
  1. package/dist/arg-parser.js +31 -1
  2. package/dist/backlog-generator.js +1 -1
  3. package/dist/backlog-sync-validator.js +3 -3
  4. package/dist/branch-check.d.ts +21 -0
  5. package/dist/branch-check.js +77 -0
  6. package/dist/cli/is-agent-branch.d.ts +11 -0
  7. package/dist/cli/is-agent-branch.js +15 -0
  8. package/dist/code-paths-overlap.js +2 -2
  9. package/dist/error-handler.d.ts +1 -0
  10. package/dist/error-handler.js +4 -1
  11. package/dist/git-adapter.d.ts +16 -0
  12. package/dist/git-adapter.js +23 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +2 -0
  15. package/dist/lane-checker.d.ts +36 -3
  16. package/dist/lane-checker.js +128 -17
  17. package/dist/lane-inference.js +3 -4
  18. package/dist/lumenflow-config-schema.d.ts +125 -0
  19. package/dist/lumenflow-config-schema.js +76 -0
  20. package/dist/orchestration-rules.d.ts +1 -1
  21. package/dist/orchestration-rules.js +2 -2
  22. package/dist/path-classifiers.d.ts +1 -1
  23. package/dist/path-classifiers.js +1 -1
  24. package/dist/rebase-artifact-cleanup.d.ts +17 -0
  25. package/dist/rebase-artifact-cleanup.js +49 -8
  26. package/dist/spawn-strategy.d.ts +53 -0
  27. package/dist/spawn-strategy.js +106 -0
  28. package/dist/stamp-utils.d.ts +10 -0
  29. package/dist/stamp-utils.js +17 -19
  30. package/dist/token-counter.js +2 -2
  31. package/dist/wu-consistency-checker.js +5 -5
  32. package/dist/wu-constants.d.ts +21 -3
  33. package/dist/wu-constants.js +28 -3
  34. package/dist/wu-done-branch-utils.d.ts +10 -0
  35. package/dist/wu-done-branch-utils.js +31 -0
  36. package/dist/wu-done-cleanup.d.ts +8 -0
  37. package/dist/wu-done-cleanup.js +122 -0
  38. package/dist/wu-done-docs-only.d.ts +20 -0
  39. package/dist/wu-done-docs-only.js +65 -0
  40. package/dist/wu-done-errors.d.ts +17 -0
  41. package/dist/wu-done-errors.js +24 -0
  42. package/dist/wu-done-inputs.d.ts +12 -0
  43. package/dist/wu-done-inputs.js +51 -0
  44. package/dist/wu-done-metadata.d.ts +100 -0
  45. package/dist/wu-done-metadata.js +193 -0
  46. package/dist/wu-done-paths.d.ts +69 -0
  47. package/dist/wu-done-paths.js +237 -0
  48. package/dist/wu-done-preflight.d.ts +48 -0
  49. package/dist/wu-done-preflight.js +185 -0
  50. package/dist/wu-done-validation.d.ts +82 -0
  51. package/dist/wu-done-validation.js +340 -0
  52. package/dist/wu-done-validators.d.ts +13 -409
  53. package/dist/wu-done-validators.js +9 -1225
  54. package/dist/wu-done-worktree.d.ts +0 -1
  55. package/dist/wu-done-worktree.js +12 -30
  56. package/dist/wu-schema.js +1 -3
  57. package/dist/wu-spawn-skills.d.ts +19 -0
  58. package/dist/wu-spawn-skills.js +148 -0
  59. package/dist/wu-spawn.d.ts +17 -4
  60. package/dist/wu-spawn.js +99 -176
  61. package/dist/wu-validation.d.ts +1 -0
  62. package/dist/wu-validation.js +21 -1
  63. package/dist/wu-validator.d.ts +51 -0
  64. package/dist/wu-validator.js +108 -0
  65. package/package.json +11 -8
@@ -1,1229 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Validation functions for wu:done workflow
4
- * Extracted from wu-done.mjs (WU-1215 refactoring)
5
- */
6
- /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
7
- import { parseWUArgs } from './arg-parser.js';
8
- import { die, createError, ErrorCodes } from './error-handler.js';
9
- import { WU_PATHS } from './wu-paths.js';
10
- import { EXIT_CODES } from './wu-constants.js';
11
- // WU-1352: Use centralized YAML functions from wu-yaml.mjs
12
- import { readWU, writeWU, parseYAML } from './wu-yaml.js';
13
- import { getGitForCwd } from './git-adapter.js';
14
- import path from 'node:path';
15
- import { existsSync, readFileSync, statSync } from 'node:fs';
16
- import { access } from 'node:fs/promises';
17
- import { exec as execCallback, execSync as execSyncImport } from 'node:child_process';
18
- import { promisify } from 'node:util';
19
- import { updateStatusRemoveInProgress, addToStatusCompleted } from './wu-status-updater.js';
20
- import { moveWUToDoneBacklog } from './wu-backlog-updater.js';
21
- import { createStamp } from './stamp-utils.js';
22
- import { WU_EVENTS_FILE_NAME } from './wu-state-store.js';
23
- import { computeWUYAMLContent, computeStatusContent, computeBacklogContent, computeWUEventsContentAfterComplete, computeStampContent, } from './wu-transaction-collectors.js';
24
- const execAsync = promisify(execCallback);
25
- import { PATTERNS, REMOTES, toKebab, VALIDATION, WU_TYPES, TEST_TYPES, DEFAULTS, LOG_PREFIX, EMOJI, CLAIMED_MODES, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, GIT_COMMANDS, BRANCHES, STRING_LITERALS, BEACON_PATHS, STDIO, } from './wu-constants.js';
26
- import { PLACEHOLDER_SENTINEL } from './wu-schema.js';
27
- // WU-1433: Manual test escape hatch validator
28
- import { validateAutomatedTestRequirement } from './manual-test-validator.js';
29
- // WU-1440: Import merged check for branch deletion
30
- import { isBranchAlreadyMerged } from './wu-done-worktree.js';
31
- // WU-2241: Import cleanup lock for concurrent collision prevention
32
- import { withCleanupLock } from './cleanup-lock.js';
33
- // WU-1805: Import preflight validators for code_paths validation
34
- import { validatePreflight } from './wu-preflight-validators.js';
35
- // WU-2242: Import isDocumentationPath for test_paths enforcement
36
- import { isDocumentationPath } from './file-classifiers.js';
37
- // WU-2278: Import ownership validation for cross-agent protection
38
- import { validateWorktreeOwnership } from './worktree-ownership.js';
39
- // WU-2278: Import cleanup install config for timeout and CI mode
40
- import { getCleanupInstallConfig, CLEANUP_INSTALL_TIMEOUT_MS } from './cleanup-install-config.js';
41
- /**
42
- * Prefixes for paths that qualify as "docs-only" (no code changes).
43
- * Unlike SKIP_TESTS_PREFIXES, this excludes tools/ and scripts/ because
44
- * those contain code files that require full gate validation.
45
- *
46
- * WU-1539: Split from shouldSkipWebTests to fix docs-only misclassification.
47
- * @constant {string[]}
48
- */
49
- const DOCS_ONLY_PREFIXES = Object.freeze(['docs/', 'ai/', '.claude/', 'memory-bank/']);
50
- /**
51
- * Root file patterns that qualify as docs-only.
52
- * @constant {string[]}
53
- */
54
- const DOCS_ONLY_ROOT_FILES = Object.freeze(['readme', 'claude']);
55
- /**
56
- * WU-1234 + WU-1255 + WU-1539: Detect docs-only WU from code_paths
57
- * Returns true if all code_paths are documentation paths only.
58
- *
59
- * Docs-only paths: docs/, ai/, .claude/, memory-bank/, README*, CLAUDE*.md
60
- * NOT docs-only: tools/, scripts/ (these are code, not documentation)
61
- *
62
- * WU-1539: Fixed misclassification where tools/ was treated as docs-only
63
- * but then rejected by validateDocsOnly(). tools/ should skip web tests
64
- * but NOT be classified as docs-only.
65
- *
66
- * @param {string[]|null|undefined} codePaths - Array of file paths from WU YAML
67
- * @returns {boolean} True if WU is docs-only (all paths are documentation)
68
- */
69
- function detectDocsOnlyByPaths(codePaths) {
70
- if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
71
- return false;
72
- }
73
- return codePaths.every((filePath) => {
74
- if (!filePath || typeof filePath !== 'string') {
75
- return false;
76
- }
77
- const path = filePath.trim();
78
- if (path.length === 0) {
79
- return false;
80
- }
81
- // Check docs-only prefixes (docs/, ai/, .claude/, memory-bank/)
82
- for (const prefix of DOCS_ONLY_PREFIXES) {
83
- if (path.startsWith(prefix)) {
84
- return true;
85
- }
86
- }
87
- // Check if it's a markdown file (*.md)
88
- if (path.endsWith('.md')) {
89
- return true;
90
- }
91
- // Check root file patterns (README*, CLAUDE*.md)
92
- const lowerPath = path.toLowerCase();
93
- for (const pattern of DOCS_ONLY_ROOT_FILES) {
94
- if (lowerPath.startsWith(pattern)) {
95
- return true;
96
- }
97
- }
98
- return false;
99
- });
100
- }
101
- /**
102
- * Validates command-line inputs and WU ID format
103
- * @param {string[]} argv - Process arguments
104
- * @returns {{ args: object, id: string }} Parsed args and validated WU ID
105
- */
106
- export function validateInputs(argv) {
107
- const args = parseWUArgs(argv);
108
- if (args.help || !args.id) {
109
- console.log('Usage: pnpm wu:done --id WU-334 [OPTIONS]\n\n' +
110
- 'Options:\n' +
111
- ' --worktree <path> Override worktree path (default: worktrees/<lane>-<wu-id>)\n' +
112
- ' --no-auto Skip auto-updating YAML/backlog/status (you staged manually)\n' +
113
- ' --no-remove Skip worktree removal\n' +
114
- ' --no-merge Skip auto-merging lane branch to main\n' +
115
- ' --delete-branch Delete lane branch after merge (both local and remote)\n' +
116
- ' --create-pr Create PR instead of auto-merge (requires gh CLI)\n' +
117
- ' --pr-draft Create PR as draft (use with --create-pr)\n' +
118
- ' --skip-gates Skip gates check (USE WITH EXTREME CAUTION)\n' +
119
- ' --reason "<text>" Required with --skip-gates or --override-owner\n' +
120
- ' --fix-wu WU-{id} Required with --skip-gates: WU ID that will fix the failures\n' +
121
- ' --allow-todo Allow TODO comments in code (requires justification in WU notes)\n' +
122
- ' --override-owner Override ownership check (requires --reason, audited)\n' +
123
- ' --no-auto-rebase Disable auto-rebase on branch divergence (WU-1303)\n' +
124
- ' --require-agents Block completion if mandatory agents not invoked (WU-1542)\n' +
125
- ' --help, -h Show this help\n\n' +
126
- '⚠️ SKIP-GATES WARNING:\n' +
127
- ' Only use --skip-gates when:\n' +
128
- ' • Test failures are confirmed pre-existing (not introduced by your WU)\n' +
129
- ' • A separate WU exists to fix those failures (specify with --fix-wu)\n' +
130
- ' • Your WU work is genuinely complete\n\n' +
131
- ' NEVER use --skip-gates for failures introduced by your WU!\n' +
132
- ' All skip-gates events are logged to .beacon/skip-gates-audit.log\n\n' +
133
- '📝 WU VALIDATOR:\n' +
134
- ' Automatically scans code_paths for:\n' +
135
- ' • TODO/FIXME/HACK/XXX comments (fails validation unless --allow-todo)\n' +
136
- ' • Mock/Stub/Fake classes in production code (warning only)\n' +
137
- ' Use --allow-todo only for legitimate cases with justification in WU notes.\n');
138
- process.exit(args.help ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
139
- }
140
- const id = args.id.toUpperCase();
141
- if (!PATTERNS.WU_ID.test(id))
142
- die(`Invalid WU id '${args.id}'. Expected format WU-123`);
143
- return { args, id };
144
- }
145
- /**
146
- * Read WU YAML preferring worktree version over main version
147
- *
148
- * WU-1584 Fix #4: Added diagnostic logging to confirm which YAML file is being
149
- * read for code_paths validation. This helps debug issues where worktree YAML
150
- * differs from main checkout YAML.
151
- *
152
- * @param {string} id - WU ID
153
- * @param {string|null} worktreePath - Worktree path (null if branch-only mode)
154
- * @param {string} mainWUPath - Path to WU YAML in main checkout
155
- * @returns {object} Parsed WU document
156
- */
157
- export function readWUPreferWorktree(id, worktreePath, mainWUPath) {
158
- if (worktreePath) {
159
- const wtWUPath = path.join(worktreePath, WU_PATHS.WU(id));
160
- if (existsSync(wtWUPath)) {
161
- try {
162
- const text = readFileSync(wtWUPath, { encoding: 'utf-8' });
163
- const doc = parseYAML(text);
164
- if (doc && doc.id === id) {
165
- // WU-1584: Log source file for validation debugging
166
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Reading WU YAML from worktree: ${wtWUPath}`);
167
- if (doc.code_paths && doc.code_paths.length > 0) {
168
- console.log(`${LOG_PREFIX.DONE} code_paths source: worktree (${doc.code_paths.length} path(s))`);
169
- }
170
- return doc;
171
- }
172
- // If ID mismatch, log warning but continue
173
- console.warn(`${LOG_PREFIX.DONE} Warning: Worktree YAML ID mismatch (expected ${id}, got ${doc?.id})`);
174
- }
175
- catch (err) {
176
- // Log parse errors for debugging
177
- console.warn(`${LOG_PREFIX.DONE} Warning: Failed to read worktree YAML: ${err.message}`);
178
- }
179
- }
180
- else {
181
- // Log missing worktree YAML for debugging
182
- console.warn(`${LOG_PREFIX.DONE} Warning: Worktree YAML not found at ${wtWUPath}`);
183
- }
184
- }
185
- // WU-1584: Log when falling back to main checkout YAML
186
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Reading WU YAML from main: ${mainWUPath}`);
187
- const doc = readWU(mainWUPath, id);
188
- if (doc.code_paths && doc.code_paths.length > 0) {
189
- console.log(`${LOG_PREFIX.DONE} code_paths source: main checkout (${doc.code_paths.length} path(s))`);
190
- }
191
- return doc;
192
- }
193
- /**
194
- * Detect if currently running inside a worktree
195
- * Checks for .git file (not directory) which indicates a worktree
196
- * @returns {string|null} Current directory path if inside worktree, null otherwise
197
- */
198
- export function detectCurrentWorktree() {
199
- const cwd = process.cwd();
200
- const gitPath = path.join(cwd, '.git');
201
- // Check if .git exists and is a file (worktrees have .git file, main has .git directory)
202
- if (!existsSync(gitPath))
203
- return null;
204
- try {
205
- const stats = statSync(gitPath);
206
- if (stats.isFile()) {
207
- // Parse .git file to verify it points to main repo's worktrees
208
- const gitContent = readFileSync(gitPath, { encoding: 'utf-8' });
209
- const match = gitContent.match(/^gitdir:\s*(.+)$/m);
210
- if (match && match[1].includes('.git/worktrees/')) {
211
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.TARGET} Auto-detected worktree from process.cwd(): ${cwd}`);
212
- return cwd;
213
- }
214
- }
215
- }
216
- catch (err) {
217
- // Ignore errors, fall back to calculated path
218
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Failed to detect worktree: ${err.message}`);
219
- }
220
- return null;
221
- }
222
- /**
223
- * Resolve worktree path from WU YAML
224
- * Originally implemented in WU-1226, extracted to validators module in WU-1215
225
- * Priority:
226
- * 1. Read worktree_path field (set at claim time, immune to lane field changes)
227
- * 2. Fall back to calculating from lane field (for old WUs without worktree_path)
228
- * 3. Use git worktree list to find actual path (defensive fallback)
229
- * @param {object} doc - WU YAML document
230
- * @returns {Promise<string|null>} - Worktree path or null if not found
231
- */
232
- export async function defaultWorktreeFrom(doc) {
233
- // Priority 1 - use recorded worktree_path if available
234
- if (doc.worktree_path) {
235
- return doc.worktree_path;
236
- }
237
- // Priority 2 - calculate from current lane field (legacy behavior)
238
- const lane = (doc.lane || '').toString();
239
- const laneK = toKebab(lane);
240
- const idK = (doc.id || '').toLowerCase();
241
- if (!laneK || !idK)
242
- return null;
243
- const calculated = `worktrees/${laneK}-${idK}`;
244
- // Priority 3 - verify calculated path exists, or find actual path via git worktree list
245
- let calculatedExists = true;
246
- try {
247
- await access(calculated);
248
- }
249
- catch {
250
- calculatedExists = false;
251
- }
252
- if (!calculatedExists) {
253
- try {
254
- const worktreeList = await getGitForCwd().worktreeList();
255
- const lines = worktreeList.split(STRING_LITERALS.NEWLINE);
256
- const branch = `lane/${laneK}/${idK}`;
257
- for (let i = 0; i < lines.length; i++) {
258
- if (lines[i].startsWith('branch ') && lines[i].includes(branch)) {
259
- // Found the branch, now get the worktree path from previous line
260
- for (let j = i - 1; j >= 0; j--) {
261
- if (lines[j].startsWith('worktree ')) {
262
- const fullPath = lines[j].substring('worktree '.length);
263
- // Convert absolute path to relative path from repo root
264
- const repoRoot = process.cwd();
265
- const relativePath = path.relative(repoRoot, fullPath);
266
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Worktree path mismatch detected:\n` +
267
- ` Expected: ${calculated}\n` +
268
- ` Actual: ${relativePath}\n` +
269
- ` Using actual path from git worktree list`);
270
- return relativePath;
271
- }
272
- }
273
- }
274
- }
275
- }
276
- catch (e) {
277
- console.warn(`${LOG_PREFIX.DONE} Could not query git worktree list: ${e.message}`);
278
- }
279
- }
280
- return calculated;
281
- }
282
- /**
283
- * Detect workspace mode from WU YAML
284
- * @param {object} doc - WU YAML document
285
- * @returns {'worktree' | 'branch-only'}
286
- */
287
- export function detectWorkspaceMode(doc) {
288
- // Explicit mode field takes precedence
289
- if (doc.claimed_mode === CLAIMED_MODES.BRANCH_ONLY)
290
- return CLAIMED_MODES.BRANCH_ONLY;
291
- if (doc.claimed_mode === CLAIMED_MODES.WORKTREE)
292
- return CLAIMED_MODES.WORKTREE;
293
- // Backward compatibility: if claimed_mode is missing, assume worktree mode
294
- // (all WUs claimed before WU-510 used worktree mode)
295
- return CLAIMED_MODES.WORKTREE;
296
- }
297
- /**
298
- * Calculate lane branch name from WU YAML
299
- * @param {object} doc - WU YAML document
300
- * @returns {string|null} Lane branch name (e.g., lane/operations-tooling/wu-1215)
301
- */
302
- export function defaultBranchFrom(doc) {
303
- const lane = (doc.lane || '').toString();
304
- const laneK = toKebab(lane);
305
- const idK = (doc.id || '').toLowerCase();
306
- if (!laneK || !idK)
307
- return null;
308
- return `lane/${laneK}/${idK}`;
309
- }
310
- /**
311
- * Check if a branch exists
312
- * @param {string} branch - Branch name to check
313
- * @returns {Promise<boolean>} True if branch exists
314
- */
315
- export async function branchExists(branch) {
316
- return await getGitForCwd().branchExists(branch);
317
- }
318
- /**
319
- * Detect workspace mode and calculate all relevant paths
320
- * @param {string} id - WU ID
321
- * @param {object} args - Parsed command-line arguments
322
- * @returns {Promise<object>} Object containing paths, mode info, and WU document
323
- */
324
- export async function detectModeAndPaths(id, args) {
325
- const WU_PATH = WU_PATHS.WU(id);
326
- const STATUS_PATH = WU_PATHS.STATUS();
327
- const BACKLOG_PATH = WU_PATHS.BACKLOG();
328
- const STAMPS_DIR = WU_PATHS.STAMPS_DIR();
329
- // Read WU YAML to detect workspace mode
330
- let docMain = readWU(WU_PATH, id);
331
- const workspaceMode = detectWorkspaceMode(docMain);
332
- const isBranchOnly = workspaceMode === CLAIMED_MODES.BRANCH_ONLY;
333
- console.log(`\n${LOG_PREFIX.DONE} Detected workspace mode: ${workspaceMode}`);
334
- // Determine candidate worktree path early (only relevant for Worktree mode)
335
- // Priority: 1) Auto-detect from cwd 2) Explicit --worktree arg 3) Calculate from YAML
336
- const detectedWorktree = detectCurrentWorktree();
337
- const worktreePathGuess = args.worktree || null;
338
- // For Worktree mode: prefer auto-detected worktree, then explicit arg, then calculated path
339
- // For Branch-Only mode: use main checkout version (no worktree exists)
340
- const derivedWorktree = isBranchOnly
341
- ? null
342
- : detectedWorktree || worktreePathGuess || (await defaultWorktreeFrom(docMain));
343
- if (!isBranchOnly && derivedWorktree && !detectedWorktree) {
344
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.FOLDER} Calculated worktree path from YAML: ${derivedWorktree}`);
345
- }
346
- // Read the actual WU YAML for validation (prefer worktree version over main)
347
- const docForValidation = isBranchOnly
348
- ? docMain
349
- : readWUPreferWorktree(id, derivedWorktree, WU_PATH);
350
- // WU-1234: Detect docs-only by type OR by code_paths
351
- // Auto-detect if all code_paths are under docs/, ai/, .claude/, or are README/CLAUDE files
352
- const isDocsOnlyByType = docForValidation.type === 'documentation';
353
- const isDocsOnlyByPaths = detectDocsOnlyByPaths(docForValidation.code_paths);
354
- const isDocsOnly = isDocsOnlyByType || isDocsOnlyByPaths;
355
- if (isDocsOnlyByPaths && !isDocsOnlyByType) {
356
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Auto-detected docs-only WU from code_paths (type: ${docForValidation.type || 'unset'})`);
357
- }
358
- return {
359
- WU_PATH,
360
- STATUS_PATH,
361
- BACKLOG_PATH,
362
- STAMPS_DIR,
363
- docMain,
364
- workspaceMode,
365
- isBranchOnly,
366
- derivedWorktree,
367
- docForValidation,
368
- isDocsOnly,
369
- };
370
- }
371
- /**
372
- * Generate commit message for WU completion
373
- * Extracted from wu-done.mjs (WU-1215 Phase 2 Extraction #1 Helper)
374
- * @param {string} id - WU ID (e.g., "WU-1215")
375
- * @param {string} title - WU title
376
- * @param {number} maxLength - Maximum commit header length from commitlint config
377
- * @returns {string} Formatted commit message
378
- * @throws {Error} If generated message exceeds maxLength
379
- */
380
- export function generateCommitMessage(id, title, maxLength = DEFAULTS.MAX_COMMIT_SUBJECT) {
381
- const prefix = `wu(${id.toLowerCase()}): done - `;
382
- const safe = String(title).trim().toLowerCase().replace(/\s+/g, ' ');
383
- const room = Math.max(0, maxLength - prefix.length);
384
- const short = safe.length > room ? `${safe.slice(0, room - 1)}…` : safe;
385
- const msg = `${prefix}${short}`;
386
- if (msg.length > maxLength) {
387
- const error = new Error(`Commit message too long (${msg.length}/${maxLength}).\n` +
388
- `Fix: Shorten WU title\n` +
389
- `Current title: "${title}" (${title.length} chars)\n` +
390
- `Suggested max: ~${maxLength - prefix.length} chars`);
391
- error.code = 'COMMIT_MESSAGE_TOO_LONG';
392
- error.data = {
393
- title,
394
- titleLength: title.length,
395
- messageLength: msg.length,
396
- maxLength,
397
- suggestedMax: maxLength - prefix.length,
398
- };
399
- throw error;
400
- }
401
- return msg;
402
- }
403
- /**
404
- * Validate that required metadata files exist before updating
405
- * WU-1275: Fail fast before mutations to prevent partial state
406
- *
407
- * @param {object} params - Parameters object
408
- * @param {string} params.statusPath - Path to status.md file
409
- * @param {string} params.backlogPath - Path to backlog.md file
410
- * @throws {WUError} If any required file is missing
411
- */
412
- export function validateMetadataFilesExist({ statusPath, backlogPath }) {
413
- const missing = [];
414
- if (!existsSync(statusPath)) {
415
- missing.push(`Status: ${statusPath}`);
416
- }
417
- if (!existsSync(backlogPath)) {
418
- missing.push(`Backlog: ${backlogPath}`);
419
- }
420
- if (missing.length > 0) {
421
- throw createError(ErrorCodes.FILE_NOT_FOUND, `Required metadata files missing:\n ${missing.join('\n ')}\n\nCannot complete WU - verify worktree has latest metadata files.`, { missingFiles: missing });
422
- }
423
- }
424
- /**
425
- * Update all metadata files for WU completion
426
- * Extracted from wu-done.mjs (WU-1215 Phase 2 Extraction #1 Helper)
427
- * WU-1572: Made async for WUStateStore integration
428
- * @param {object} params - Parameters object
429
- * @param {string} params.id - WU ID
430
- * @param {string} params.title - WU title
431
- * @param {object} params.doc - WU YAML document to update
432
- * @param {string} params.wuPath - Path to WU YAML file
433
- * @param {string} params.statusPath - Path to status.md file
434
- * @param {string} params.backlogPath - Path to backlog.md file
435
- */
436
- export async function updateMetadataFiles({ id, title, doc, wuPath, statusPath, backlogPath }) {
437
- // WU-1275: Fail fast before any mutations
438
- validateMetadataFilesExist({ statusPath, backlogPath });
439
- // Update WU YAML (mark as done, lock, set completion timestamp)
440
- doc.status = 'done';
441
- doc.locked = true;
442
- doc.completed_at = new Date().toISOString();
443
- writeWU(wuPath, doc);
444
- // Update status.md (remove from In Progress, add to Completed)
445
- updateStatusRemoveInProgress(statusPath, id);
446
- addToStatusCompleted(statusPath, id, title);
447
- // Update backlog.md (move to Done section)
448
- // WU-1572: Now async for state store integration
449
- await moveWUToDoneBacklog(backlogPath, id, title);
450
- // Create completion stamp
451
- createStamp({ id, title });
452
- }
453
- /**
454
- * Collect metadata updates to a transaction (WU-1369: Atomic pattern)
455
- *
456
- * This is the atomic version of updateMetadataFiles.
457
- * Instead of writing files immediately, it collects all changes
458
- * into a WUTransaction object for atomic commit.
459
- *
460
- * Usage:
461
- * ```js
462
- * const tx = new WUTransaction(id);
463
- * collectMetadataToTransaction({ id, title, doc, wuPath, statusPath, backlogPath, stampPath, transaction: tx });
464
- * // All changes are now in tx.pendingWrites
465
- * // Validate, then commit or abort
466
- * tx.commit();
467
- * ```
468
- *
469
- * @param {object} params - Parameters object
470
- * @param {string} params.id - WU ID
471
- * @param {string} params.title - WU title
472
- * @param {object} params.doc - WU YAML document to update (will be mutated)
473
- * @param {string} params.wuPath - Path to WU YAML file
474
- * @param {string} params.statusPath - Path to status.md file
475
- * @param {string} params.backlogPath - Path to backlog.md file
476
- * @param {string} params.stampPath - Path to stamp file
477
- * @param {WUTransaction} params.transaction - Transaction to add writes to
478
- */
479
- // WU-1574: Made async for computeBacklogContent
480
- export async function collectMetadataToTransaction({ id, title, doc, wuPath, statusPath, backlogPath, stampPath, transaction, }) {
481
- // WU-1369: Fail fast before any computations
482
- validateMetadataFilesExist({ statusPath, backlogPath });
483
- // Compute WU YAML content (mutates doc, returns YAML string)
484
- const wuYAMLContent = computeWUYAMLContent(doc);
485
- transaction.addWrite(wuPath, wuYAMLContent, 'WU YAML');
486
- // Compute status.md content
487
- const statusContent = computeStatusContent(statusPath, id, title);
488
- transaction.addWrite(statusPath, statusContent, 'status.md');
489
- // Compute backlog.md content (WU-1574: now async)
490
- const backlogContent = await computeBacklogContent(backlogPath, id, title);
491
- transaction.addWrite(backlogPath, backlogContent, 'backlog.md');
492
- const wuEventsUpdate = await computeWUEventsContentAfterComplete(backlogPath, id);
493
- if (wuEventsUpdate) {
494
- transaction.addWrite(wuEventsUpdate.eventsPath, wuEventsUpdate.content, 'wu-events.jsonl');
495
- }
496
- // Compute stamp content
497
- const stampContent = computeStampContent(id, title);
498
- transaction.addWrite(stampPath, stampContent, 'completion stamp');
499
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Collected ${transaction.size} metadata updates for atomic commit`);
500
- }
501
- /**
502
- * Stage and format metadata files
503
- * Extracted from wu-done.mjs (WU-1215 Phase 2 Extraction #1 Helper)
504
- * @param {object} params - Parameters object
505
- * @param {string} params.id - WU ID (for error reporting)
506
- * @param {string} params.wuPath - Path to WU YAML file
507
- * @param {string} params.statusPath - Path to status.md file
508
- * @param {string} params.backlogPath - Path to backlog.md file
509
- * @param {string} params.stampsDir - Path to stamps directory
510
- * @throws {Error} If formatting fails
511
- */
512
- export async function stageAndFormatMetadata({ id, wuPath, statusPath, backlogPath, stampsDir }) {
513
- // WU-1235: Use getGitForCwd() to capture current directory (worktree after chdir)
514
- // The singleton git adapter captures cwd at import time, which is wrong after process.chdir()
515
- const gitCwd = getGitForCwd();
516
- // Stage files
517
- const wuEventsPath = path.join(BEACON_PATHS.STATE_DIR, WU_EVENTS_FILE_NAME);
518
- const filesToStage = [wuPath, statusPath, backlogPath, stampsDir];
519
- if (existsSync(wuEventsPath)) {
520
- filesToStage.push(wuEventsPath);
521
- }
522
- await gitCwd.add(filesToStage);
523
- // Format documentation
524
- console.log(`${LOG_PREFIX.DONE} Formatting auto-generated documentation...`);
525
- try {
526
- const prettierCmd = `${PKG_MANAGER} ${SCRIPTS.PRETTIER} ${PRETTIER_FLAGS.WRITE} "${wuPath}" "${statusPath}" "${backlogPath}"`;
527
- await execAsync(prettierCmd);
528
- await gitCwd.add([wuPath, statusPath, backlogPath]);
529
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Documentation formatted`);
530
- }
531
- catch (err) {
532
- throw createError(ErrorCodes.VALIDATION_ERROR, `Failed to format documentation: ${err.message}`, { wuId: id, error: err.message });
533
- }
534
- }
535
- /**
536
- * Run cleanup operations after successful merge
537
- * Removes worktree and optionally deletes lane branch
538
- * Extracted from wu-done.mjs (WU-1215 Phase 1 Extraction #3)
539
- *
540
- * WU-2241: Now wrapped with cleanup lock to prevent concurrent collision
541
- * when multiple wu:done commands complete simultaneously.
542
- *
543
- * @param {object} docMain - WU YAML document
544
- * @param {object} args - Parsed CLI arguments
545
- */
546
- export async function runCleanup(docMain, args) {
547
- const wuId = docMain.id;
548
- const worktreePath = args.worktree || (await defaultWorktreeFrom(docMain));
549
- // WU-2278: Validate worktree ownership before cleanup
550
- // Prevents cross-agent worktree deletion
551
- if (!args.overrideOwner) {
552
- const ownershipResult = validateWorktreeOwnership({ worktreePath, wuId });
553
- if (!ownershipResult.valid) {
554
- throw createError(ErrorCodes.VALIDATION_ERROR, `${ownershipResult.error}\n\nTo override (DANGEROUS): pnpm wu:done --id ${wuId} --override-owner --reason "explanation"`, { wuId, worktreePath, error: ownershipResult.error });
555
- }
556
- }
557
- // WU-2241: Wrap cleanup operations in cleanup lock to prevent concurrent collision
558
- await withCleanupLock(wuId, async () => {
559
- await runCleanupInternal(docMain, args, worktreePath);
560
- }, { worktreePath });
561
- }
562
- /**
563
- * Internal cleanup implementation (runs under cleanup lock)
564
- *
565
- * @param {object} docMain - WU YAML document
566
- * @param {object} args - Parsed CLI arguments
567
- * @param {string|null} worktreePath - Path to worktree
568
- */
569
- async function runCleanupInternal(docMain, args, worktreePath) {
570
- // Step 6: Remove worktree (runs even if commit/push failed)
571
- // Skip removal in PR mode (worktree needed for cleanup after PR merge)
572
- const claimedMode = docMain.claimed_mode || CLAIMED_MODES.WORKTREE;
573
- const requiresReview = docMain.requires_review === true;
574
- const prModeEnabled = claimedMode === CLAIMED_MODES.WORKTREE_PR || args.createPR || requiresReview;
575
- // WU-2241: Track branch for cleanup after worktree removal
576
- const laneBranch = await defaultBranchFrom(docMain);
577
- if (!args.noRemove && !prModeEnabled) {
578
- if (worktreePath && existsSync(worktreePath)) {
579
- try {
580
- await getGitForCwd().worktreeRemove(worktreePath, { force: true });
581
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Removed worktree ${worktreePath}`);
582
- // WU-2241: Delete branch AFTER worktree removal (correct ordering)
583
- // This ensures we don't leave orphan branches when worktree is removed
584
- if (laneBranch && (await branchExists(laneBranch))) {
585
- await deleteBranchWithCleanup(laneBranch);
586
- }
587
- // WU-1743: Re-run pnpm install to fix broken symlinks
588
- // When pnpm install runs in a worktree, it may create symlinks with absolute paths
589
- // to the worktree. After worktree removal, these symlinks break.
590
- // Re-running pnpm install regenerates them with correct paths.
591
- // WU-2278: Use timeout and CI=true to prevent hangs
592
- console.log(`${LOG_PREFIX.DONE} Reinstalling dependencies to fix symlinks...`);
593
- try {
594
- const installConfig = getCleanupInstallConfig();
595
- await execAsync(installConfig.command, {
596
- timeout: installConfig.timeout,
597
- env: installConfig.env,
598
- });
599
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Dependencies reinstalled`);
600
- }
601
- catch (installErr) {
602
- // Non-fatal: warn but don't fail wu:done
603
- // WU-2278: Include timeout info in error message
604
- const isTimeout = installErr.killed || installErr.signal === 'SIGTERM';
605
- const errorMsg = isTimeout
606
- ? `pnpm install timed out after ${CLEANUP_INSTALL_TIMEOUT_MS / 1000}s`
607
- : `pnpm install failed: ${installErr.message}`;
608
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} ${errorMsg}`);
609
- }
610
- }
611
- catch (e) {
612
- console.warn(`${LOG_PREFIX.DONE} Could not remove worktree ${worktreePath}: ${e.message}`);
613
- }
614
- }
615
- else {
616
- console.log(`${LOG_PREFIX.DONE} Worktree not found; skipping removal`);
617
- // WU-2241: Still cleanup branch if worktree doesn't exist (orphan branch scenario)
618
- if (!prModeEnabled && laneBranch && (await branchExists(laneBranch))) {
619
- await deleteBranchWithCleanup(laneBranch);
620
- }
621
- }
622
- }
623
- else if (prModeEnabled) {
624
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Worktree preserved (PR mode - run wu:cleanup after PR merge)`);
625
- }
626
- }
627
- /**
628
- * WU-2241: Delete both local and remote branch with proper error handling
629
- *
630
- * @param {string} laneBranch - Branch name to delete
631
- */
632
- async function deleteBranchWithCleanup(laneBranch) {
633
- const gitAdapter = getGitForCwd();
634
- // WU-1440: Check if branch is merged before deletion
635
- // Use -D (force) when confirmed merged to handle rebased branches
636
- const isMerged = await isBranchAlreadyMerged(laneBranch);
637
- try {
638
- await gitAdapter.deleteBranch(laneBranch, { force: isMerged });
639
- const modeIndicator = isMerged ? ' (force: merged)' : '';
640
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Deleted local branch ${laneBranch}${modeIndicator}`);
641
- // Also delete remote if it exists
642
- try {
643
- await gitAdapter.raw(['push', REMOTES.ORIGIN, '--delete', laneBranch]);
644
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Deleted remote branch ${laneBranch}`);
645
- }
646
- catch (e) {
647
- // WU-2241: Non-fatal - remote branch may already be deleted or never existed
648
- console.warn(`${LOG_PREFIX.DONE} Could not delete remote branch: ${e.message}`);
649
- }
650
- }
651
- catch (e) {
652
- console.warn(`${LOG_PREFIX.DONE} Could not delete branch ${laneBranch}: ${e.message}`);
653
- }
654
- }
655
- export async function validateCodePathsExist(doc, id, options = {}) {
656
- const { targetBranch = BRANCHES.MAIN, worktreePath = null } = options;
657
- const errors = [];
658
- const missing = [];
659
- const codePaths = doc.code_paths || [];
660
- // Skip validation for WUs without code_paths (docs-only, process WUs)
661
- if (codePaths.length === 0) {
662
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No code_paths to validate for ${id}`);
663
- return { valid: true, errors: [], missing: [] };
664
- }
665
- console.log(`${LOG_PREFIX.DONE} Validating ${codePaths.length} code_paths exist...`);
666
- // For worktree mode, check files exist in the worktree (will be merged)
667
- // For branch-only mode or post-merge validation, check files exist on target branch
668
- if (worktreePath && existsSync(worktreePath)) {
669
- // Worktree mode: validate files exist in worktree
670
- for (const filePath of codePaths) {
671
- const fullPath = path.join(worktreePath, filePath);
672
- if (!existsSync(fullPath)) {
673
- missing.push(filePath);
674
- }
675
- }
676
- if (missing.length > 0) {
677
- errors.push(`code_paths validation failed - ${missing.length} file(s) not found in worktree:\n${missing
678
- .map((p) => ` - ${p}`)
679
- .join(STRING_LITERALS.NEWLINE)}\n\nEnsure all files listed in code_paths exist before running wu:done.`);
680
- }
681
- }
682
- else {
683
- // Branch-only or post-merge: use git ls-tree to check files on target branch
684
- try {
685
- const gitAdapter = getGitForCwd();
686
- for (const filePath of codePaths) {
687
- try {
688
- // git ls-tree returns empty for non-existent files
689
- const result = await gitAdapter.raw([GIT_COMMANDS.LS_TREE, targetBranch, '--', filePath]);
690
- if (!result || result.trim() === '') {
691
- missing.push(filePath);
692
- }
693
- }
694
- catch {
695
- // git ls-tree fails for non-existent paths
696
- missing.push(filePath);
697
- }
698
- }
699
- if (missing.length > 0) {
700
- errors.push(`code_paths validation failed - ${missing.length} file(s) not found on ${targetBranch}:\n${missing
701
- .map((p) => ` - ${p}`)
702
- .join(STRING_LITERALS.NEWLINE)}\n\n❌ POTENTIAL FALSE COMPLETION DETECTED\n\n` +
703
- `These files are listed in code_paths but do not exist on ${targetBranch}.\n` +
704
- `This prevents creating a stamp for incomplete work.\n\n` +
705
- `Fix options:\n` +
706
- ` 1. Ensure all code is committed and merged to ${targetBranch}\n` +
707
- ` 2. Update code_paths in ${id}.yaml to match actual files\n` +
708
- ` 3. Remove files that were intentionally not created\n\n` +
709
- `Context: WU-1351 prevents false completions from INIT-WORKFLOW-INTEGRITY`);
710
- }
711
- }
712
- catch (err) {
713
- // Non-fatal: warn but don't block if git command fails
714
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not validate code_paths: ${err.message}`);
715
- return { valid: true, errors: [], missing: [] };
716
- }
717
- }
718
- if (errors.length > 0) {
719
- return { valid: false, errors, missing };
720
- }
721
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All ${codePaths.length} code_paths verified`);
722
- return { valid: true, errors: [], missing: [] };
723
- }
724
- /**
725
- * Validate WU spec completeness (WU-1162, WU-1280)
726
- *
727
- * Ensures WU specifications are complete before allowing wu:done to proceed.
728
- * Prevents placeholder WUs from being marked as done.
729
- *
730
- * WU-1280: Added tests array validation to catch empty tests.manual early
731
- * (previously only validated in pre-commit hook, causing late failures).
732
- *
733
- * @param {object} doc - WU YAML document
734
- * @param {string} id - WU ID
735
- * @returns {{ valid: boolean, errors: string[] }} Validation result
736
- */
737
- export function validateSpecCompleteness(doc, _id) {
738
- const errors = [];
739
- // Check for placeholder text in description
740
- if (doc.description && doc.description.includes(PLACEHOLDER_SENTINEL)) {
741
- errors.push(`Description contains ${PLACEHOLDER_SENTINEL} marker`);
742
- }
743
- // Handle both array and object formats for acceptance criteria
744
- if (doc.acceptance) {
745
- const hasPlaceholder = (value) => {
746
- if (typeof value === 'string') {
747
- return value.includes(PLACEHOLDER_SENTINEL);
748
- }
749
- if (Array.isArray(value)) {
750
- return value.some((item) => hasPlaceholder(item));
751
- }
752
- if (typeof value === 'object' && value !== null) {
753
- return Object.values(value).some((item) => hasPlaceholder(item));
754
- }
755
- return false;
756
- };
757
- if (hasPlaceholder(doc.acceptance)) {
758
- errors.push(`Acceptance criteria contain ${PLACEHOLDER_SENTINEL} markers`);
759
- }
760
- }
761
- // Check minimum description length
762
- // WU-1281: Using centralized constant from wu-constants.mjs
763
- if (!doc.description || doc.description.trim().length < VALIDATION.MIN_DESCRIPTION_LENGTH) {
764
- errors.push(`Description too short (${doc.description?.trim().length || 0} chars, minimum ${VALIDATION.MIN_DESCRIPTION_LENGTH})`);
765
- }
766
- // Check code_paths for non-documentation WUs
767
- // WU-1281: Using centralized type constants from wu-constants.mjs
768
- if (doc.type !== WU_TYPES.DOCUMENTATION && doc.type !== WU_TYPES.PROCESS) {
769
- if (!doc.code_paths || doc.code_paths.length === 0) {
770
- errors.push('Code paths required for non-documentation WUs');
771
- }
772
- // WU-1280: Check tests array for non-documentation WUs
773
- // Support both tests: (current) and test_paths: (legacy)
774
- const testObj = doc.tests || doc.test_paths || {};
775
- // Helper to check if array has items
776
- const hasItems = (arr) => Array.isArray(arr) && arr.length > 0;
777
- // WU-1281: Using centralized test type constants from wu-constants.mjs
778
- const hasUnitTests = hasItems(testObj[TEST_TYPES.UNIT]);
779
- const hasE2ETests = hasItems(testObj[TEST_TYPES.E2E]);
780
- const hasManualTests = hasItems(testObj[TEST_TYPES.MANUAL]);
781
- const hasIntegrationTests = hasItems(testObj[TEST_TYPES.INTEGRATION]);
782
- if (!(hasUnitTests || hasE2ETests || hasManualTests || hasIntegrationTests)) {
783
- errors.push('At least one test path required (unit, e2e, integration, or manual)');
784
- }
785
- // WU-2332: Require automated tests for code file changes
786
- // Manual-only tests are not sufficient when code_paths contain actual code files
787
- const automatedTestResult = validateAutomatedTestRequirement(doc);
788
- if (!automatedTestResult.valid) {
789
- errors.push(...automatedTestResult.errors);
790
- }
791
- }
792
- return { valid: errors.length === 0, errors };
793
- }
794
- // WU-1433: Re-export manual test validator for use in wu:done workflow
795
- export { validateAutomatedTestRequirement };
796
- /**
797
- * WU-1617: Post-mutation validation for wu:done
798
- *
799
- * Validates that metadata files written by tx.commit() are valid:
800
- * 1. WU YAML has completed_at field with valid ISO datetime
801
- * 2. WU YAML has locked: true
802
- * 3. Stamp file exists
803
- *
804
- * This catches schema violations that could persist silently after
805
- * transaction commit.
806
- *
807
- * @param {object} params - Validation parameters
808
- * @param {string} params.id - WU ID
809
- * @param {string} params.wuPath - Path to WU YAML file
810
- * @param {string} params.stampPath - Path to stamp file
811
- * @returns {{ valid: boolean, errors: string[] }} Validation result
812
- */
813
- export function validatePostMutation({ id, wuPath, stampPath }) {
814
- const errors = [];
815
- // Check stamp file exists
816
- if (!existsSync(stampPath)) {
817
- errors.push(`Stamp file not created: ${stampPath}`);
818
- }
819
- // Read and validate WU YAML after mutation
820
- if (!existsSync(wuPath)) {
821
- errors.push(`WU YAML not found after mutation: ${wuPath}`);
822
- return { valid: false, errors };
823
- }
824
- try {
825
- const content = readFileSync(wuPath, { encoding: 'utf-8' });
826
- const doc = parseYAML(content);
827
- // Verify completed_at exists and is valid ISO datetime
828
- if (!doc.completed_at) {
829
- errors.push(`Missing required field 'completed_at' in ${id}.yaml`);
830
- }
831
- else {
832
- // Validate ISO datetime format (YYYY-MM-DDTHH:mm:ss.sssZ or similar)
833
- const timestamp = new Date(doc.completed_at);
834
- if (isNaN(timestamp.getTime())) {
835
- errors.push(`Invalid completed_at timestamp: ${doc.completed_at}`);
836
- }
837
- }
838
- // Verify locked is true
839
- if (doc.locked !== true) {
840
- errors.push(`Missing or invalid 'locked' field in ${id}.yaml (expected: true, got: ${doc.locked})`);
841
- }
842
- // Verify status is done
843
- if (doc.status !== 'done') {
844
- errors.push(`Invalid status in ${id}.yaml (expected: 'done', got: '${doc.status}')`);
845
- }
846
- }
847
- catch (err) {
848
- errors.push(`Failed to parse WU YAML after mutation: ${err.message}`);
849
- }
850
- return { valid: errors.length === 0, errors };
851
- }
852
- /**
853
- * WU-1781: Build preflight error message with actionable guidance
854
- *
855
- * Creates a formatted error message for preflight validation failures,
856
- * including specific guidance for stamp-status mismatch errors.
857
- *
858
- * @param {string} id - WU ID being completed
859
- * @param {string[]} errors - List of validation errors
860
- * @returns {string} Formatted error message with fix options
861
- */
862
- export function buildPreflightErrorMessage(id, errors) {
863
- const hasStampStatusError = errors.some((e) => e.includes('stamp but status is not done'));
864
- let message = `
865
- ❌ PREFLIGHT VALIDATION FAILED
866
-
867
- tasks:validate found errors that would block pre-push hooks.
868
- Aborting wu:done BEFORE any merge operations to prevent deadlocks.
869
-
870
- Errors:
871
- ${errors.map((e) => ` - ${e}`).join('\n')}
872
-
873
- Fix options:
874
- `;
875
- if (hasStampStatusError) {
876
- message += `
877
- For stamp-status mismatch errors:
878
- 1. Fix the WU status to match the stamp (set status: done, locked: true)
879
- 2. Or add the WU ID to .lumenflow.config.yaml > exemptions > stamp_status_mismatch
880
-
881
- `;
882
- }
883
- message += `
884
- General fixes:
885
- 1. Run: pnpm tasks:validate to see full errors
886
- 2. Fix the validation errors
887
- 3. Retry: pnpm wu:done --id ${id}
888
-
889
- This preflight check prevents wu:done from leaving main in a stuck state
890
- where husky pre-push would block all further operations.
891
- `;
892
- return message;
893
- }
894
- export async function executePreflightCodePathValidation(id, paths, options = {}) {
895
- // Use injected validator for testability, default to actual implementation
896
- const validatePreflightFn = options.validatePreflightFn || validatePreflight;
897
- console.log(`\n${LOG_PREFIX.DONE} 🔍 Preflight: validating code_paths and test paths...`);
898
- const result = await validatePreflightFn(id, {
899
- rootDir: paths.rootDir,
900
- worktreePath: paths.worktreePath,
901
- });
902
- if (result.valid) {
903
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Preflight code_paths validation passed`);
904
- return {
905
- valid: true,
906
- errors: [],
907
- missingCodePaths: [],
908
- missingTestPaths: [],
909
- abortedBeforeGates: false,
910
- };
911
- }
912
- console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Preflight code_paths validation failed`);
913
- return {
914
- valid: false,
915
- errors: result.errors,
916
- missingCodePaths: result.missingCodePaths || [],
917
- missingTestPaths: result.missingTestPaths || [],
918
- abortedBeforeGates: true,
919
- };
920
- }
921
- /**
922
- * WU-1805: Build preflight code_paths error message with actionable guidance
923
- *
924
- * Creates a formatted error message for preflight code_paths validation failures,
925
- * including specific guidance for fixing missing files.
926
- *
927
- * @param {string} id - WU ID being completed
928
- * @param {object} result - Preflight validation result
929
- * @param {string[]} result.errors - List of validation errors
930
- * @param {string[]} result.missingCodePaths - Missing code_paths files
931
- * @param {string[]} result.missingTestPaths - Missing test files
932
- * @returns {string} Formatted error message with fix options
933
- */
934
- export function buildPreflightCodePathErrorMessage(id, result) {
935
- const { errors, missingCodePaths = [], missingTestPaths = [] } = result;
936
- let message = `
937
- ❌ PREFLIGHT CODE_PATHS VALIDATION FAILED
938
-
939
- code_paths/test_paths validation found errors that would cause gates to fail.
940
- Aborting wu:done BEFORE running gates to save time.
941
-
942
- Errors:
943
- ${errors.map((e) => ` ${e}`).join('\n')}
944
-
945
- `;
946
- if (missingCodePaths.length > 0) {
947
- message += `
948
- Fix options for missing code_paths:
949
- 1. Create the missing files in your worktree
950
- 2. Update code_paths in ${id}.yaml using: pnpm wu:edit --id ${id} --code-paths "<corrected-paths>"
951
- 3. Remove paths that were intentionally not created
952
-
953
- `;
954
- }
955
- if (missingTestPaths.length > 0) {
956
- message += `
957
- Fix options for missing test_paths:
958
- 1. Create the missing test files
959
- 2. Update test paths in ${id}.yaml using wu:edit
960
- 3. Use tests.manual for descriptions instead of file paths
961
-
962
- `;
963
- }
964
- message += `
965
- After fixing, retry:
966
- pnpm wu:done --id ${id}
967
-
968
- This preflight check runs BEFORE gates to catch YAML mismatches early.
969
- See: ai/onboarding/troubleshooting-wu-done.md for more recovery options.
970
- `;
971
- return message;
972
- }
973
- export function runPreflightTasksValidation(id, options = {}) {
974
- // Use injected execSync for testability, default to node's child_process
975
- const execSyncFn = options.execSyncFn || execSyncImport;
976
- console.log(`\n${LOG_PREFIX.DONE} 🔍 Preflight: running tasks:validate...`);
977
- try {
978
- // Run tasks:validate with WU_ID context (single-WU validation mode)
979
- execSyncFn('node tools/validate.js', {
980
- stdio: STDIO.PIPE,
981
- encoding: 'utf-8',
982
- env: { ...process.env, WU_ID: id },
983
- });
984
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Preflight tasks:validate passed`);
985
- return {
986
- valid: true,
987
- errors: [],
988
- abortedBeforeMerge: false,
989
- localMainModified: false,
990
- hasStampStatusError: false,
991
- };
992
- }
993
- catch (err) {
994
- // Validation failed - extract errors from output
995
- const output = err.stdout || err.message || 'Unknown validation error';
996
- const errors = output
997
- .split('\n')
998
- .filter((line) => line.includes('[') && line.includes(']'))
999
- .map((line) => line.trim());
1000
- const hasStampStatusError = errors.some((e) => e.includes('stamp but status is not done'));
1001
- console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Preflight tasks:validate failed`);
1002
- return {
1003
- valid: false,
1004
- errors: errors.length > 0 ? errors : [output],
1005
- abortedBeforeMerge: true,
1006
- localMainModified: false,
1007
- hasStampStatusError,
1008
- };
1009
- }
1010
- }
1011
- /**
1012
- * WU-2308: Validate all pre-commit hooks with worktree context
1013
- *
1014
- * Runs pre-commit validation gates from the worktree directory when provided.
1015
- * This ensures that dependency audits check the worktree's dependencies
1016
- * (with any fixes) rather than main's potentially stale dependencies.
1017
- *
1018
- * @param {string} id - WU ID being completed
1019
- * @param {string|null} worktreePath - Path to worktree (null = run from current dir)
1020
- * @param {ExecSyncOverrideOptions} options - Options for testing
1021
- * @returns {{ valid: boolean, errors: string[] }}
1022
- */
1023
- export function validateAllPreCommitHooks(id, worktreePath = null, options = {}) {
1024
- const execSyncFn = options.execSyncFn || execSyncImport;
1025
- console.log(`\n${LOG_PREFIX.DONE} 🔍 Pre-flight: validating all pre-commit hooks...`);
1026
- const errors = [];
1027
- try {
1028
- // WU-2308: Run from worktree context when provided to ensure audit checks
1029
- // the worktree's dependencies (with fixes) not main's stale dependencies
1030
- const execOptions = {
1031
- stdio: STDIO.INHERIT,
1032
- encoding: 'utf-8',
1033
- };
1034
- // Only set cwd when worktreePath is provided
1035
- if (worktreePath) {
1036
- execOptions.cwd = worktreePath;
1037
- }
1038
- // Run the gates-pre-commit script that contains all validation gates
1039
- execSyncFn('node tools/gates-pre-commit.js', execOptions);
1040
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All pre-commit hooks passed`);
1041
- return { valid: true, errors: [] };
1042
- }
1043
- catch {
1044
- // Pre-commit hooks failed
1045
- errors.push('Pre-commit hook validation failed. Fix these issues before wu:done:');
1046
- errors.push('');
1047
- errors.push('Common fixes:');
1048
- errors.push(' • Formatting issues: Run pnpm format');
1049
- errors.push(' • Lint errors: Run pnpm lint:fix');
1050
- errors.push(' • Type errors: Check pnpm typecheck output');
1051
- errors.push(' • Audit issues: Check pnpm audit output');
1052
- errors.push('');
1053
- errors.push(`After fixing, re-run: pnpm wu:done --id ${id}`);
1054
- return { valid: false, errors };
1055
- }
1056
- }
1057
- /**
1058
- * WU-2242: Validate that test_paths is required for non-doc WUs
1059
- *
1060
- * Enforces that WUs with code changes (non-documentation types with code_paths
1061
- * that contain actual code) have at least one test path specified.
1062
- *
1063
- * Returns valid: true in the following cases:
1064
- * - WU type is 'documentation' or 'process'
1065
- * - code_paths is empty or only contains documentation paths
1066
- * - tests object has at least one test (unit, e2e, manual, or integration)
1067
- *
1068
- * @param {object} wu - WU document
1069
- * @param {string} wu.id - WU ID
1070
- * @param {string} wu.type - WU type (feature, bug, documentation, etc.)
1071
- * @param {object} wu.tests - Tests object with unit, e2e, manual, integration arrays
1072
- * @param {string[]} wu.code_paths - Array of code paths
1073
- * @returns {{ valid: boolean, error?: string }}
1074
- */
1075
- export function validateTestPathsRequired(wu) {
1076
- // Skip validation for documentation and process WUs
1077
- if (wu.type === WU_TYPES.DOCUMENTATION || wu.type === WU_TYPES.PROCESS) {
1078
- return { valid: true };
1079
- }
1080
- // Skip if code_paths is empty or undefined
1081
- const codePaths = wu.code_paths || [];
1082
- if (codePaths.length === 0) {
1083
- return { valid: true };
1084
- }
1085
- // Skip if all code_paths are documentation paths
1086
- const hasCodeChanges = codePaths.some((p) => !isDocumentationPath(p));
1087
- if (!hasCodeChanges) {
1088
- return { valid: true };
1089
- }
1090
- // Check if tests object exists and has at least one test
1091
- const testObj = wu.tests || {};
1092
- // Helper to check if array has items
1093
- const hasItems = (arr) => Array.isArray(arr) && arr.length > 0;
1094
- const hasUnitTests = hasItems(testObj[TEST_TYPES.UNIT]);
1095
- const hasE2ETests = hasItems(testObj[TEST_TYPES.E2E]);
1096
- const hasManualTests = hasItems(testObj[TEST_TYPES.MANUAL]);
1097
- const hasIntegrationTests = hasItems(testObj[TEST_TYPES.INTEGRATION]);
1098
- // No tests at all - fail
1099
- if (!(hasUnitTests || hasE2ETests || hasManualTests || hasIntegrationTests)) {
1100
- return {
1101
- valid: false,
1102
- error: `${wu.id} requires test_paths: WU has code_paths but no tests specified. Add unit, e2e, integration, or manual tests.`,
1103
- };
1104
- }
1105
- // WU-2332: If we have tests, also check automated test requirement for code files
1106
- // Manual-only tests are not sufficient for code changes
1107
- const automatedTestResult = validateAutomatedTestRequirement(wu);
1108
- if (!automatedTestResult.valid) {
1109
- // Extract the first error line for the single-error format of this function
1110
- const errorSummary = automatedTestResult.errors[0]?.split('\n')[0] || 'Automated tests required';
1111
- return {
1112
- valid: false,
1113
- error: `${wu.id}: ${errorSummary}`,
1114
- };
1115
- }
1116
- return { valid: true };
1117
- }
1118
- /**
1119
- * WU-2310: Allowed path patterns for documentation WUs.
1120
- * Mirrors the patterns in gates-pre-commit.mjs gateDocsOnlyPathEnforcement()
1121
- * to enable early validation at preflight (before transaction starts).
1122
- *
1123
- * @constant {RegExp[]}
1124
- */
1125
- const DOCS_ONLY_ALLOWED_PATTERNS = [
1126
- /^memory-bank\//i,
1127
- /^docs\//i,
1128
- /\.md$/i,
1129
- /^\.beacon\/stamps\//i,
1130
- /^\.claude\//i,
1131
- /^ai\//i,
1132
- /^README\.md$/i,
1133
- /^CLAUDE\.md$/i,
1134
- ];
1135
- /**
1136
- * WU-2310: Check if a path is allowed for documentation WUs.
1137
- *
1138
- * @param {string} filePath - File path to check
1139
- * @returns {boolean} True if path is allowed for docs WUs
1140
- */
1141
- function isAllowedDocsPath(filePath) {
1142
- if (!filePath || typeof filePath !== 'string')
1143
- return false;
1144
- return DOCS_ONLY_ALLOWED_PATTERNS.some((pattern) => pattern.test(filePath));
1145
- }
1146
- /**
1147
- * WU-2310: Validate type vs code_paths at preflight (before transaction starts).
1148
- *
1149
- * This catches the documentation WU + code file mismatch BEFORE any transaction
1150
- * begins, preventing the scenario where:
1151
- * 1. Transaction commits files (stamp, status, backlog)
1152
- * 2. Git commit fails due to pre-commit hook (gateDocsOnlyPathEnforcement)
1153
- * 3. Files are left in inconsistent state
1154
- *
1155
- * By running this validation at preflight, we fail fast with a clear error
1156
- * message before any file mutations occur.
1157
- *
1158
- * @param {object} wu - WU document
1159
- * @param {string} wu.id - WU ID
1160
- * @param {string} wu.type - WU type (documentation, feature, bug, etc.)
1161
- * @param {string[]} [wu.code_paths] - Array of code paths
1162
- * @returns {{ valid: boolean, errors: string[], blockedPaths: string[], abortedBeforeTransaction: boolean }}
1163
- */
1164
- export function validateTypeVsCodePathsPreflight(wu) {
1165
- const errors = [];
1166
- const blockedPaths = [];
1167
- // Only validate documentation WUs
1168
- if (wu.type !== WU_TYPES.DOCUMENTATION) {
1169
- return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
1170
- }
1171
- // Skip if no code_paths
1172
- const codePaths = wu.code_paths;
1173
- if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
1174
- return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
1175
- }
1176
- // Check each code_path against allowed patterns
1177
- for (const filePath of codePaths) {
1178
- if (!isAllowedDocsPath(filePath)) {
1179
- blockedPaths.push(filePath);
1180
- }
1181
- }
1182
- if (blockedPaths.length > 0) {
1183
- const pathsList = blockedPaths.map((p) => ` - ${p}`).join('\n');
1184
- errors.push(`Documentation WU ${wu.id} has code_paths that would fail pre-commit hook:\n${pathsList}`);
1185
- return { valid: false, errors, blockedPaths, abortedBeforeTransaction: true };
1186
- }
1187
- return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
1188
- }
1189
- /**
1190
- * WU-2310: Build error message for type vs code_paths preflight failure.
1191
- *
1192
- * Provides actionable guidance for fixing the mismatch:
1193
- * 1. Change WU type to 'engineering' or appropriate type
1194
- * 2. Update code_paths to only include documentation files
3
+ * wu:done validators - aggregated exports
1195
4
  *
1196
- * @param {string} id - WU ID
1197
- * @param {string[]} blockedPaths - Paths that would be blocked
1198
- * @returns {string} Formatted error message
5
+ * WU-1049: Split validators into focused modules while preserving exports.
1199
6
  */
1200
- export function buildTypeVsCodePathsErrorMessage(id, blockedPaths) {
1201
- return `
1202
- PREFLIGHT VALIDATION FAILED (WU-2310)
1203
-
1204
- WU ${id} is type: documentation but has code_paths that are not allowed:
1205
-
1206
- ${blockedPaths.map((p) => ` - ${p}`).join('\n')}
1207
-
1208
- This would fail at git commit time (pre-commit hook: gateDocsOnlyPathEnforcement).
1209
- Aborting BEFORE transaction to prevent inconsistent state.
1210
-
1211
- Fix options:
1212
-
1213
- 1. Change WU type to 'engineering' (or 'feature', 'bug', etc.):
1214
- pnpm wu:edit --id ${id} --type engineering
1215
-
1216
- 2. Update code_paths to only include documentation files:
1217
- pnpm wu:edit --id ${id} --code-paths "docs/..." "*.md"
1218
-
1219
- Allowed paths for documentation WUs:
1220
- - docs/
1221
- - ai/
1222
- - .claude/
1223
- - memory-bank/
1224
- - .beacon/stamps/
1225
- - *.md files
1226
-
1227
- After fixing, retry: pnpm wu:done --id ${id}
1228
- `;
1229
- }
7
+ export { validateInputs } from './wu-done-inputs.js';
8
+ export { readWUPreferWorktree, detectCurrentWorktree, defaultWorktreeFrom, detectWorkspaceMode, defaultBranchFrom, branchExists, detectModeAndPaths, } from './wu-done-paths.js';
9
+ export { generateCommitMessage, validateMetadataFilesExist, updateMetadataFiles, collectMetadataToTransaction, stageAndFormatMetadata, } from './wu-done-metadata.js';
10
+ export { runCleanup } from './wu-done-cleanup.js';
11
+ export { applyExposureDefaults, validateCodePathsExist, validateSpecCompleteness, validatePostMutation, validateTestPathsRequired, validateTypeVsCodePathsPreflight, buildTypeVsCodePathsErrorMessage, } from './wu-done-validation.js';
12
+ export { buildPreflightErrorMessage, executePreflightCodePathValidation, buildPreflightCodePathErrorMessage, runPreflightTasksValidation, validateAllPreCommitHooks, } from './wu-done-preflight.js';
13
+ export { validateAutomatedTestRequirement } from './manual-test-validator.js';