@lumenflow/cli 3.8.7 → 3.9.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 (80) hide show
  1. package/README.md +11 -15
  2. package/dist/cli-entry-point.js +7 -0
  3. package/dist/cli-entry-point.js.map +1 -1
  4. package/dist/gate-defaults.js +5 -0
  5. package/dist/gate-defaults.js.map +1 -1
  6. package/dist/gates-runners.js +77 -1
  7. package/dist/gates-runners.js.map +1 -1
  8. package/dist/guard-main-branch.js +3 -2
  9. package/dist/guard-main-branch.js.map +1 -1
  10. package/dist/hooks/enforcement-checks.js +3 -2
  11. package/dist/hooks/enforcement-checks.js.map +1 -1
  12. package/dist/hooks/path-utils.js +2 -1
  13. package/dist/hooks/path-utils.js.map +1 -1
  14. package/dist/init-detection.js +3 -3
  15. package/dist/init-detection.js.map +1 -1
  16. package/dist/lane-create.js +223 -0
  17. package/dist/lane-create.js.map +1 -0
  18. package/dist/mem-signal.js +1 -1
  19. package/dist/mem-signal.js.map +1 -1
  20. package/dist/public-manifest.js +7 -0
  21. package/dist/public-manifest.js.map +1 -1
  22. package/dist/signal-middleware.js +192 -0
  23. package/dist/signal-middleware.js.map +1 -0
  24. package/dist/state-doctor.js +135 -0
  25. package/dist/state-doctor.js.map +1 -1
  26. package/dist/state-emit.js +72 -10
  27. package/dist/state-emit.js.map +1 -1
  28. package/dist/validate.js +2 -2
  29. package/dist/validate.js.map +1 -1
  30. package/dist/wu-claim.js +75 -10
  31. package/dist/wu-claim.js.map +1 -1
  32. package/dist/wu-done-already-merged.js +2 -7
  33. package/dist/wu-done-already-merged.js.map +1 -1
  34. package/dist/wu-done-gates.js +296 -0
  35. package/dist/wu-done-gates.js.map +1 -0
  36. package/dist/wu-done-memory-telemetry.js +145 -0
  37. package/dist/wu-done-memory-telemetry.js.map +1 -0
  38. package/dist/wu-done-mode-execution.js +136 -0
  39. package/dist/wu-done-mode-execution.js.map +1 -0
  40. package/dist/wu-done-policies.js +206 -1
  41. package/dist/wu-done-policies.js.map +1 -1
  42. package/dist/wu-done-preflight.js +207 -0
  43. package/dist/wu-done-preflight.js.map +1 -0
  44. package/dist/wu-done.js +48 -888
  45. package/dist/wu-done.js.map +1 -1
  46. package/dist/wu-edit-validators.js +30 -1
  47. package/dist/wu-edit-validators.js.map +1 -1
  48. package/dist/wu-edit.js +7 -2
  49. package/dist/wu-edit.js.map +1 -1
  50. package/dist/wu-prep.js +17 -1
  51. package/dist/wu-prep.js.map +1 -1
  52. package/dist/wu-spawn-prompt-builders.js +65 -10
  53. package/dist/wu-spawn-prompt-builders.js.map +1 -1
  54. package/package.json +9 -8
  55. package/packs/sidekick/.turbo/turbo-build.log +1 -1
  56. package/packs/sidekick/package.json +1 -1
  57. package/packs/software-delivery/.turbo/turbo-build.log +1 -1
  58. package/packs/software-delivery/package.json +1 -1
  59. package/templates/core/.lumenflow/constraints.md.template +12 -14
  60. package/templates/core/LUMENFLOW.md.template +7 -4
  61. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +2 -2
  62. package/templates/core/ai/onboarding/docs-generation.md.template +1 -1
  63. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +40 -0
  64. package/templates/core/ai/onboarding/initiative-orchestration.md.template +402 -0
  65. package/templates/core/ai/onboarding/quick-ref-commands.md.template +129 -56
  66. package/templates/core/ai/onboarding/release-process.md.template +102 -39
  67. package/templates/core/ai/onboarding/starting-prompt.md.template +1 -0
  68. package/templates/core/ai/onboarding/test-ratchet.md.template +2 -8
  69. package/templates/vendors/claude/.claude/CLAUDE.md.template +1 -1
  70. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +2 -2
  71. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +3 -3
  72. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +1 -1
  73. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +1 -1
  74. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +4 -4
  75. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +4 -4
  76. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -5
  77. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +4 -4
  78. package/templates/vendors/cline/.clinerules.template +1 -1
  79. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +22 -2
  80. package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +22 -2
package/dist/wu-done.js CHANGED
@@ -42,70 +42,56 @@ import { wuDoneMachine, WU_DONE_EVENTS } from '@lumenflow/core/wu-done-machine';
42
42
  // The guard runs in executeWorktreeCompletion() before metadata transaction
43
43
  // See: packages/@lumenflow/core/src/wu-done-validation.ts
44
44
  import { execSync } from 'node:child_process';
45
- import prettyMs from 'pretty-ms';
46
45
  import { runGates } from './gates.js';
46
+ import { executeGates } from './wu-done-gates.js';
47
47
  // WU-2102: Import scoped test resolver for wu:done gate fallback
48
48
  import { resolveScopedUnitTestsForPrep } from './wu-prep.js';
49
- import { resolveWuDonePreCommitGateDecision } from '@lumenflow/core/gates-agent-mode';
50
49
  import { buildClaimRepairCommand } from './wu-claim-repair-guidance.js';
51
50
  import { resolveStateDir, resolveWuEventsRelativePath } from './state-path-resolvers.js';
52
- import { getGitForCwd, createGitForPath } from '@lumenflow/core/git-adapter';
53
- import { die, getErrorMessage, createError, ErrorCodes } from '@lumenflow/core/error-handler';
51
+ import { appendClaimSessionOverrideAuditEvent, auditOwnershipOverride, checkBacklogConsistencyForWU, checkOwnership, computeBranchOnlyFallback, runWuDoneStagedValidation, } from './wu-done-preflight.js';
52
+ import { getGitForCwd } from '@lumenflow/core/git-adapter';
53
+ import { die, getErrorMessage } from '@lumenflow/core/error-handler';
54
54
  // WU-1223: Location detection for worktree check
55
55
  import { resolveLocation } from '@lumenflow/core/context/location-resolver';
56
- import { existsSync, readFileSync, mkdirSync, appendFileSync, unlinkSync, statSync, readdirSync, } from 'node:fs';
56
+ import { existsSync, readFileSync, mkdirSync, appendFileSync, unlinkSync, readdirSync, } from 'node:fs';
57
57
  import path from 'node:path';
58
58
  // WU-1825: Import from unified code-path-validator (consolidates 3 validators)
59
59
  import { validateWUCodePaths } from '@lumenflow/core/code-path-validator';
60
60
  import { rollbackFiles } from '@lumenflow/core/rollback-utils';
61
- import { validateInputs, detectModeAndPaths, defaultBranchFrom, runCleanup, validateSpecCompleteness, runPreflightTasksValidation, buildPreflightErrorMessage,
61
+ import { validateInputs, detectModeAndPaths, defaultBranchFrom, runCleanup, validateSpecCompleteness,
62
62
  // WU-1805: Preflight code_paths validation before gates
63
63
  executePreflightCodePathValidation, buildPreflightCodePathErrorMessage,
64
- // WU-2308: Pre-commit hooks with worktree context
65
- validateAllPreCommitHooks,
66
64
  // WU-2310: Type vs code_paths preflight validation
67
65
  validateTypeVsCodePathsPreflight, buildTypeVsCodePathsErrorMessage, } from '@lumenflow/core/wu-done-validators';
68
66
  import { formatPreflightWarnings } from '@lumenflow/core/wu-preflight-validators';
69
67
  // WU-1825: validateCodePathsExist moved to unified code-path-validator
70
68
  import { validateCodePathsExist } from '@lumenflow/core/code-path-validator';
71
- import { BRANCHES, PATTERNS, DEFAULTS, LOG_PREFIX, EMOJI, GIT, SESSION, WU_STATUS, PKG_MANAGER, SCRIPTS, CLI_FLAGS, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, TELEMETRY_STEPS, SKIP_GATES_REASONS, CHECKPOINT_MESSAGES, ENV_VARS, getWUStatusDisplay,
69
+ import { BRANCHES, PATTERNS, DEFAULTS, LOG_PREFIX, EMOJI, GIT, SESSION, WU_STATUS, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, TELEMETRY_STEPS, ENV_VARS, getWUStatusDisplay,
72
70
  // WU-1223: Location types for worktree detection
73
71
  CONTEXT_VALIDATION, } from '@lumenflow/core/wu-constants';
74
72
  import { getDocsOnlyPrefixes, DOCS_ONLY_ROOT_FILES } from '@lumenflow/core';
75
- import { printGateFailureBox, printStatusPreview } from '@lumenflow/core/wu-done-ui';
73
+ import { printStatusPreview } from '@lumenflow/core/wu-done-ui';
76
74
  import { ensureOnMain } from '@lumenflow/core/wu-helpers';
77
75
  import { WU_PATHS } from '@lumenflow/core/wu-paths';
78
76
  import { getConfig, clearConfigCache } from '@lumenflow/core/config';
79
77
  import { writeWU, appendNote, parseYAML } from '@lumenflow/core/wu-yaml';
80
78
  import { PLACEHOLDER_SENTINEL, validateWU, validateDoneWU, validateApprovalGates, } from '@lumenflow/core/wu-schema';
81
- import { validateBacklogSync } from '@lumenflow/core/backlog-sync-validator';
82
- import { executeBranchOnlyCompletion,
83
- // WU-1492: Import branch-pr completion path
84
- executeBranchPRCompletion, } from '@lumenflow/core/wu-done-branch-only';
85
- import { executeWorktreeCompletion, autoRebaseBranch } from '@lumenflow/core/wu-done-worktree';
86
- // WU-1746: Already-merged worktree resilience
87
- // WU-2248: Removed executeAlreadyMergedCompletion (wrote directly to local main).
88
- // Both code paths now use executeAlreadyMergedFinalizeFromModule (micro-worktree isolation).
89
- import { detectAlreadyMergedNoWorktree } from '@lumenflow/core/wu-done-merged-worktree';
79
+ import { autoRebaseBranch } from '@lumenflow/core/wu-done-worktree';
90
80
  // WU-2211: --already-merged finalize-only mode
91
81
  import { verifyCodePathsOnMainHead, executeAlreadyMergedFinalize as executeAlreadyMergedFinalizeFromModule, } from './wu-done-already-merged.js';
82
+ import { executeModeSpecificCompletion } from './wu-done-mode-execution.js';
92
83
  import { checkWUConsistency } from '@lumenflow/core/wu-consistency-checker';
93
84
  // WU-1542: Use blocking mode compliance check (replaces non-blocking checkMandatoryAgentsCompliance)
94
85
  import { checkMandatoryAgentsComplianceBlocking } from '@lumenflow/core/orchestration-rules';
95
86
  import { endSessionForWU, getCurrentSessionForWU } from '@lumenflow/agent/auto-session';
96
87
  import { runBackgroundProcessCheck } from '@lumenflow/core/process-detector';
97
88
  import { WUStateStore } from '@lumenflow/core/wu-state-store';
98
- // WU-1588: INIT-007 memory layer integration
99
- import { createCheckpoint } from '@lumenflow/memory/checkpoint';
100
- import { createSignal, loadSignals } from '@lumenflow/memory/signal';
101
89
  // WU-1763: Memory store for loading discoveries (lifecycle nudges)
102
- import { loadMemory, queryByWu } from '@lumenflow/memory/store';
103
- // WU-1943: Checkpoint warning helper
104
- import { hasSessionCheckpoints } from '@lumenflow/core/wu-done-worktree';
90
+ import { loadMemory } from '@lumenflow/memory/store';
105
91
  // WU-1603: Atomic lane locking - release lock on WU completion
106
92
  import { releaseLaneLock } from '@lumenflow/core/lane-lock';
107
93
  // WU-1747: Checkpoint and lock for concurrent load resilience
108
- import { createPreGatesCheckpoint as createWU1747Checkpoint, markGatesPassed, canSkipGates, clearCheckpoint, } from '@lumenflow/core/wu-checkpoint';
94
+ import { canSkipGates, clearCheckpoint } from '@lumenflow/core/wu-checkpoint';
109
95
  // WU-1946: Spawn registry for tracking sub-agent spawns
110
96
  import { DelegationRegistryStore } from '@lumenflow/core/delegation-registry-store';
111
97
  import { DelegationStatus } from '@lumenflow/core/delegation-registry-schema';
@@ -121,77 +107,15 @@ import { evaluateMainDirtyMutationGuard } from './hooks/dirty-guard.js';
121
107
  // WU-1474: Decay policy invocation during completion lifecycle
122
108
  import { runDecayOnDone } from './wu-done-decay.js';
123
109
  import { validateClaimSessionOwnership } from './wu-done-ownership.js';
110
+ import { broadcastCompletionSignal, checkInboxForRecentSignals, createPreGatesCheckpoint, emitTelemetry, enforceCheckpointGateForDone, resolveCheckpointGateMode, } from './wu-done-memory-telemetry.js';
124
111
  import { enforceSpawnProvenanceForDone, enforceWuBriefEvidenceForDone, printExposureWarnings, validateAccessibilityOrDie, validateDocsOnlyFlag, } from './wu-done-policies.js';
125
112
  import { detectParallelCompletions, ensureNoAutoStagedOrNoop, runTripwireCheck, validateBranchOnlyMode, validateStagedFiles, } from './wu-done-git-ops.js';
126
113
  import { flushWuLifecycleSync } from './wu-lifecycle-sync/service.js';
127
114
  import { WU_LIFECYCLE_COMMANDS } from './wu-lifecycle-sync/constants.js';
128
115
  export { buildGatesCommand, buildMissingSpawnPickupEvidenceMessage, buildMissingSpawnProvenanceMessage, buildMissingWuBriefEvidenceMessage, enforceSpawnProvenanceForDone, enforceWuBriefEvidenceForDone, hasSpawnPickupEvidence, printExposureWarnings, shouldEnforceSpawnProvenance, shouldEnforceWuBriefEvidence, validateAccessibilityOrDie, validateDocsOnlyFlag, } from './wu-done-policies.js';
129
116
  export { isBranchAlreadyMerged } from './wu-done-git-ops.js';
130
- // WU-1588: Memory layer constants
131
- const MEMORY_SIGNAL_TYPES = {
132
- WU_COMPLETION: 'wu_completion',
133
- };
134
- const MEMORY_CHECKPOINT_NOTES = {
135
- PRE_GATES: 'Pre-gates checkpoint for recovery if gates fail',
136
- };
137
- const MEMORY_SIGNAL_WINDOW_MS = 60 * 60 * 1000; // 1 hour for recent signals
138
- export const CHECKPOINT_GATE_MODES = {
139
- OFF: 'off',
140
- WARN: 'warn',
141
- BLOCK: 'block',
142
- };
143
- const CHECKPOINT_GATE_CONFIG = {
144
- PATH: 'memory.enforcement.require_checkpoint_for_done',
145
- COMMAND_PREFIX: 'pnpm mem:checkpoint --wu',
146
- WARN_TAG: 'WU-1998',
147
- };
148
- function buildCheckpointGateBlockMessage(id) {
149
- return (`${STRING_LITERALS.NEWLINE}${LOG_PREFIX.DONE} ${EMOJI.FAILURE} No checkpoints found for ${id} session.${STRING_LITERALS.NEWLINE}` +
150
- `${LOG_PREFIX.DONE} ${CHECKPOINT_GATE_CONFIG.PATH} is set to '${CHECKPOINT_GATE_MODES.BLOCK}'.${STRING_LITERALS.NEWLINE}` +
151
- `${LOG_PREFIX.DONE} Create a checkpoint before completing: ${CHECKPOINT_GATE_CONFIG.COMMAND_PREFIX} ${id}${STRING_LITERALS.NEWLINE}`);
152
- }
153
- function buildCheckpointGateWarnMessages(id) {
154
- return [
155
- `${STRING_LITERALS.NEWLINE}${LOG_PREFIX.DONE} ${EMOJI.INFO} ${CHECKPOINT_GATE_CONFIG.WARN_TAG}: No prior checkpoints recorded for ${id} in this session.`,
156
- `${LOG_PREFIX.DONE} A pre-gates checkpoint will be created automatically by wu:done.`,
157
- `${LOG_PREFIX.DONE} For earlier crash recovery, run '${CHECKPOINT_GATE_CONFIG.COMMAND_PREFIX} ${id}' after each acceptance criterion, before gates, or every 30 tool calls.${STRING_LITERALS.NEWLINE}`,
158
- ];
159
- }
160
- export function resolveCheckpointGateMode(mode) {
161
- if (mode === CHECKPOINT_GATE_MODES.OFF) {
162
- return CHECKPOINT_GATE_MODES.OFF;
163
- }
164
- if (mode === CHECKPOINT_GATE_MODES.BLOCK) {
165
- return CHECKPOINT_GATE_MODES.BLOCK;
166
- }
167
- return CHECKPOINT_GATE_MODES.WARN;
168
- }
169
- export async function enforceCheckpointGateForDone({ id, workspacePath, mode, queryByWuFn = queryByWu, hasSessionCheckpointsFn = hasSessionCheckpoints, log = console.log, blocker = (message) => {
170
- die(message);
171
- }, }) {
172
- if (mode === CHECKPOINT_GATE_MODES.OFF) {
173
- return;
174
- }
175
- let wuNodes;
176
- try {
177
- wuNodes = await queryByWuFn(workspacePath, id);
178
- if (hasSessionCheckpointsFn(id, wuNodes)) {
179
- return;
180
- }
181
- }
182
- catch {
183
- // Fail-open: checkpoint discovery issues should not block wu:done.
184
- return;
185
- }
186
- if (mode === CHECKPOINT_GATE_MODES.BLOCK) {
187
- blocker(buildCheckpointGateBlockMessage(id));
188
- return;
189
- }
190
- const warnMessages = buildCheckpointGateWarnMessages(id);
191
- for (const message of warnMessages) {
192
- log(message);
193
- }
194
- }
117
+ export { checkBacklogConsistencyForWU, computeBranchOnlyFallback, normalizeUsername, } from './wu-done-preflight.js';
118
+ export { CHECKPOINT_GATE_MODES, enforceCheckpointGateForDone, resolveCheckpointGateMode, } from './wu-done-memory-telemetry.js';
195
119
  const WU_FILE_NAME_PATTERN = /^WU-\d+\.ya?ml$/;
196
120
  function normalizeRepoRelativePathForYaml(rawPath) {
197
121
  return rawPath.replaceAll('\\', '/').replace(/^\.\/+/, '');
@@ -364,91 +288,6 @@ async function _assertWorktreeWUInProgressInStateStore(id, worktreePath) {
364
288
  `Fix the claim/state log first, then rerun wu:done.`);
365
289
  }
366
290
  }
367
- /**
368
- * WU-1588: Create pre-gates checkpoint for recovery if gates fail.
369
- * Non-blocking wrapper around mem:checkpoint - failures logged as warnings.
370
- *
371
- * @param {string} id - WU ID
372
- * @param {string|null} worktreePath - Path to worktree
373
- * @param {string} baseDir - Base directory for memory layer
374
- * @returns {Promise<void>}
375
- */
376
- async function createPreGatesCheckpoint(id, worktreePath, baseDir = process.cwd()) {
377
- try {
378
- const result = await createCheckpoint(baseDir, {
379
- note: MEMORY_CHECKPOINT_NOTES.PRE_GATES,
380
- wuId: id,
381
- progress: `Starting gates execution for ${id}`,
382
- nextSteps: worktreePath
383
- ? `Gates running in worktree: ${worktreePath}`
384
- : 'Gates running in branch-only mode',
385
- trigger: 'wu-done-pre-gates',
386
- });
387
- if (result.success) {
388
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Pre-gates checkpoint created (${result.checkpoint.id})`);
389
- }
390
- }
391
- catch (err) {
392
- // Non-blocking: checkpoint failure should not block wu:done
393
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not create pre-gates checkpoint: ${getErrorMessage(err)}`);
394
- }
395
- }
396
- /**
397
- * WU-1588: Broadcast completion signal to parallel agents.
398
- * Non-blocking wrapper around mem:signal - failures logged as warnings.
399
- *
400
- * @param {string} id - WU ID
401
- * @param {string} title - WU title
402
- * @param {string} baseDir - Base directory for memory layer
403
- * @returns {Promise<void>}
404
- */
405
- async function broadcastCompletionSignal(id, title, baseDir = process.cwd()) {
406
- try {
407
- const result = await createSignal(baseDir, {
408
- message: `${MEMORY_SIGNAL_TYPES.WU_COMPLETION}: ${id} - ${title}`,
409
- wuId: id,
410
- });
411
- if (result.success) {
412
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Completion signal broadcast (${result.signal.id})`);
413
- }
414
- }
415
- catch (err) {
416
- // Non-blocking: signal failure should not block wu:done
417
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not broadcast completion signal: ${getErrorMessage(err)}`);
418
- }
419
- }
420
- /**
421
- * WU-1588: Check inbox for recent signals from parallel agents.
422
- * Non-blocking wrapper around loadSignals - failures logged as warnings.
423
- *
424
- * @param {string} id - Current WU ID (for filtering)
425
- * @param {string} baseDir - Base directory for memory layer
426
- * @returns {Promise<void>}
427
- */
428
- async function checkInboxForRecentSignals(id, baseDir = process.cwd()) {
429
- try {
430
- const since = new Date(Date.now() - MEMORY_SIGNAL_WINDOW_MS);
431
- const signals = await loadSignals(baseDir, { since, unreadOnly: true });
432
- // Filter out signals for current WU
433
- const relevantSignals = signals.filter((s) => s.wu_id !== id);
434
- if (relevantSignals.length > 0) {
435
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.INFO} Recent signals from parallel agents:`);
436
- for (const signal of relevantSignals.slice(0, 5)) {
437
- // Show at most 5
438
- const timestamp = new Date(signal.created_at).toLocaleTimeString();
439
- console.log(` - [${timestamp}] ${signal.message}`);
440
- }
441
- if (relevantSignals.length > 5) {
442
- console.log(` ... and ${relevantSignals.length - 5} more`);
443
- }
444
- console.log(` Run 'pnpm mem:inbox' for full list\n`);
445
- }
446
- }
447
- catch (err) {
448
- // Non-blocking: inbox check failure should not block wu:done
449
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not check inbox for signals: ${getErrorMessage(err)}`);
450
- }
451
- }
452
291
  /**
453
292
  * WU-1946: Update spawn registry on WU completion.
454
293
  * Non-blocking wrapper - failures logged as warnings.
@@ -490,64 +329,10 @@ const GIT_CONFIG_USER_NAME = 'user.name';
490
329
  const GIT_CONFIG_USER_EMAIL = 'user.email';
491
330
  // Default fallback messages
492
331
  const DEFAULT_NO_REASON = '(no reason provided)';
493
- /**
494
- * WU-1234: Normalize username for ownership comparison
495
- * Extracts username from email address for comparison.
496
- * This allows tom@hellm.ai to match 'tom' assigned_to field.
497
- *
498
- * @param {string|null|undefined} value - Email address or username
499
- * @returns {string} Normalized username (lowercase)
500
- */
501
- export function normalizeUsername(value) {
502
- if (!value)
503
- return '';
504
- const str = String(value).trim();
505
- // Extract username from email: tom@hellm.ai -> tom
506
- // WU-1281: Using string split instead of regex
507
- const atIndex = str.indexOf('@');
508
- const username = atIndex > 0 ? str.slice(0, atIndex) : str;
509
- return username.toLowerCase();
510
- }
511
332
  // WU-1281: isDocsOnlyByPaths removed - use shouldSkipWebTests from path-classifiers.ts
512
333
  // The validators already use shouldSkipWebTests via detectDocsOnlyByPaths wrapper.
513
334
  // Keeping the export for backward compatibility but re-exporting the canonical function.
514
335
  export { shouldSkipWebTests as isDocsOnlyByPaths } from '@lumenflow/core/path-classifiers';
515
- /**
516
- * WU-1234: Pre-flight check for backlog state consistency
517
- * Fails fast if the WU appears in both Done and In Progress sections.
518
- *
519
- * @param {string} id - WU ID to check
520
- * @param {string} backlogPath - Path to backlog.md
521
- * @returns {{ valid: boolean, error: string|null }}
522
- */
523
- export function checkBacklogConsistencyForWU(id, backlogPath) {
524
- try {
525
- const result = validateBacklogSync(backlogPath);
526
- // Check if this specific WU is in both Done and In Progress
527
- if (!result.valid) {
528
- for (const error of result.errors) {
529
- // Check if the error mentions both Done and In Progress AND mentions our WU
530
- if (error.includes('Done and In Progress') && error.includes(id)) {
531
- return {
532
- valid: false,
533
- error: `❌ BACKLOG STATE INCONSISTENCY: ${id} found in both Done and In Progress sections.\n\n` +
534
- `This is an invalid state that must be fixed manually before wu:done can proceed.\n\n` +
535
- `Fix options:\n` +
536
- ` 1. If ${id} is truly done: Remove from In Progress in backlog.md\n` +
537
- ` 2. If ${id} needs more work: Remove from Done in backlog.md, update WU YAML status\n\n` +
538
- `After fixing backlog.md, retry: pnpm wu:done --id ${id}`,
539
- };
540
- }
541
- }
542
- }
543
- return { valid: true, error: null };
544
- }
545
- catch (e) {
546
- // If validation fails (e.g., file not found), warn but don't block
547
- console.warn(`${LOG_PREFIX.DONE} Warning: Could not validate backlog consistency: ${getErrorMessage(e)}`);
548
- return { valid: true, error: null };
549
- }
550
- }
551
336
  /**
552
337
  * Read commitlint header-max-length from config, fallback to DEFAULTS.MAX_COMMIT_SUBJECT
553
338
  * WU-1281: Using centralized constant instead of hardcoded 100
@@ -565,14 +350,6 @@ function getCommitHeaderLimit() {
565
350
  }
566
351
  }
567
352
  // ensureOnMain() moved to wu-helpers.ts (WU-1256)
568
- export function emitTelemetry(event) {
569
- const logPath = path.join('.lumenflow', 'flow.log');
570
- const logDir = path.dirname(logPath);
571
- if (!existsSync(logDir))
572
- mkdirSync(logDir, { recursive: true });
573
- const line = JSON.stringify({ timestamp: new Date().toISOString(), ...event });
574
- appendFileSync(logPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
575
- }
576
353
  async function auditSkipGates(id, reason, fixWU, worktreePath) {
577
354
  const auditBaseDir = worktreePath || process.cwd();
578
355
  const auditPath = path.join(auditBaseDir, '.lumenflow', 'skip-gates-audit.log');
@@ -626,117 +403,6 @@ async function auditSkipCosGates(id, reason, worktreePath) {
626
403
  }
627
404
  // WU-2308: validateAllPreCommitHooks moved to wu-done-validators.ts
628
405
  // Now accepts worktreePath parameter to run audit from worktree context
629
- /**
630
- * Check if node_modules in worktree may be stale
631
- * Detects when package.json differs between main and worktree, which indicates
632
- * dependencies were added/removed but pnpm install may not have run in worktree.
633
- * This prevents confusing typecheck failures due to missing dependencies.
634
- * @param {string} worktreePath - Path to worktree
635
- */
636
- function checkNodeModulesStaleness(worktreePath) {
637
- try {
638
- const mainPackageJson = path.resolve('package.json');
639
- const worktreePackageJson = path.resolve(worktreePath, 'package.json');
640
- if (!existsSync(mainPackageJson) || !existsSync(worktreePackageJson)) {
641
- // No package.json to compare
642
- return;
643
- }
644
- const mainContent = readFileSync(mainPackageJson, {
645
- encoding: FILE_SYSTEM.UTF8,
646
- });
647
- const worktreeContent = readFileSync(worktreePackageJson, {
648
- encoding: FILE_SYSTEM.UTF8,
649
- });
650
- // Compare package.json files
651
- if (mainContent !== worktreeContent) {
652
- const worktreeNodeModules = path.resolve(worktreePath, 'node_modules');
653
- // Check if node_modules exists and when it was last modified
654
- if (existsSync(worktreeNodeModules)) {
655
- const nodeModulesStat = statSync(worktreeNodeModules);
656
- const packageJsonStat = statSync(worktreePackageJson);
657
- // If package.json is newer than node_modules, dependencies may be stale
658
- if (packageJsonStat.mtimeMs > nodeModulesStat.mtimeMs) {
659
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WARNING: Potentially stale node_modules detected\n\n` +
660
- ` package.json in worktree differs from main checkout\n` +
661
- ` node_modules was last modified: ${nodeModulesStat.mtime.toISOString()}\n` +
662
- ` package.json was last modified: ${packageJsonStat.mtime.toISOString()}\n\n` +
663
- ` If gates fail with missing dependencies/types, run:\n` +
664
- ` cd ${worktreePath}\n` +
665
- ` pnpm install\n` +
666
- ` cd -\n` +
667
- ` pnpm wu:done --id <WU-ID>\n`);
668
- }
669
- }
670
- else {
671
- // node_modules doesn't exist at all
672
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WARNING: node_modules missing in worktree\n\n` +
673
- ` package.json in worktree differs from main checkout\n` +
674
- ` but node_modules directory does not exist\n\n` +
675
- ` If gates fail with missing dependencies/types, run:\n` +
676
- ` cd ${worktreePath}\n` +
677
- ` pnpm install\n` +
678
- ` cd -\n` +
679
- ` pnpm wu:done --id <WU-ID>\n`);
680
- }
681
- }
682
- }
683
- catch (e) {
684
- // Non-critical check - just warn if it fails
685
- console.warn(`${LOG_PREFIX.DONE} Could not check node_modules staleness: ${getErrorMessage(e)}`);
686
- }
687
- }
688
- /**
689
- * Run gates in worktree
690
- * @param {string} worktreePath - Path to worktree
691
- * @param {string} id - WU ID
692
- * @param {object} options - Gates options
693
- * @param {boolean} options.isDocsOnly - Auto-detected docs-only from code_paths
694
- * @param {boolean} options.docsOnly - Explicit --docs-only flag from CLI
695
- */
696
- async function runGatesInWorktree(worktreePath, id, options = {}) {
697
- const { isDocsOnly = false, docsOnly = false, scopedTestPaths } = options;
698
- console.log(`\n${LOG_PREFIX.DONE} Running gates in worktree: ${worktreePath}`);
699
- // Check for stale node_modules before running gates (prevents confusing failures)
700
- checkNodeModulesStaleness(worktreePath);
701
- // WU-1012: Use docs-only gates if explicit --docs-only flag OR auto-detected
702
- const useDocsOnlyGates = docsOnly || isDocsOnly;
703
- if (useDocsOnlyGates) {
704
- console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
705
- if (docsOnly) {
706
- console.log(`${LOG_PREFIX.DONE} (explicit --docs-only flag)`);
707
- }
708
- }
709
- const startTime = Date.now();
710
- try {
711
- const ok = Boolean(await runGates({
712
- cwd: worktreePath,
713
- docsOnly: useDocsOnlyGates,
714
- coverageMode: undefined,
715
- scopedTestPaths,
716
- }));
717
- if (!ok) {
718
- throw createError(ErrorCodes.GATES_FAILED, 'Gates failed');
719
- }
720
- const duration = Date.now() - startTime;
721
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Gates passed in ${prettyMs(duration)}`);
722
- emitTelemetry({ script: 'wu-done', wu_id: id, step: 'gates', ok: true, duration_ms: duration });
723
- return true;
724
- }
725
- catch {
726
- const duration = Date.now() - startTime;
727
- emitTelemetry({
728
- script: 'wu-done',
729
- wu_id: id,
730
- step: 'gates',
731
- ok: false,
732
- duration_ms: duration,
733
- });
734
- // WU-1280: Prominent error summary box (visible after ~130k chars of gate output)
735
- // WU-1281: Extracted to helper using pretty-ms for duration formatting
736
- printGateFailureBox({ id, location: worktreePath, durationMs: duration, isWorktreeMode: true });
737
- die(`Gates failed in ${worktreePath}. Fix issues in the worktree and try again.`);
738
- }
739
- }
740
406
  // Note: updateStatusRemoveInProgress, addToStatusCompleted, and moveWUToDoneBacklog
741
407
  // have been extracted to tools/lib/wu-status-updater.ts and imported above (WU-1163)
742
408
  //
@@ -955,155 +621,6 @@ function runWUValidator(doc, id, allowTodo = false, worktreePath = null) {
955
621
  }
956
622
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU validator passed`);
957
623
  }
958
- /**
959
- * GUARDRAIL 2: Enforce ownership semantics in wu:done
960
- *
961
- * Validates that the current user owns the WU before allowing completion.
962
- * Prevents agents/humans from finishing WUs they do not own.
963
- *
964
- * @param {string} id - WU ID
965
- * @param {object} doc - WU YAML document
966
- * @param {string|null} worktreePath - Expected worktree path
967
- * @param {boolean} overrideOwner - Override flag (requires reason)
968
- * @param {string|null} overrideReason - Reason for override
969
- * @returns {{valid: boolean, error: string|null, auditEntry: object|null}}
970
- */
971
- async function appendClaimSessionOverrideAuditEvent({ wuId, claimedSessionId, activeSessionId, reason, worktreePath, }) {
972
- const stateDir = resolveStateDir(worktreePath);
973
- const stateStore = new WUStateStore(stateDir);
974
- await stateStore.load();
975
- await stateStore.checkpoint(wuId, `[wu:done] force ownership override claimed_session=${claimedSessionId} active_session=${activeSessionId || 'none'}`, {
976
- progress: 'wu:done ownership override',
977
- nextSteps: reason,
978
- });
979
- }
980
- async function checkOwnership(id, doc, worktreePath, overrideOwner = false, overrideReason = null) {
981
- // Missing worktree means WU was not claimed properly (unless escape hatch applies)
982
- if (!worktreePath || !existsSync(worktreePath)) {
983
- return {
984
- valid: false,
985
- error: `Missing worktree for ${id}.\n\n` +
986
- `Expected worktree at: ${worktreePath || 'unknown'}\n\n` +
987
- `Worktrees are required for proper WU completion in Worktree mode.\n` +
988
- `If the worktree was removed, recreate it and retry, or use --skip-gates with justification.`,
989
- auditEntry: null,
990
- };
991
- }
992
- // Get assigned owner from WU YAML - read directly from worktree to ensure we get the lane branch version
993
- let assignedTo = doc.assigned_to || null;
994
- if (!assignedTo && worktreePath) {
995
- // Fallback: Read directly from worktree YAML if not present in doc (fixes WU-1106)
996
- const wtWUPath = path.join(worktreePath, WU_PATHS.WU(id));
997
- if (existsSync(wtWUPath)) {
998
- try {
999
- const text = readFileSync(wtWUPath, { encoding: FILE_SYSTEM.UTF8 });
1000
- const wtDoc = parseYAML(text);
1001
- assignedTo = wtDoc?.assigned_to || null;
1002
- if (assignedTo) {
1003
- console.log(`${LOG_PREFIX.DONE} Note: Read assigned_to from worktree YAML (not found in main)`);
1004
- }
1005
- }
1006
- catch (err) {
1007
- console.warn(`${LOG_PREFIX.DONE} Warning: Failed to read assigned_to from worktree: ${getErrorMessage(err)}`);
1008
- }
1009
- }
1010
- }
1011
- if (!assignedTo) {
1012
- return {
1013
- valid: false,
1014
- error: `WU ${id} has no assigned_to field.\n\n` +
1015
- `This WU was claimed before ownership tracking was implemented.\n` +
1016
- `To complete this WU:\n` +
1017
- ` 1. Add assigned_to: <your-email> to ${id}.yaml\n` +
1018
- ` 2. Commit the change\n` +
1019
- ` 3. Re-run: pnpm wu:done --id ${id}`,
1020
- auditEntry: null,
1021
- };
1022
- }
1023
- // Get current user identity
1024
- let currentUser;
1025
- try {
1026
- currentUser = (await getGitForCwd().getConfigValue(GIT_CONFIG_USER_EMAIL)).trim();
1027
- }
1028
- catch {
1029
- // Fallback to environment variable
1030
- currentUser = process.env.GIT_USER || process.env.USER || null;
1031
- }
1032
- if (!currentUser) {
1033
- return {
1034
- valid: false,
1035
- error: `Cannot determine current user identity.\n\n` +
1036
- `Set git user.email or GIT_USER environment variable.`,
1037
- auditEntry: null,
1038
- };
1039
- }
1040
- // WU-1234: Normalize usernames for comparison (allows email vs username match)
1041
- // e.g., tom@hellm.ai matches 'tom' assigned_to field
1042
- const normalizedAssigned = normalizeUsername(assignedTo);
1043
- const normalizedCurrent = normalizeUsername(currentUser);
1044
- const isOwner = normalizedAssigned === normalizedCurrent;
1045
- if (isOwner) {
1046
- // Owner is completing their own WU - allow
1047
- if (assignedTo !== currentUser) {
1048
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Ownership match via normalization: "${assignedTo}" == "${currentUser}"`);
1049
- }
1050
- return { valid: true, error: null, auditEntry: null };
1051
- }
1052
- // Not the owner - check for override
1053
- if (overrideOwner) {
1054
- if (!overrideReason) {
1055
- return {
1056
- valid: false,
1057
- error: `--override-owner requires --reason "<why you're completing someone else's WU>"`,
1058
- auditEntry: null,
1059
- };
1060
- }
1061
- // Create audit entry
1062
- const auditEntry = {
1063
- timestamp: new Date().toISOString(),
1064
- wu_id: id,
1065
- assigned_to: assignedTo,
1066
- completed_by: currentUser,
1067
- reason: overrideReason,
1068
- git_commit: (await getGitForCwd().getCommitHash()).trim(),
1069
- };
1070
- console.log(`\n⚠️ --override-owner: Completing WU assigned to someone else`);
1071
- console.log(` Assigned to: ${assignedTo}`);
1072
- console.log(` Completed by: ${currentUser}`);
1073
- console.log(` Reason: ${overrideReason}\n`);
1074
- return { valid: true, error: null, auditEntry };
1075
- }
1076
- // Not the owner and no override - block
1077
- return {
1078
- valid: false,
1079
- error: `\n❌ OWNERSHIP VIOLATION: ${id} is assigned to someone else\n\n` +
1080
- ` Assigned to: ${assignedTo}\n` +
1081
- ` Current user: ${currentUser}\n\n` +
1082
- ` You cannot complete WUs you do not own.\n\n` +
1083
- ` 📋 Options:\n` +
1084
- ` 1. Contact ${assignedTo} to complete the WU\n` +
1085
- ` 2. Reassign the WU to yourself in ${id}.yaml (requires approval)\n` +
1086
- ` 3. Add co_assigned field for pairing (requires approval)\n\n` +
1087
- ` ⚠️ To override (use with extreme caution):\n` +
1088
- ` pnpm wu:done --id ${id} --override-owner --reason "<why>"\n\n` +
1089
- ` AGENTS: NEVER use --override-owner without explicit instruction.\n` +
1090
- ` Language protocol: "pick up WU-${id.replace('WU-', '')}" = READ ONLY.\n`,
1091
- auditEntry: null,
1092
- };
1093
- }
1094
- /**
1095
- * Log ownership override to audit trail
1096
- * @param {object} auditEntry - Audit entry to log
1097
- */
1098
- function auditOwnershipOverride(auditEntry) {
1099
- const auditPath = path.join('.lumenflow', 'ownership-override-audit.log');
1100
- const auditDir = path.dirname(auditPath);
1101
- if (!existsSync(auditDir))
1102
- mkdirSync(auditDir, { recursive: true });
1103
- const line = JSON.stringify(auditEntry);
1104
- appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
1105
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Ownership override logged to ${auditPath}`);
1106
- }
1107
624
  /**
1108
625
  * Execute pre-flight checks before gates
1109
626
  * Extracted from main() to reduce complexity (WU-1215 Phase 2 Extraction #3)
@@ -1431,222 +948,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1431
948
  }
1432
949
  return { title, docForValidation };
1433
950
  }
1434
- async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath, branchName, scopedTestPaths, }) {
1435
- const gateResult = {
1436
- fullGatesRanInCurrentRun: false,
1437
- skippedByCheckpoint: false,
1438
- checkpointId: null,
1439
- };
1440
- // WU-1747: Check if gates can be skipped based on valid checkpoint
1441
- // This allows resuming wu:done without re-running gates if nothing changed
1442
- // WU-2102: Look for checkpoint in worktree (where wu:prep writes it)
1443
- const skipResult = canSkipGates(id, {
1444
- currentHeadSha: undefined,
1445
- baseDir: worktreePath || undefined,
1446
- });
1447
- if (skipResult.canSkip) {
1448
- gateResult.skippedByCheckpoint = true;
1449
- gateResult.checkpointId = skipResult.checkpoint.checkpointId;
1450
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${CHECKPOINT_MESSAGES.SKIPPING_GATES_VALID}`);
1451
- console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.CHECKPOINT_LABEL}: ${skipResult.checkpoint.checkpointId}`);
1452
- console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.GATES_PASSED_AT}: ${skipResult.checkpoint.gatesPassedAt}`);
1453
- emitTelemetry({
1454
- script: TELEMETRY_STEPS.GATES,
1455
- wu_id: id,
1456
- step: TELEMETRY_STEPS.GATES,
1457
- skipped: true,
1458
- reason: SKIP_GATES_REASONS.CHECKPOINT_VALID,
1459
- checkpoint_id: skipResult.checkpoint.checkpointId,
1460
- });
1461
- return gateResult; // Skip gates entirely
1462
- }
1463
- // WU-1747: Create checkpoint before gates for resumption on failure
1464
- if (worktreePath && branchName) {
1465
- try {
1466
- await createWU1747Checkpoint({ wuId: id, worktreePath, branchName }, { gatesPassed: false });
1467
- }
1468
- catch (err) {
1469
- // Non-blocking: checkpoint failure should not block wu:done
1470
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} ${CHECKPOINT_MESSAGES.COULD_NOT_CREATE}: ${getErrorMessage(err)}`);
1471
- }
1472
- }
1473
- // WU-1588: Create pre-gates checkpoint for recovery if gates fail
1474
- // Non-blocking: failures handled internally by createPreGatesCheckpoint
1475
- // WU-1749 Bug 5: Pass worktreePath as baseDir to write to worktree's wu-events.jsonl, not main's
1476
- await createPreGatesCheckpoint(id, worktreePath, worktreePath || process.cwd());
1477
- // P0 EMERGENCY FIX: Restore wu-events.jsonl after checkpoint creation
1478
- // WU-1748 added checkpoint persistence to wu-events.jsonl but doesn't commit it,
1479
- // leaving unstaged changes that cause "git rebase" to fail with "You have unstaged changes"
1480
- // This restores the file to HEAD state - checkpoint data is preserved in memory store
1481
- if (worktreePath) {
1482
- try {
1483
- execSync(`git -C "${worktreePath}" restore "${resolveWuEventsRelativePath(worktreePath)}"`);
1484
- }
1485
- catch {
1486
- // Non-fatal: file might not exist or already clean
1487
- }
1488
- }
1489
- // Step 0a: Run invariants check (WU-2252: NON-BYPASSABLE, runs even with --skip-gates)
1490
- // This ensures repo invariants are never violated, regardless of skip-gates flag
1491
- // WU-2253: Run against worktreePath (when present) to catch violations that only exist in the worktree
1492
- // WU-2425: Pass wuId to scope WU-specific invariants to just the completing WU
1493
- const invariantsBaseDir = worktreePath || process.cwd();
1494
- console.log(`\n${LOG_PREFIX.DONE} Running invariants check (non-bypassable)...`);
1495
- console.log(`${LOG_PREFIX.DONE} Checking invariants in: ${invariantsBaseDir}`);
1496
- const { runInvariants } = await import('@lumenflow/core/invariants-runner');
1497
- const invariantsResult = runInvariants({ baseDir: invariantsBaseDir, silent: false, wuId: id });
1498
- if (!invariantsResult.success) {
1499
- emitTelemetry({
1500
- script: 'wu-done',
1501
- wu_id: id,
1502
- step: 'invariants',
1503
- ok: false,
1504
- });
1505
- die(`Invariants check failed. Fix violations before completing WU.\n\n${invariantsResult.formatted}`);
1506
- }
1507
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Invariants check passed`);
1508
- emitTelemetry({
1509
- script: 'wu-done',
1510
- wu_id: id,
1511
- step: 'invariants',
1512
- ok: true,
1513
- });
1514
- // Step 0b: Run gates BEFORE merge (or skip with audit trail)
1515
- if (args.skipGates) {
1516
- console.log(`\n${EMOJI.WARNING} ${EMOJI.WARNING} ${EMOJI.WARNING} SKIP-GATES MODE ACTIVE ${EMOJI.WARNING} ${EMOJI.WARNING} ${EMOJI.WARNING}\n`);
1517
- console.log(`${LOG_PREFIX.DONE} Skipping gates check as requested`);
1518
- console.log(`${LOG_PREFIX.DONE} Reason: ${args.reason}`);
1519
- console.log(`${LOG_PREFIX.DONE} Fix WU: ${args.fixWu}`);
1520
- console.log(`${LOG_PREFIX.DONE} Worktree: ${worktreePath || 'Branch-Only mode (no worktree)'}`);
1521
- await auditSkipGates(id, args.reason, args.fixWu, worktreePath);
1522
- console.log('\n⚠️ Ensure test failures are truly pre-existing!\n');
1523
- emitTelemetry({
1524
- script: 'wu-done',
1525
- wu_id: id,
1526
- step: 'gates',
1527
- skipped: true,
1528
- reason: args.reason,
1529
- fix_wu: args.fixWu,
1530
- });
1531
- }
1532
- else if (isBranchOnly) {
1533
- // Branch-Only mode: run gates in-place (current directory on lane branch)
1534
- console.log(`\n${LOG_PREFIX.DONE} Running gates in Branch-Only mode (in-place on lane branch)`);
1535
- // WU-1012: Use docs-only gates if explicit --docs-only flag OR auto-detected
1536
- const useDocsOnlyGates = Boolean(args.docsOnly) || Boolean(isDocsOnly);
1537
- if (useDocsOnlyGates) {
1538
- console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
1539
- if (args.docsOnly) {
1540
- console.log(`${LOG_PREFIX.DONE} (explicit --docs-only flag)`);
1541
- }
1542
- }
1543
- const startTime = Date.now();
1544
- try {
1545
- const ok = Boolean(await runGates({ docsOnly: useDocsOnlyGates }));
1546
- if (!ok) {
1547
- throw createError(ErrorCodes.GATES_FAILED, 'Gates failed');
1548
- }
1549
- const duration = Date.now() - startTime;
1550
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Gates passed in ${prettyMs(duration)}`);
1551
- emitTelemetry({
1552
- script: 'wu-done',
1553
- wu_id: id,
1554
- step: 'gates',
1555
- ok: true,
1556
- duration_ms: duration,
1557
- });
1558
- }
1559
- catch {
1560
- const duration = Date.now() - startTime;
1561
- emitTelemetry({
1562
- script: 'wu-done',
1563
- wu_id: id,
1564
- step: 'gates',
1565
- ok: false,
1566
- duration_ms: duration,
1567
- });
1568
- // WU-1280: Prominent error summary box (Branch-Only mode)
1569
- // WU-1281: Extracted to helper using pretty-ms for duration formatting
1570
- printGateFailureBox({
1571
- id,
1572
- location: 'Branch-Only',
1573
- durationMs: duration,
1574
- isWorktreeMode: false,
1575
- });
1576
- die(`Gates failed in Branch-Only mode. Fix issues and try again.`);
1577
- }
1578
- gateResult.fullGatesRanInCurrentRun = true;
1579
- }
1580
- else if (worktreePath && existsSync(worktreePath)) {
1581
- // Worktree mode: run gates in the dedicated worktree
1582
- // WU-1012: Pass both auto-detected and explicit docs-only flags
1583
- // WU-2102: Forward scopedTestPaths so wu:done uses scoped tests when no checkpoint skip
1584
- await runGatesInWorktree(worktreePath, id, {
1585
- isDocsOnly,
1586
- docsOnly: Boolean(args.docsOnly),
1587
- scopedTestPaths,
1588
- });
1589
- gateResult.fullGatesRanInCurrentRun = true;
1590
- }
1591
- else {
1592
- die(`Worktree not found (${worktreePath || 'unknown'}). Gates must run in the lane worktree.\n` +
1593
- `If the worktree was removed, recreate it and retry, or rerun with --branch-only when the lane branch exists.\n` +
1594
- `Use --skip-gates only with justification.`);
1595
- }
1596
- // Step 0.75: Run COS governance gates (WU-614, COS v1.3 §7)
1597
- if (!args.skipCosGates) {
1598
- console.log(`\n${LOG_PREFIX.DONE} Running COS governance gates...`);
1599
- const startTime = Date.now();
1600
- try {
1601
- execSync(`${PKG_MANAGER} ${SCRIPTS.COS_GATES} ${CLI_FLAGS.WU} ${id}`, {
1602
- stdio: 'inherit',
1603
- });
1604
- const duration = Date.now() - startTime;
1605
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} COS gates passed in ${prettyMs(duration)}`);
1606
- emitTelemetry({
1607
- script: 'wu-done',
1608
- wu_id: id,
1609
- step: 'cos-gates',
1610
- ok: true,
1611
- duration_ms: duration,
1612
- });
1613
- }
1614
- catch {
1615
- const duration = Date.now() - startTime;
1616
- emitTelemetry({
1617
- script: 'wu-done',
1618
- wu_id: id,
1619
- step: 'cos-gates',
1620
- ok: false,
1621
- duration_ms: duration,
1622
- });
1623
- console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} COS governance gates failed`);
1624
- console.error('\nTo fix:');
1625
- console.error(' 1. Add required evidence to governance.evidence field in WU YAML');
1626
- console.error(' 2. See: https://lumenflow.dev/reference/evidence-format/');
1627
- console.error('\nEmergency bypass (creates audit trail):');
1628
- // WU-1852: Reference --skip-gates (the actual CLI flag), not the non-existent --skip-cos-gates
1629
- console.error(` pnpm wu:done --id ${id} --skip-gates --reason "COS evidence pending" --fix-wu WU-XXXX`);
1630
- die('Abort: WU not completed. Fix governance evidence and retry pnpm wu:done.');
1631
- }
1632
- }
1633
- else {
1634
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Skipping COS governance gates as requested`);
1635
- console.log(`${LOG_PREFIX.DONE} Reason: ${args.reason || DEFAULT_NO_REASON}`);
1636
- await auditSkipCosGates(id, args.reason, worktreePath);
1637
- emitTelemetry({
1638
- script: 'wu-done',
1639
- wu_id: id,
1640
- step: 'cos-gates',
1641
- skipped: true,
1642
- reason: args.reason,
1643
- });
1644
- }
1645
- // WU-1747: Mark checkpoint as gates passed for resumption on failure
1646
- // This allows subsequent wu:done attempts to skip gates if nothing changed
1647
- markGatesPassed(id, { baseDir: worktreePath || undefined });
1648
- return gateResult;
1649
- }
1650
951
  /**
1651
952
  * Print State HUD for visibility
1652
953
  * Extracted from main() to reduce complexity (WU-1215 Phase 2 Extraction #4)
@@ -1658,13 +959,6 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1658
959
  * @param {string|null} params.derivedWorktree - Derived worktree path
1659
960
  * @param {string} params.STAMPS_DIR - Stamps directory path
1660
961
  */
1661
- export function computeBranchOnlyFallback({ isBranchOnly, branchOnlyRequested, worktreeExists, derivedWorktree, }) {
1662
- const allowFallback = Boolean(branchOnlyRequested) && !isBranchOnly && !worktreeExists && Boolean(derivedWorktree);
1663
- return {
1664
- allowFallback,
1665
- effectiveBranchOnly: isBranchOnly || allowFallback,
1666
- };
1667
- }
1668
962
  export function getYamlStatusForDisplay(status) {
1669
963
  return getWUStatusDisplay(status);
1670
964
  }
@@ -1896,6 +1190,11 @@ export async function main() {
1896
1190
  isDocsOnly,
1897
1191
  worktreePath,
1898
1192
  scopedTestPaths: scopedTestPathsForDone,
1193
+ }, {
1194
+ auditSkipGates,
1195
+ auditSkipCosGates,
1196
+ createPreGatesCheckpoint,
1197
+ emitTelemetry,
1899
1198
  });
1900
1199
  }
1901
1200
  catch (gateErr) {
@@ -1923,183 +1222,44 @@ export async function main() {
1923
1222
  derivedWorktree: effectiveDerivedWorktree,
1924
1223
  STAMPS_DIR,
1925
1224
  });
1926
- // Step 0.5: Pre-flight hook validation policy.
1927
- // WU-1659: Reuse Step 0 gate attestation/checkpoint and avoid duplicate full-suite execution.
1928
- const preCommitGateDecision = resolveWuDonePreCommitGateDecision({
1225
+ // Step 0.5 + 0.6: Pre-flight staged validation policy and tasks:validate guard.
1226
+ await runWuDoneStagedValidation({
1227
+ id,
1228
+ worktreePath,
1229
+ gateResult: {
1230
+ fullGatesRanInCurrentRun: gateExecutionResult.fullGatesRanInCurrentRun,
1231
+ skippedByCheckpoint: gateExecutionResult.skippedByCheckpoint,
1232
+ checkpointId: gateExecutionResult.checkpointId,
1233
+ },
1929
1234
  skipGates: Boolean(args.skipGates),
1930
- fullGatesRanInCurrentRun: gateExecutionResult.fullGatesRanInCurrentRun,
1931
- skippedByCheckpoint: gateExecutionResult.skippedByCheckpoint,
1932
- checkpointId: gateExecutionResult.checkpointId,
1235
+ runGatesFn: ({ cwd }) => runGates({ cwd, docsOnly: false }),
1933
1236
  });
1934
- console.log(`${LOG_PREFIX.DONE} ${preCommitGateDecision.message}`);
1935
- // Fallback path remains available if gate attestation is missing for any reason.
1936
- if (preCommitGateDecision.runPreCommitFullSuite) {
1937
- const hookResult = await validateAllPreCommitHooks(id, worktreePath, {
1938
- runGates: ({ cwd }) => runGates({ cwd, docsOnly: false }),
1939
- });
1940
- if (!hookResult.valid) {
1941
- die('Pre-flight validation failed. Fix hook issues and try again.');
1942
- }
1943
- }
1944
- // Step 0.6: WU-1781 - Run tasks:validate preflight BEFORE any merge/push operations
1945
- // This prevents deadlocks where validation fails after merge, leaving local main ahead of origin
1946
- // Specifically catches stamp-status mismatches from legacy WUs that would block pre-push hooks
1947
- const tasksValidationResult = runPreflightTasksValidation(id);
1948
- if (!tasksValidationResult.valid) {
1949
- const errorMessage = buildPreflightErrorMessage(id, tasksValidationResult.errors);
1950
- console.error(errorMessage);
1951
- die('Preflight tasks:validate failed. See errors above for fix options.');
1952
- }
1953
- // Step 1: Execute mode-specific completion workflow (WU-1215: extracted to mode modules)
1954
- // Worktree mode: Update metadata in worktree → commit → merge to main
1955
- // Branch-Only mode: Merge to main → update metadata on main → commit
1956
- // WU-1811: Track cleanupSafe flag to conditionally skip worktree removal on failure
1957
- let completionResult = { cleanupSafe: true }; // Default to safe for no-auto mode
1237
+ // Step 1: Execute mode-specific completion workflow (WU-2167)
1238
+ // Main remains orchestration-only; execution details live in wu-done-mode-execution.ts.
1239
+ let completionResult = { cleanupSafe: true };
1958
1240
  if (!args.noAuto) {
1959
- // Build context for mode-specific execution
1960
- // WU-1369: Worktree mode uses atomic transaction pattern (no recordTransactionState/rollbackTransaction)
1961
- // Branch-only mode still uses the old rollback mechanism
1962
- const baseContext = {
1241
+ completionResult = await executeModeSpecificCompletion({
1963
1242
  id,
1964
1243
  args,
1965
1244
  docMain,
1966
1245
  title,
1967
1246
  isDocsOnly,
1968
1247
  maxCommitLength: getCommitHeaderLimit(),
1248
+ isBranchPR,
1249
+ effectiveBranchOnly,
1250
+ worktreePath,
1251
+ resolvedWorktreePath,
1252
+ pipelineActor: {
1253
+ send: (event) => pipelineActor.send(event),
1254
+ stop: () => pipelineActor.stop(),
1255
+ getSnapshot: () => pipelineActor.getSnapshot(),
1256
+ },
1969
1257
  validateStagedFiles,
1970
- };
1971
- try {
1972
- if (isBranchPR) {
1973
- // WU-1492: Branch-PR mode: commit metadata on lane branch, push, create PR
1974
- // Never checks out or merges to main
1975
- const laneBranch = defaultBranchFrom(docMain);
1976
- const branchPRContext = {
1977
- ...baseContext,
1978
- laneBranch,
1979
- };
1980
- completionResult = await executeBranchPRCompletion(branchPRContext);
1981
- }
1982
- else if (effectiveBranchOnly) {
1983
- // Branch-Only mode: merge first, then update metadata on main
1984
- // NOTE: Branch-only still uses old rollback mechanism
1985
- const branchOnlyContext = {
1986
- ...baseContext,
1987
- recordTransactionState,
1988
- rollbackTransaction,
1989
- };
1990
- completionResult = await executeBranchOnlyCompletion(branchOnlyContext);
1991
- }
1992
- else {
1993
- // Worktree mode: update in worktree, commit, then merge or create PR
1994
- // WU-1369: Uses atomic transaction pattern
1995
- // WU-1541: Create worktree-aware validateStagedFiles to avoid process.chdir dependency
1996
- if (!worktreePath) {
1997
- // WU-1746: Before dying, check if branch is already merged to main
1998
- // This handles the case where worktree was manually deleted after branch was merged
1999
- const laneBranch = defaultBranchFrom(docMain);
2000
- const mergedDetection = await detectAlreadyMergedNoWorktree({
2001
- wuId: id,
2002
- laneBranch: laneBranch || '',
2003
- worktreePath: resolvedWorktreePath,
2004
- });
2005
- if (mergedDetection.merged && !mergedDetection.worktreeExists) {
2006
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1746: Worktree missing but branch already merged to main`);
2007
- // WU-2248: Use micro-worktree isolation (same as --already-merged flag path)
2008
- // Previously called executeAlreadyMergedCompletion which wrote directly to local main.
2009
- const mergedTitle = title || String(docMain.title || id);
2010
- const mergedResult = await executeAlreadyMergedFinalizeFromModule({
2011
- id,
2012
- title: mergedTitle,
2013
- lane: String(docMain.lane || ''),
2014
- doc: docMain,
2015
- });
2016
- completionResult = {
2017
- success: mergedResult.success,
2018
- committed: true,
2019
- pushed: true,
2020
- merged: true,
2021
- cleanupSafe: true,
2022
- };
2023
- }
2024
- else {
2025
- die(`Missing worktree path for ${id} completion in worktree mode`);
2026
- }
2027
- }
2028
- else {
2029
- const worktreeGitForValidation = createGitForPath(worktreePath);
2030
- const worktreeContext = {
2031
- ...baseContext,
2032
- worktreePath,
2033
- validateStagedFiles: (wuId, docsOnly, options) => validateStagedFiles(wuId, docsOnly, worktreeGitForValidation, options),
2034
- };
2035
- completionResult = await executeWorktreeCompletion(worktreeContext);
2036
- }
2037
- }
2038
- // WU-1663: Mode-specific completion succeeded - send pipeline events.
2039
- // The completion modules handle commit, merge, and push internally.
2040
- // We send the corresponding pipeline events based on the completion result.
2041
- pipelineActor.send({ type: WU_DONE_EVENTS.COMMIT_COMPLETE });
2042
- pipelineActor.send({ type: WU_DONE_EVENTS.MERGE_COMPLETE });
2043
- pipelineActor.send({ type: WU_DONE_EVENTS.PUSH_COMPLETE });
2044
- // Handle recovery mode (zombie state cleanup completed)
2045
- if ('recovered' in completionResult && completionResult.recovered) {
2046
- // P0 FIX: Release lane lock before early exit
2047
- try {
2048
- const lane = docMain.lane;
2049
- if (lane)
2050
- releaseLaneLock(lane, { wuId: id });
2051
- }
2052
- catch {
2053
- // Intentionally ignore lock release errors during cleanup
2054
- }
2055
- pipelineActor.stop();
2056
- process.exit(EXIT_CODES.SUCCESS);
2057
- }
2058
- }
2059
- catch (err) {
2060
- // WU-1663: Mode execution failed - determine which stage failed
2061
- // based on completion result flags and send appropriate failure event.
2062
- const failureStage = completionResult.committed === false
2063
- ? WU_DONE_EVENTS.COMMIT_FAILED
2064
- : completionResult.merged === false
2065
- ? WU_DONE_EVENTS.MERGE_FAILED
2066
- : completionResult.pushed === false
2067
- ? WU_DONE_EVENTS.PUSH_FAILED
2068
- : WU_DONE_EVENTS.COMMIT_FAILED; // Default to commit as earliest possible failure
2069
- pipelineActor.send({
2070
- type: failureStage,
2071
- error: getErrorMessage(err),
2072
- });
2073
- // WU-1663: Log pipeline state for diagnostics
2074
- const failedSnapshot = pipelineActor.getSnapshot();
2075
- console.error(`${LOG_PREFIX.DONE} Pipeline state: ${failedSnapshot.value} (failedAt: ${failedSnapshot.context.failedAt})`);
2076
- pipelineActor.stop();
2077
- // P0 FIX: Release lane lock before error exit
2078
- try {
2079
- const lane = docMain.lane;
2080
- if (lane)
2081
- releaseLaneLock(lane, { wuId: id });
2082
- }
2083
- catch {
2084
- // Intentionally ignore lock release errors during error handling
2085
- }
2086
- console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Mode execution failed: ${getErrorMessage(err)}`);
2087
- console.error(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Next step: resolve the reported error and retry: pnpm wu:done --id ${id}`);
2088
- // WU-1811: Check if cleanup is safe before removing worktree
2089
- // If cleanupSafe is false (or undefined), preserve worktree for recovery
2090
- const cleanupSafe = typeof err === 'object' &&
2091
- err !== null &&
2092
- 'cleanupSafe' in err &&
2093
- typeof err.cleanupSafe === 'boolean'
2094
- ? err.cleanupSafe
2095
- : undefined;
2096
- if (cleanupSafe === false) {
2097
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1811: Worktree preserved - rerun wu:done to recover`);
2098
- }
2099
- // Mode modules handle rollback internally, we just need to exit
2100
- // Exit code 1 = recoverable (rebase/fix and retry)
2101
- process.exit(EXIT_CODES.ERROR);
2102
- }
1258
+ defaultBranchFrom: (doc) => defaultBranchFrom(doc),
1259
+ executeAlreadyMergedFinalize: executeAlreadyMergedFinalizeFromModule,
1260
+ recordTransactionState,
1261
+ rollbackTransaction,
1262
+ });
2103
1263
  }
2104
1264
  else {
2105
1265
  await ensureNoAutoStagedOrNoop([WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR]);