@lumenflow/cli 1.0.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 (129) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +116 -0
  3. package/dist/gates.d.ts +41 -0
  4. package/dist/gates.d.ts.map +1 -0
  5. package/dist/gates.js +684 -0
  6. package/dist/gates.js.map +1 -0
  7. package/dist/initiative-add-wu.d.ts +22 -0
  8. package/dist/initiative-add-wu.d.ts.map +1 -0
  9. package/dist/initiative-add-wu.js +234 -0
  10. package/dist/initiative-add-wu.js.map +1 -0
  11. package/dist/initiative-create.d.ts +28 -0
  12. package/dist/initiative-create.d.ts.map +1 -0
  13. package/dist/initiative-create.js +172 -0
  14. package/dist/initiative-create.js.map +1 -0
  15. package/dist/initiative-edit.d.ts +34 -0
  16. package/dist/initiative-edit.d.ts.map +1 -0
  17. package/dist/initiative-edit.js +440 -0
  18. package/dist/initiative-edit.js.map +1 -0
  19. package/dist/initiative-list.d.ts +12 -0
  20. package/dist/initiative-list.d.ts.map +1 -0
  21. package/dist/initiative-list.js +101 -0
  22. package/dist/initiative-list.js.map +1 -0
  23. package/dist/initiative-status.d.ts +11 -0
  24. package/dist/initiative-status.d.ts.map +1 -0
  25. package/dist/initiative-status.js +221 -0
  26. package/dist/initiative-status.js.map +1 -0
  27. package/dist/mem-checkpoint.d.ts +16 -0
  28. package/dist/mem-checkpoint.d.ts.map +1 -0
  29. package/dist/mem-checkpoint.js +237 -0
  30. package/dist/mem-checkpoint.js.map +1 -0
  31. package/dist/mem-cleanup.d.ts +29 -0
  32. package/dist/mem-cleanup.d.ts.map +1 -0
  33. package/dist/mem-cleanup.js +267 -0
  34. package/dist/mem-cleanup.js.map +1 -0
  35. package/dist/mem-create.d.ts +17 -0
  36. package/dist/mem-create.d.ts.map +1 -0
  37. package/dist/mem-create.js +265 -0
  38. package/dist/mem-create.js.map +1 -0
  39. package/dist/mem-inbox.d.ts +35 -0
  40. package/dist/mem-inbox.d.ts.map +1 -0
  41. package/dist/mem-inbox.js +373 -0
  42. package/dist/mem-inbox.js.map +1 -0
  43. package/dist/mem-init.d.ts +15 -0
  44. package/dist/mem-init.d.ts.map +1 -0
  45. package/dist/mem-init.js +146 -0
  46. package/dist/mem-init.js.map +1 -0
  47. package/dist/mem-ready.d.ts +16 -0
  48. package/dist/mem-ready.d.ts.map +1 -0
  49. package/dist/mem-ready.js +224 -0
  50. package/dist/mem-ready.js.map +1 -0
  51. package/dist/mem-signal.d.ts +16 -0
  52. package/dist/mem-signal.d.ts.map +1 -0
  53. package/dist/mem-signal.js +204 -0
  54. package/dist/mem-signal.js.map +1 -0
  55. package/dist/mem-start.d.ts +16 -0
  56. package/dist/mem-start.d.ts.map +1 -0
  57. package/dist/mem-start.js +158 -0
  58. package/dist/mem-start.js.map +1 -0
  59. package/dist/mem-summarize.d.ts +22 -0
  60. package/dist/mem-summarize.d.ts.map +1 -0
  61. package/dist/mem-summarize.js +213 -0
  62. package/dist/mem-summarize.js.map +1 -0
  63. package/dist/mem-triage.d.ts +22 -0
  64. package/dist/mem-triage.d.ts.map +1 -0
  65. package/dist/mem-triage.js +328 -0
  66. package/dist/mem-triage.js.map +1 -0
  67. package/dist/spawn-list.d.ts +16 -0
  68. package/dist/spawn-list.d.ts.map +1 -0
  69. package/dist/spawn-list.js +140 -0
  70. package/dist/spawn-list.js.map +1 -0
  71. package/dist/wu-block.d.ts +16 -0
  72. package/dist/wu-block.d.ts.map +1 -0
  73. package/dist/wu-block.js +241 -0
  74. package/dist/wu-block.js.map +1 -0
  75. package/dist/wu-claim.d.ts +32 -0
  76. package/dist/wu-claim.d.ts.map +1 -0
  77. package/dist/wu-claim.js +1106 -0
  78. package/dist/wu-claim.js.map +1 -0
  79. package/dist/wu-cleanup.d.ts +17 -0
  80. package/dist/wu-cleanup.d.ts.map +1 -0
  81. package/dist/wu-cleanup.js +194 -0
  82. package/dist/wu-cleanup.js.map +1 -0
  83. package/dist/wu-create.d.ts +38 -0
  84. package/dist/wu-create.d.ts.map +1 -0
  85. package/dist/wu-create.js +520 -0
  86. package/dist/wu-create.js.map +1 -0
  87. package/dist/wu-deps.d.ts +13 -0
  88. package/dist/wu-deps.d.ts.map +1 -0
  89. package/dist/wu-deps.js +119 -0
  90. package/dist/wu-deps.js.map +1 -0
  91. package/dist/wu-done.d.ts +153 -0
  92. package/dist/wu-done.d.ts.map +1 -0
  93. package/dist/wu-done.js +2096 -0
  94. package/dist/wu-done.js.map +1 -0
  95. package/dist/wu-edit.d.ts +29 -0
  96. package/dist/wu-edit.d.ts.map +1 -0
  97. package/dist/wu-edit.js +852 -0
  98. package/dist/wu-edit.js.map +1 -0
  99. package/dist/wu-infer-lane.d.ts +17 -0
  100. package/dist/wu-infer-lane.d.ts.map +1 -0
  101. package/dist/wu-infer-lane.js +135 -0
  102. package/dist/wu-infer-lane.js.map +1 -0
  103. package/dist/wu-preflight.d.ts +47 -0
  104. package/dist/wu-preflight.d.ts.map +1 -0
  105. package/dist/wu-preflight.js +167 -0
  106. package/dist/wu-preflight.js.map +1 -0
  107. package/dist/wu-prune.d.ts +16 -0
  108. package/dist/wu-prune.d.ts.map +1 -0
  109. package/dist/wu-prune.js +259 -0
  110. package/dist/wu-prune.js.map +1 -0
  111. package/dist/wu-repair.d.ts +60 -0
  112. package/dist/wu-repair.d.ts.map +1 -0
  113. package/dist/wu-repair.js +226 -0
  114. package/dist/wu-repair.js.map +1 -0
  115. package/dist/wu-spawn-completion.d.ts +10 -0
  116. package/dist/wu-spawn-completion.js +30 -0
  117. package/dist/wu-spawn.d.ts +168 -0
  118. package/dist/wu-spawn.d.ts.map +1 -0
  119. package/dist/wu-spawn.js +1327 -0
  120. package/dist/wu-spawn.js.map +1 -0
  121. package/dist/wu-unblock.d.ts +16 -0
  122. package/dist/wu-unblock.d.ts.map +1 -0
  123. package/dist/wu-unblock.js +234 -0
  124. package/dist/wu-unblock.js.map +1 -0
  125. package/dist/wu-validate.d.ts +16 -0
  126. package/dist/wu-validate.d.ts.map +1 -0
  127. package/dist/wu-validate.js +193 -0
  128. package/dist/wu-validate.js.map +1 -0
  129. package/package.json +92 -0
@@ -0,0 +1,2096 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Done Helper
4
+ *
5
+ * Canonical sequence (Worktree mode - DEFAULT):
6
+ * 1) Run gates in lane worktree (validates the change, not just main)
7
+ * 2) Pre-flight validation: run ALL pre-commit hooks before merge (prevents partial completion)
8
+ * 3) cd into worktree
9
+ * 4) Auto-update WU YAML/backlog/status to Done in worktree (unless --no-auto)
10
+ * 5) Create `.beacon/stamps/WU-{id}.done` in worktree
11
+ * 6) Validate staged files against whitelist
12
+ * 7) Commit metadata changes in worktree (on lane branch)
13
+ * 8) cd back to main
14
+ * 9) Merge lane branch to main with --ff-only (metadata + code merged atomically)
15
+ * 10) Push to `main`
16
+ * 11) Remove the associated worktree (unless --no-remove)
17
+ * 12) Optionally delete the lane branch (with --delete-branch)
18
+ * 13) Emit telemetry to .beacon/flow.log
19
+ *
20
+ * Canonical sequence (Branch-Only mode - LEGACY):
21
+ * 1) Run gates on lane branch (in main checkout)
22
+ * 2) Pre-flight validation
23
+ * 3) Merge lane branch to main
24
+ * 4) Update metadata on main
25
+ * 5) Commit and push
26
+ * 6) Delete lane branch
27
+ *
28
+ * Usage:
29
+ * pnpm wu:done --id WU-334 [--worktree worktrees/intelligence-wu-334] [--no-auto] [--no-remove] [--no-merge] [--delete-branch]
30
+ *
31
+ * WU-2542: This script imports utilities from @lumenflow/core package.
32
+ * Full migration to thin shim pending @lumenflow/core CLI export implementation.
33
+ */
34
+ import { execSync } from 'node:child_process';
35
+ import prettyMs from 'pretty-ms';
36
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
37
+ import { die } from '@lumenflow/core/dist/error-handler.js';
38
+ import { existsSync, readFileSync, mkdirSync, appendFileSync, unlinkSync, statSync } from 'node:fs';
39
+ import path from 'node:path';
40
+ // WU-1825: Import from unified code-path-validator (consolidates 3 validators)
41
+ import { validateWUCodePaths } from '@lumenflow/core/dist/code-path-validator.js';
42
+ // WU-1983: Migration deployment utilities
43
+ import { discoverLocalMigrations, hasMigrationChanges, } from '@lumenflow/core/dist/migration-deployer.js';
44
+ import { validateDocsOnly, getAllowedPathsDescription, } from '@lumenflow/core/dist/docs-path-validator.js';
45
+ import { scanLogForViolations, rotateLog } from '@lumenflow/core/dist/commands-logger.js';
46
+ import { rollbackFiles } from '@lumenflow/core/dist/rollback-utils.js';
47
+ import { validateInputs, detectModeAndPaths, defaultBranchFrom, runCleanup, validateSpecCompleteness, runPreflightTasksValidation, buildPreflightErrorMessage,
48
+ // WU-1805: Preflight code_paths validation before gates
49
+ executePreflightCodePathValidation, buildPreflightCodePathErrorMessage,
50
+ // WU-2308: Pre-commit hooks with worktree context
51
+ validateAllPreCommitHooks,
52
+ // WU-2310: Type vs code_paths preflight validation
53
+ validateTypeVsCodePathsPreflight, buildTypeVsCodePathsErrorMessage, } from '@lumenflow/core/dist/wu-done-validators.js';
54
+ // WU-1825: validateCodePathsExist moved to unified code-path-validator
55
+ import { validateCodePathsExist } from '@lumenflow/core/dist/code-path-validator.js';
56
+ import { BRANCHES, REMOTES, 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, } from '@lumenflow/core/dist/wu-constants.js';
57
+ import { printGateFailureBox, printStatusPreview } from '@lumenflow/core/dist/wu-done-ui.js';
58
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
59
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
60
+ import { writeWU, appendNote, parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
61
+ import { PLACEHOLDER_SENTINEL, validateWU, validateDoneWU, validateApprovalGates, } from '@lumenflow/core/dist/wu-schema.js';
62
+ import { validateBacklogSync } from '@lumenflow/core/dist/backlog-sync-validator.js';
63
+ import { executeBranchOnlyCompletion } from '@lumenflow/core/dist/wu-done-branch-only.js';
64
+ import { executeWorktreeCompletion, autoRebaseBranch, } from '@lumenflow/core/dist/wu-done-worktree.js';
65
+ import { checkWUConsistency } from '@lumenflow/core/dist/wu-consistency-checker.js';
66
+ // WU-1542: Use blocking mode compliance check (replaces non-blocking checkMandatoryAgentsCompliance)
67
+ import { checkMandatoryAgentsComplianceBlocking } from '@lumenflow/core/dist/orchestration-rules.js';
68
+ import { endSessionForWU } from '@lumenflow/agent/dist/auto-session-integration.js';
69
+ import { runBackgroundProcessCheck } from '@lumenflow/core/dist/process-detector.js';
70
+ import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
71
+ // WU-1588: INIT-007 memory layer integration
72
+ import { createCheckpoint } from '@lumenflow/memory/dist/mem-checkpoint-core.js';
73
+ import { createSignal, loadSignals } from '@lumenflow/memory/dist/mem-signal-core.js';
74
+ // WU-1763: Memory store for loading discoveries (lifecycle nudges)
75
+ import { loadMemory, queryByWu } from '@lumenflow/memory/dist/memory-store.js';
76
+ // WU-1943: Checkpoint warning helper
77
+ import { hasSessionCheckpoints } from '@lumenflow/core/dist/wu-done-worktree.js';
78
+ // WU-1603: Atomic lane locking - release lock on WU completion
79
+ import { releaseLaneLock } from '@lumenflow/core/dist/lane-lock.js';
80
+ // WU-1747: Checkpoint and lock for concurrent load resilience
81
+ import { createPreGatesCheckpoint as createWU1747Checkpoint, markGatesPassed, canSkipGates, clearCheckpoint, } from '@lumenflow/core/dist/wu-checkpoint.js';
82
+ // WU-1946: Spawn registry for tracking sub-agent spawns
83
+ import { SpawnRegistryStore } from '@lumenflow/core/dist/spawn-registry-store.js';
84
+ import { SpawnStatus } from '@lumenflow/core/dist/spawn-registry-schema.js';
85
+ // WU-1999: Exposure validation for UI pairing
86
+ // WU-2022: Feature accessibility validation (blocking)
87
+ import { validateExposure, validateFeatureAccessibility, } from '@lumenflow/core/dist/wu-validation.js';
88
+ // WU-1588: Memory layer constants
89
+ const MEMORY_SIGNAL_TYPES = {
90
+ WU_COMPLETION: 'wu_completion',
91
+ };
92
+ const MEMORY_CHECKPOINT_NOTES = {
93
+ PRE_GATES: 'Pre-gates checkpoint for recovery if gates fail',
94
+ };
95
+ const MEMORY_SIGNAL_WINDOW_MS = 60 * 60 * 1000; // 1 hour for recent signals
96
+ // Path constant for wu-events.jsonl (used in multiple places)
97
+ const WU_EVENTS_PATH = '.beacon/state/wu-events.jsonl';
98
+ /**
99
+ * WU-1804: Preflight validation for claim metadata before gates.
100
+ *
101
+ * Validates that the WU is properly claimed before running gates:
102
+ * 1. Worktree YAML status must be 'in_progress'
103
+ * 2. State store must show WU as 'in_progress'
104
+ *
105
+ * If either fails, exits before gates with actionable guidance to run wu:repair-claim.
106
+ * This prevents burning tokens on gates that will ultimately fail.
107
+ *
108
+ * @param {string} id - WU ID
109
+ * @param {string} worktreePath - Path to the worktree
110
+ * @param {string} yamlStatus - Current status from worktree YAML
111
+ * @returns {Promise<void>}
112
+ */
113
+ async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
114
+ const errors = [];
115
+ // Check 1: YAML status must be in_progress
116
+ if (yamlStatus !== WU_STATUS.IN_PROGRESS) {
117
+ errors.push(`Worktree YAML status is '${yamlStatus}', expected '${WU_STATUS.IN_PROGRESS}'`);
118
+ }
119
+ // Check 2: State store must show WU as in_progress
120
+ const resolvedWorktreePath = path.resolve(worktreePath);
121
+ const stateDir = path.join(resolvedWorktreePath, '.beacon', 'state');
122
+ const eventsPath = path.join(resolvedWorktreePath, WU_EVENTS_PATH);
123
+ try {
124
+ const store = new WUStateStore(stateDir);
125
+ await store.load();
126
+ const inProgress = store.getByStatus(WU_STATUS.IN_PROGRESS);
127
+ if (!inProgress.has(id)) {
128
+ errors.push(`State store does not show ${id} as in_progress (path: ${eventsPath})`);
129
+ }
130
+ }
131
+ catch (err) {
132
+ errors.push(`Cannot read state store: ${err.message} (path: ${eventsPath})`);
133
+ }
134
+ // If no errors, we're good
135
+ if (errors.length === 0) {
136
+ return;
137
+ }
138
+ // Build actionable error message with wu:repair-claim guidance
139
+ die(`❌ CLAIM METADATA VALIDATION FAILED (WU-1804)\n\n` +
140
+ `Cannot proceed with wu:done - the WU is not properly claimed.\n\n` +
141
+ `Issues detected:\n${errors.map((e) => ` - ${e}`).join('\n')}\n\n` +
142
+ `This typically happens when:\n` +
143
+ ` • A crash/rebase interrupted worktree creation\n` +
144
+ ` • The claim transaction was partially completed\n` +
145
+ ` • Another process modified the WU state\n\n` +
146
+ `Next step:\n` +
147
+ ` pnpm wu:repair-claim --id ${id}\n\n` +
148
+ `After repair, retry:\n` +
149
+ ` pnpm wu:done --id ${id}\n\n` +
150
+ `See: ai/onboarding/troubleshooting-wu-done.md for more recovery options.`);
151
+ }
152
+ export function printExposureWarnings(wu, options = {}) {
153
+ // Validate exposure
154
+ const result = validateExposure(wu, { skipExposureCheck: options.skipExposureCheck });
155
+ // Print warnings if any
156
+ if (result.warnings.length > 0) {
157
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1999: Exposure validation warnings:`);
158
+ for (const warning of result.warnings) {
159
+ console.log(`${LOG_PREFIX.DONE} ${warning}`);
160
+ }
161
+ console.log(`${LOG_PREFIX.DONE} These are non-blocking warnings. ` +
162
+ `To skip, use --skip-exposure-check flag.\n`);
163
+ }
164
+ }
165
+ export function validateAccessibilityOrDie(wu, options = {}) {
166
+ const result = validateFeatureAccessibility(wu, {
167
+ skipAccessibilityCheck: options.skipAccessibilityCheck,
168
+ });
169
+ if (!result.valid) {
170
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} WU-2022: Feature accessibility validation failed`);
171
+ die(`❌ FEATURE ACCESSIBILITY VALIDATION FAILED (WU-2022)\n\n` +
172
+ `Cannot complete wu:done - UI feature accessibility not verified.\n\n` +
173
+ `${result.errors.join('\n\n')}\n\n` +
174
+ `This gate prevents "orphaned code" - features that exist but users cannot access.`);
175
+ }
176
+ }
177
+ async function assertWorktreeWUInProgressInStateStore(id, worktreePath) {
178
+ const resolvedWorktreePath = path.resolve(worktreePath);
179
+ const stateDir = path.join(resolvedWorktreePath, '.beacon', 'state');
180
+ const eventsPath = path.join(resolvedWorktreePath, WU_EVENTS_PATH);
181
+ const store = new WUStateStore(stateDir);
182
+ try {
183
+ await store.load();
184
+ }
185
+ catch (err) {
186
+ die(`Cannot read WU state store for ${id}.\n\n` +
187
+ `Path: ${eventsPath}\n\n` +
188
+ `Error: ${err.message}\n\n` +
189
+ `If this WU was claimed on an older tool version or the event log is missing/corrupt,\n` +
190
+ `repair the worktree state store before rerunning wu:done.`);
191
+ }
192
+ const inProgress = store.getByStatus(WU_STATUS.IN_PROGRESS);
193
+ if (!inProgress.has(id)) {
194
+ die(`WU ${id} is not in_progress in the worktree state store.\n\n` +
195
+ `Path: ${eventsPath}\n\n` +
196
+ `This will fail later when wu:done tries to append a complete event and regenerate backlog/status.\n` +
197
+ `Fix the claim/state log first, then rerun wu:done.`);
198
+ }
199
+ }
200
+ /**
201
+ * WU-1588: Create pre-gates checkpoint for recovery if gates fail.
202
+ * Non-blocking wrapper around mem:checkpoint - failures logged as warnings.
203
+ *
204
+ * @param {string} id - WU ID
205
+ * @param {string|null} worktreePath - Path to worktree
206
+ * @param {string} baseDir - Base directory for memory layer
207
+ * @returns {Promise<void>}
208
+ */
209
+ async function createPreGatesCheckpoint(id, worktreePath, baseDir = process.cwd()) {
210
+ try {
211
+ const result = await createCheckpoint(baseDir, {
212
+ note: MEMORY_CHECKPOINT_NOTES.PRE_GATES,
213
+ wuId: id,
214
+ progress: `Starting gates execution for ${id}`,
215
+ nextSteps: worktreePath
216
+ ? `Gates running in worktree: ${worktreePath}`
217
+ : 'Gates running in branch-only mode',
218
+ trigger: 'wu-done-pre-gates',
219
+ });
220
+ if (result.success) {
221
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Pre-gates checkpoint created (${result.checkpoint.id})`);
222
+ }
223
+ }
224
+ catch (err) {
225
+ // Non-blocking: checkpoint failure should not block wu:done
226
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not create pre-gates checkpoint: ${err.message}`);
227
+ }
228
+ }
229
+ /**
230
+ * WU-1588: Broadcast completion signal to parallel agents.
231
+ * Non-blocking wrapper around mem:signal - failures logged as warnings.
232
+ *
233
+ * @param {string} id - WU ID
234
+ * @param {string} title - WU title
235
+ * @param {string} baseDir - Base directory for memory layer
236
+ * @returns {Promise<void>}
237
+ */
238
+ async function broadcastCompletionSignal(id, title, baseDir = process.cwd()) {
239
+ try {
240
+ const result = await createSignal(baseDir, {
241
+ message: `${MEMORY_SIGNAL_TYPES.WU_COMPLETION}: ${id} - ${title}`,
242
+ wuId: id,
243
+ });
244
+ if (result.success) {
245
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Completion signal broadcast (${result.signal.id})`);
246
+ }
247
+ }
248
+ catch (err) {
249
+ // Non-blocking: signal failure should not block wu:done
250
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not broadcast completion signal: ${err.message}`);
251
+ }
252
+ }
253
+ /**
254
+ * WU-1588: Check inbox for recent signals from parallel agents.
255
+ * Non-blocking wrapper around loadSignals - failures logged as warnings.
256
+ *
257
+ * @param {string} id - Current WU ID (for filtering)
258
+ * @param {string} baseDir - Base directory for memory layer
259
+ * @returns {Promise<void>}
260
+ */
261
+ async function checkInboxForRecentSignals(id, baseDir = process.cwd()) {
262
+ try {
263
+ const since = new Date(Date.now() - MEMORY_SIGNAL_WINDOW_MS);
264
+ const signals = await loadSignals(baseDir, { since, unreadOnly: true });
265
+ // Filter out signals for current WU
266
+ const relevantSignals = signals.filter((s) => s.wu_id !== id);
267
+ if (relevantSignals.length > 0) {
268
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.INFO} Recent signals from parallel agents:`);
269
+ for (const signal of relevantSignals.slice(0, 5)) {
270
+ // Show at most 5
271
+ const timestamp = new Date(signal.created_at).toLocaleTimeString();
272
+ console.log(` - [${timestamp}] ${signal.message}`);
273
+ }
274
+ if (relevantSignals.length > 5) {
275
+ console.log(` ... and ${relevantSignals.length - 5} more`);
276
+ }
277
+ console.log(` Run 'pnpm mem:inbox' for full list\n`);
278
+ }
279
+ }
280
+ catch (err) {
281
+ // Non-blocking: inbox check failure should not block wu:done
282
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not check inbox for signals: ${err.message}`);
283
+ }
284
+ }
285
+ /**
286
+ * WU-1946: Update spawn registry on WU completion.
287
+ * Non-blocking wrapper - failures logged as warnings.
288
+ *
289
+ * When a WU is completed via wu:done, this function updates the spawn registry
290
+ * to mark the spawned entry as completed (if one exists). This allows orchestrators
291
+ * to track sub-agent spawn completion status.
292
+ *
293
+ * Gracefully skips if:
294
+ * - No spawn entry found for this WU (legacy WU created before registry)
295
+ * - Registry file doesn't exist
296
+ * - Any error during update
297
+ *
298
+ * @param {string} id - WU ID being completed
299
+ * @param {string} baseDir - Base directory containing .beacon/state/
300
+ * @returns {Promise<void>}
301
+ */
302
+ export async function updateSpawnRegistryOnCompletion(id, baseDir = process.cwd()) {
303
+ try {
304
+ const store = new SpawnRegistryStore(path.join(baseDir, '.beacon', 'state'));
305
+ await store.load();
306
+ const spawnEntry = store.getByTarget(id);
307
+ // Graceful skip if no spawn entry found (legacy WU)
308
+ if (!spawnEntry) {
309
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No spawn registry entry found for ${id} (legacy WU or not spawned)`);
310
+ return;
311
+ }
312
+ // Update status to completed with completedAt timestamp
313
+ await store.updateStatus(spawnEntry.id, SpawnStatus.COMPLETED);
314
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spawn registry updated: ${id} marked as completed`);
315
+ }
316
+ catch (err) {
317
+ // Non-blocking: spawn registry update failure should not block wu:done
318
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not update spawn registry for ${id}: ${err.message}`);
319
+ }
320
+ }
321
+ // Git config keys used for user identification
322
+ const GIT_CONFIG_USER_NAME = 'user.name';
323
+ const GIT_CONFIG_USER_EMAIL = 'user.email';
324
+ // Default fallback messages
325
+ const DEFAULT_NO_REASON = '(no reason provided)';
326
+ /**
327
+ * WU-1234: Normalize username for ownership comparison
328
+ * Extracts username from email address for comparison.
329
+ * This allows tom@hellm.ai to match 'tom' assigned_to field.
330
+ *
331
+ * @param {string|null|undefined} value - Email address or username
332
+ * @returns {string} Normalized username (lowercase)
333
+ */
334
+ export function normalizeUsername(value) {
335
+ if (!value)
336
+ return '';
337
+ const str = String(value).trim();
338
+ // Extract username from email: tom@hellm.ai -> tom
339
+ // WU-1281: Using string split instead of regex
340
+ const atIndex = str.indexOf('@');
341
+ const username = atIndex > 0 ? str.slice(0, atIndex) : str;
342
+ return username.toLowerCase();
343
+ }
344
+ /**
345
+ * WU-1234: Detect if branch is already merged to main
346
+ * Checks if branch tip is an ancestor of main HEAD (i.e., already merged).
347
+ * This prevents merge loops when code was merged via emergency fix or manual merge.
348
+ *
349
+ * @param {string} branch - Lane branch name
350
+ * @returns {Promise<boolean>} True if branch is already merged to main
351
+ */
352
+ export async function isBranchAlreadyMerged(branch) {
353
+ try {
354
+ const gitAdapter = getGitForCwd();
355
+ const branchTip = (await gitAdapter.getCommitHash(branch)).trim();
356
+ const mergeBase = (await gitAdapter.mergeBase(BRANCHES.MAIN, branch)).trim();
357
+ const mainHead = (await gitAdapter.getCommitHash(BRANCHES.MAIN)).trim();
358
+ // Branch is already merged if:
359
+ // 1. Branch tip equals merge-base (branch has been rebased/merged onto main)
360
+ // 2. Branch tip is an ancestor of main HEAD
361
+ if (branchTip === mergeBase) {
362
+ // Emergency fix Session 2: Use GIT.SHA_SHORT_LENGTH constant
363
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Branch ${branch} is already merged to main\n` +
364
+ ` Branch tip: ${branchTip.substring(0, GIT.SHA_SHORT_LENGTH)}\n` +
365
+ ` Merge-base: ${mergeBase.substring(0, GIT.SHA_SHORT_LENGTH)}\n` +
366
+ ` Main HEAD: ${mainHead.substring(0, GIT.SHA_SHORT_LENGTH)}`);
367
+ return true;
368
+ }
369
+ return false;
370
+ }
371
+ catch (e) {
372
+ console.warn(`${LOG_PREFIX.DONE} Could not check if branch is already merged: ${e.message}`);
373
+ return false;
374
+ }
375
+ }
376
+ // WU-1281: isDocsOnlyByPaths removed - use shouldSkipWebTests from path-classifiers.mjs
377
+ // The validators already use shouldSkipWebTests via detectDocsOnlyByPaths wrapper.
378
+ // Keeping the export for backward compatibility but re-exporting the canonical function.
379
+ export { shouldSkipWebTests as isDocsOnlyByPaths } from '@lumenflow/core/dist/path-classifiers.js';
380
+ /**
381
+ * WU-1234: Pre-flight check for backlog state consistency
382
+ * Fails fast if the WU appears in both Done and In Progress sections.
383
+ *
384
+ * @param {string} id - WU ID to check
385
+ * @param {string} backlogPath - Path to backlog.md
386
+ * @returns {{ valid: boolean, error: string|null }}
387
+ */
388
+ export function checkBacklogConsistencyForWU(id, backlogPath) {
389
+ try {
390
+ const result = validateBacklogSync(backlogPath);
391
+ // Check if this specific WU is in both Done and In Progress
392
+ if (!result.valid) {
393
+ for (const error of result.errors) {
394
+ // Check if the error mentions both Done and In Progress AND mentions our WU
395
+ if (error.includes('Done and In Progress') && error.includes(id)) {
396
+ return {
397
+ valid: false,
398
+ error: `❌ BACKLOG STATE INCONSISTENCY: ${id} found in both Done and In Progress sections.\n\n` +
399
+ `This is an invalid state that must be fixed manually before wu:done can proceed.\n\n` +
400
+ `Fix options:\n` +
401
+ ` 1. If ${id} is truly done: Remove from In Progress in backlog.md\n` +
402
+ ` 2. If ${id} needs more work: Remove from Done in backlog.md, update WU YAML status\n\n` +
403
+ `After fixing backlog.md, retry: pnpm wu:done --id ${id}`,
404
+ };
405
+ }
406
+ }
407
+ }
408
+ return { valid: true, error: null };
409
+ }
410
+ catch (e) {
411
+ // If validation fails (e.g., file not found), warn but don't block
412
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not validate backlog consistency: ${e.message}`);
413
+ return { valid: true, error: null };
414
+ }
415
+ }
416
+ /**
417
+ * Read commitlint header-max-length from config, fallback to DEFAULTS.MAX_COMMIT_SUBJECT
418
+ * WU-1281: Using centralized constant instead of hardcoded 100
419
+ */
420
+ function getCommitHeaderLimit() {
421
+ try {
422
+ const configPath = path.join(process.cwd(), '.commitlintrc.json');
423
+ if (!existsSync(configPath))
424
+ return DEFAULTS.MAX_COMMIT_SUBJECT;
425
+ const cfg = JSON.parse(readFileSync(configPath, { encoding: FILE_SYSTEM.UTF8 }));
426
+ return cfg?.rules?.['header-max-length']?.[2] ?? DEFAULTS.MAX_COMMIT_SUBJECT;
427
+ }
428
+ catch {
429
+ return DEFAULTS.MAX_COMMIT_SUBJECT; // Fallback if config is malformed or missing
430
+ }
431
+ }
432
+ // ensureOnMain() moved to wu-helpers.mjs (WU-1256)
433
+ /**
434
+ * Ensure working tree is clean before wu:done operations.
435
+ *
436
+ * Prevents multi-agent data loss: If uncommitted files exist in main
437
+ * checkout, wu:done operations may fail mid-workflow. Agents then
438
+ * automatically "clean up" by running git reset/clean, destroying
439
+ * other agents' uncommitted work.
440
+ *
441
+ * This check HALTS wu:done immediately and guides the agent to verify
442
+ * ownership before proceeding.
443
+ *
444
+ * Context: WU-635 (multi-agent coordination)
445
+ * See: CLAUDE.md §2.2
446
+ */
447
+ async function ensureCleanWorkingTree() {
448
+ const status = await getGitForCwd().getStatus();
449
+ if (status.trim()) {
450
+ die(`Working tree is not clean. Cannot proceed with wu:done.\n\n` +
451
+ `Uncommitted changes in main checkout:\n${status}\n\n` +
452
+ `⚠️ CRITICAL: These may be another agent's work!\n\n` +
453
+ `Before proceeding:\n` +
454
+ `1. Check if these are YOUR changes (forgot to commit in main)\n` +
455
+ ` → If yes: Commit them now, then retry wu:done\n\n` +
456
+ `2. Check if these are ANOTHER AGENT's changes\n` +
457
+ ` → If yes: STOP. Coordinate with user before proceeding\n` +
458
+ ` → NEVER remove another agent's uncommitted work\n\n` +
459
+ `Multi-agent coordination: See CLAUDE.md §2.2\n\n` +
460
+ `Common causes:\n` +
461
+ ` - You forgot to commit changes before claiming a different WU\n` +
462
+ ` - Another agent is actively working in main checkout\n` +
463
+ ` - Leftover changes from previous session`);
464
+ }
465
+ }
466
+ /**
467
+ * Extract completed WU IDs from git log output.
468
+ * @param {string} logOutput - Git log output (one commit per line)
469
+ * @param {string} currentId - Current WU ID to exclude
470
+ * @returns {string[]} Array of completed WU IDs
471
+ */
472
+ function extractCompletedWUIds(logOutput, currentId) {
473
+ const wuPattern = /wu\((wu-\d+)\):/gi;
474
+ const seenIds = new Set();
475
+ const completedWUs = [];
476
+ for (const line of logOutput.split(STRING_LITERALS.NEWLINE)) {
477
+ // Only process "done" commits
478
+ if (!line.toLowerCase().includes('done'))
479
+ continue;
480
+ let match;
481
+ while ((match = wuPattern.exec(line)) !== null) {
482
+ const wuId = match[1].toUpperCase();
483
+ // Skip current WU and duplicates
484
+ if (wuId !== currentId && !seenIds.has(wuId)) {
485
+ seenIds.add(wuId);
486
+ completedWUs.push(wuId);
487
+ }
488
+ }
489
+ }
490
+ return completedWUs;
491
+ }
492
+ /**
493
+ * Build warning message for parallel completions.
494
+ */
495
+ function buildParallelWarning(id, completedWUs, baselineSha, currentSha) {
496
+ const wuList = completedWUs.map((wu) => ` • ${wu}`).join(STRING_LITERALS.NEWLINE);
497
+ return `
498
+ ${EMOJI.WARNING} PARALLEL COMPLETIONS DETECTED ${EMOJI.WARNING}
499
+
500
+ The following WUs were completed and merged to main since you claimed ${id}:
501
+
502
+ ${wuList}
503
+
504
+ This may cause rebase conflicts when wu:done attempts to merge.
505
+
506
+ Options:
507
+ 1. Proceed anyway - rebase will attempt to resolve conflicts
508
+ 2. Abort and manually rebase: git fetch origin main && git rebase origin/main
509
+ 3. Check if any completed WUs touched the same files
510
+
511
+ Baseline: ${baselineSha.substring(0, 8)}
512
+ Current: ${currentSha.substring(0, 8)}
513
+ `;
514
+ }
515
+ /**
516
+ * WU-1382: Detect parallel WU completions since claim time.
517
+ *
518
+ * When multiple agents work in parallel, one may complete a WU and merge to main
519
+ * while another is still working. This function detects such completions early,
520
+ * before wu:done attempts the merge, allowing the agent to decide whether to
521
+ * proceed (with potential rebase conflicts) or abort.
522
+ *
523
+ * @param {string} id - Current WU ID
524
+ * @param {object} doc - WU YAML document (from worktree or main)
525
+ * @returns {Promise<{hasParallelCompletions: boolean, completedWUs: string[], warning: string|null}>}
526
+ */
527
+ async function detectParallelCompletions(id, doc) {
528
+ const noParallel = { hasParallelCompletions: false, completedWUs: [], warning: null };
529
+ const baselineSha = doc.baseline_main_sha;
530
+ // If no baseline recorded (legacy WU), skip detection
531
+ if (!baselineSha) {
532
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No baseline_main_sha recorded (legacy WU) - skipping parallel detection`);
533
+ return noParallel;
534
+ }
535
+ try {
536
+ const gitAdapter = getGitForCwd();
537
+ await gitAdapter.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
538
+ const currentSha = (await gitAdapter.getCommitHash(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`)).trim();
539
+ if (currentSha === baselineSha) {
540
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} No parallel completions detected (main unchanged since claim)`);
541
+ return noParallel;
542
+ }
543
+ const logOutput = await gitAdapter.raw([
544
+ 'log',
545
+ '--oneline',
546
+ '--grep=^wu(wu-',
547
+ `${baselineSha}..${REMOTES.ORIGIN}/${BRANCHES.MAIN}`,
548
+ ]);
549
+ if (!logOutput?.trim()) {
550
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main advanced since claim but no WU completions detected`);
551
+ return noParallel;
552
+ }
553
+ const completedWUs = extractCompletedWUIds(logOutput, id);
554
+ if (completedWUs.length === 0) {
555
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main advanced since claim but no other WU completions`);
556
+ return noParallel;
557
+ }
558
+ const warning = buildParallelWarning(id, completedWUs, baselineSha, currentSha);
559
+ return { hasParallelCompletions: true, completedWUs, warning };
560
+ }
561
+ catch (err) {
562
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not detect parallel completions: ${err.message}`);
563
+ return noParallel;
564
+ }
565
+ }
566
+ /**
567
+ * Ensure main branch is up-to-date with origin before merge operations.
568
+ *
569
+ * Prevents coordination failures when Agent A pushes to main while Agent B
570
+ * is working. Without this check, Agent B's wu:done would fail with cryptic
571
+ * fast-forward errors when trying to merge.
572
+ *
573
+ * Context: WU-705 (fix agent coordination failures)
574
+ * See: CLAUDE.md §2.7
575
+ */
576
+ async function ensureMainUpToDate() {
577
+ console.log(`${LOG_PREFIX.DONE} Checking if main is up-to-date with origin...`);
578
+ try {
579
+ // Fetch latest without merging
580
+ const gitAdapter = getGitForCwd();
581
+ await gitAdapter.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
582
+ const localMain = await gitAdapter.getCommitHash(BRANCHES.MAIN);
583
+ const remoteMain = await gitAdapter.getCommitHash(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
584
+ if (localMain !== remoteMain) {
585
+ const behind = await gitAdapter.revList([
586
+ '--count',
587
+ `${BRANCHES.MAIN}..${REMOTES.ORIGIN}/${BRANCHES.MAIN}`,
588
+ ]);
589
+ const ahead = await gitAdapter.revList([
590
+ '--count',
591
+ `${REMOTES.ORIGIN}/${BRANCHES.MAIN}..${BRANCHES.MAIN}`,
592
+ ]);
593
+ die(`Main branch is out of sync with ${REMOTES.ORIGIN}.\n\n` +
594
+ `Local ${BRANCHES.MAIN} is ${behind} commits behind and ${ahead} commits ahead of ${REMOTES.ORIGIN}/${BRANCHES.MAIN}.\n\n` +
595
+ `Update main before running wu:done:\n` +
596
+ ` git pull origin main\n` +
597
+ ` # Then retry:\n` +
598
+ ` pnpm wu:done --id ${process.argv.find((a) => a.startsWith('WU-')) || 'WU-XXX'}\n\n` +
599
+ `This prevents fast-forward merge failures during wu:done completion.\n\n` +
600
+ `Why this happens:\n` +
601
+ ` - Another agent completed a WU and pushed to main\n` +
602
+ ` - Your main checkout is now behind origin/main\n` +
603
+ ` - The fast-forward merge will fail without updating first\n\n` +
604
+ `Multi-agent coordination: See CLAUDE.md §2.7`);
605
+ }
606
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main is up-to-date with origin`);
607
+ }
608
+ catch (err) {
609
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not verify main sync: ${err.message}`);
610
+ console.warn(`${LOG_PREFIX.DONE} Proceeding anyway (network issue or no remote)`);
611
+ }
612
+ }
613
+ /**
614
+ * Tripwire check: Scan commands log for violations (WU-630 detective layer)
615
+ *
616
+ * Scans .beacon/commands.log for destructive git commands executed during
617
+ * this agent session. If violations are found, aborts wu:done and displays
618
+ * remediation guidance.
619
+ *
620
+ * This is defense-in-depth: catches violations even if git shim was bypassed
621
+ * by calling /usr/bin/git directly or if PATH was not set up correctly.
622
+ *
623
+ * Context: WU-630 (detective layer, Layer 3 of 4)
624
+ * See: docs/04-operations/_frameworks/lumenflow/02-playbook.md §4.6
625
+ */
626
+ function runTripwireCheck() {
627
+ const violations = scanLogForViolations();
628
+ if (violations.length === 0) {
629
+ return; // All clear
630
+ }
631
+ // Violations detected - format error message with remediation
632
+ console.error('\n⛔ VIOLATION DETECTED: Destructive Git Commands on Main\n');
633
+ console.error('The following forbidden git commands were executed during this session:\n');
634
+ violations.forEach((v, i) => {
635
+ console.error(` ${i + 1}. ${v.command}`);
636
+ console.error(` Branch: ${v.branch}`);
637
+ console.error(` Worktree: ${v.worktree}`);
638
+ console.error(` Time: ${v.timestamp}\n`);
639
+ });
640
+ console.error(`\nTotal: ${violations.length} violations\n`);
641
+ // Remediation guidance based on violation type
642
+ console.error("⚠️ CRITICAL: These commands may have destroyed other agents' work!\n");
643
+ console.error('Remediation Steps:\n');
644
+ const hasReset = violations.some((v) => v.command.includes('reset --hard'));
645
+ const hasStash = violations.some((v) => v.command.includes('stash'));
646
+ const hasClean = violations.some((v) => v.command.includes('clean'));
647
+ if (hasReset) {
648
+ console.error('📋 git reset --hard detected:');
649
+ console.error(' 1. Check git reflog to recover lost commits:');
650
+ console.error(' git reflog');
651
+ console.error(' git reset --hard HEAD@{N} (where N is the commit before reset)');
652
+ console.error(' 2. If reflog shows lost work, restore it immediately\n');
653
+ }
654
+ if (hasStash) {
655
+ console.error('📋 git stash detected:');
656
+ console.error(" 1. Check if stash contains other agents' work:");
657
+ console.error(' git stash list');
658
+ console.error(' git stash show -p stash@{0}');
659
+ console.error(' 2. If stash contains work, pop it back:');
660
+ console.error(' git stash pop\n');
661
+ }
662
+ if (hasClean) {
663
+ console.error('📋 git clean detected:');
664
+ console.error(' 1. Deleted files may not be recoverable');
665
+ console.error(' 2. Check git status for any remaining untracked files');
666
+ console.error(' 3. Escalate to human if critical files were deleted\n');
667
+ }
668
+ console.error('📖 See detailed recovery steps:');
669
+ console.error(' docs/04-operations/_frameworks/lumenflow/02-playbook.md §4.6\n');
670
+ console.error('🚫 DO NOT proceed with wu:done until violations are remediated.\n');
671
+ console.error('Fix violations first, then retry wu:done.\n');
672
+ // Also rotate log (cleanup old entries)
673
+ rotateLog();
674
+ process.exit(EXIT_CODES.ERROR);
675
+ }
676
+ async function listStaged() {
677
+ // WU-1235: Use getGitForCwd() to capture current directory (worktree after chdir)
678
+ const gitCwd = getGitForCwd();
679
+ const raw = await gitCwd.raw(['diff', '--cached', '--name-only']);
680
+ return raw ? raw.split(/\r?\n/).filter(Boolean) : [];
681
+ }
682
+ // In --no-auto mode, allow a safe no-op: if NONE of the expected files are staged,
683
+ // treat as already-synchronised and continue. If SOME are staged and SOME missing,
684
+ // still fail with guidance.
685
+ async function ensureNoAutoStagedOrNoop(paths) {
686
+ const staged = await listStaged();
687
+ const isStaged = (p) => staged.some((name) => name === p || name.startsWith(`${p}/`));
688
+ const present = paths.filter(Boolean).filter((p) => isStaged(p));
689
+ if (present.length === 0) {
690
+ console.log(`${LOG_PREFIX.DONE} No staged changes detected for --no-auto; treating as no-op finalisation (repo already in done state)`);
691
+ return { noop: true };
692
+ }
693
+ const missing = paths.filter(Boolean).filter((p) => !isStaged(p));
694
+ if (missing.length > 0) {
695
+ die(`Stage updates for: ${missing.join(', ')}`);
696
+ }
697
+ return { noop: false };
698
+ }
699
+ export function emitTelemetry(event) {
700
+ const logPath = path.join('.beacon', 'flow.log');
701
+ const logDir = path.dirname(logPath);
702
+ if (!existsSync(logDir))
703
+ mkdirSync(logDir, { recursive: true });
704
+ const line = JSON.stringify({ timestamp: new Date().toISOString(), ...event });
705
+ appendFileSync(logPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
706
+ }
707
+ async function auditSkipGates(id, reason, fixWU, worktreePath) {
708
+ const auditPath = path.join('.beacon', 'skip-gates-audit.log');
709
+ const auditDir = path.dirname(auditPath);
710
+ if (!existsSync(auditDir))
711
+ mkdirSync(auditDir, { recursive: true });
712
+ const gitAdapter = getGitForCwd();
713
+ const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
714
+ const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
715
+ const commitHash = await gitAdapter.getCommitHash();
716
+ const entry = {
717
+ timestamp: new Date().toISOString(),
718
+ wu_id: id,
719
+ reason: reason || DEFAULT_NO_REASON,
720
+ fix_wu: fixWU || '(no fix WU specified)',
721
+ worktree: worktreePath || '(unknown)',
722
+ git_user: `${userName.trim()} <${userEmail.trim()}>`,
723
+ git_commit: commitHash.trim(),
724
+ };
725
+ const line = JSON.stringify(entry);
726
+ appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
727
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Skip-gates event logged to ${auditPath}`);
728
+ }
729
+ /**
730
+ * Audit trail for --skip-cos-gates (COS v1.3 §7)
731
+ */
732
+ async function auditSkipCosGates(id, reason) {
733
+ const auditPath = path.join('.beacon', 'skip-cos-gates-audit.log');
734
+ const auditDir = path.dirname(auditPath);
735
+ if (!existsSync(auditDir))
736
+ mkdirSync(auditDir, { recursive: true });
737
+ const gitAdapter = getGitForCwd();
738
+ const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
739
+ const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
740
+ const commitHash = await gitAdapter.getCommitHash();
741
+ const entry = {
742
+ timestamp: new Date().toISOString(),
743
+ wu_id: id,
744
+ reason: reason || DEFAULT_NO_REASON,
745
+ git_user: `${userName.trim()} <${userEmail.trim()}>`,
746
+ git_commit: commitHash.trim(),
747
+ };
748
+ const line = JSON.stringify(entry);
749
+ appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
750
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Skip-COS-gates event logged to ${auditPath}`);
751
+ }
752
+ // WU-2308: validateAllPreCommitHooks moved to wu-done-validators.mjs
753
+ // Now accepts worktreePath parameter to run audit from worktree context
754
+ /**
755
+ * Check if node_modules in worktree may be stale
756
+ * Detects when package.json differs between main and worktree, which indicates
757
+ * dependencies were added/removed but pnpm install may not have run in worktree.
758
+ * This prevents confusing typecheck failures due to missing dependencies.
759
+ * @param {string} worktreePath - Path to worktree
760
+ */
761
+ function checkNodeModulesStaleness(worktreePath) {
762
+ try {
763
+ const mainPackageJson = path.resolve('package.json');
764
+ const worktreePackageJson = path.resolve(worktreePath, 'package.json');
765
+ if (!existsSync(mainPackageJson) || !existsSync(worktreePackageJson)) {
766
+ // No package.json to compare
767
+ return;
768
+ }
769
+ const mainContent = readFileSync(mainPackageJson, {
770
+ encoding: FILE_SYSTEM.UTF8,
771
+ });
772
+ const worktreeContent = readFileSync(worktreePackageJson, {
773
+ encoding: FILE_SYSTEM.UTF8,
774
+ });
775
+ // Compare package.json files
776
+ if (mainContent !== worktreeContent) {
777
+ const worktreeNodeModules = path.resolve(worktreePath, 'node_modules');
778
+ // Check if node_modules exists and when it was last modified
779
+ if (existsSync(worktreeNodeModules)) {
780
+ const nodeModulesStat = statSync(worktreeNodeModules);
781
+ const packageJsonStat = statSync(worktreePackageJson);
782
+ // If package.json is newer than node_modules, dependencies may be stale
783
+ if (packageJsonStat.mtimeMs > nodeModulesStat.mtimeMs) {
784
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WARNING: Potentially stale node_modules detected\n\n` +
785
+ ` package.json in worktree differs from main checkout\n` +
786
+ ` node_modules was last modified: ${nodeModulesStat.mtime.toISOString()}\n` +
787
+ ` package.json was last modified: ${packageJsonStat.mtime.toISOString()}\n\n` +
788
+ ` If gates fail with missing dependencies/types, run:\n` +
789
+ ` cd ${worktreePath}\n` +
790
+ ` pnpm install\n` +
791
+ ` cd -\n` +
792
+ ` pnpm wu:done --id <WU-ID>\n`);
793
+ }
794
+ }
795
+ else {
796
+ // node_modules doesn't exist at all
797
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WARNING: node_modules missing in worktree\n\n` +
798
+ ` package.json in worktree differs from main checkout\n` +
799
+ ` but node_modules directory does not exist\n\n` +
800
+ ` If gates fail with missing dependencies/types, run:\n` +
801
+ ` cd ${worktreePath}\n` +
802
+ ` pnpm install\n` +
803
+ ` cd -\n` +
804
+ ` pnpm wu:done --id <WU-ID>\n`);
805
+ }
806
+ }
807
+ }
808
+ catch (e) {
809
+ // Non-critical check - just warn if it fails
810
+ console.warn(`${LOG_PREFIX.DONE} Could not check node_modules staleness: ${e.message}`);
811
+ }
812
+ }
813
+ function runGatesInWorktree(worktreePath, id, isDocsOnly = false) {
814
+ console.log(`\n${LOG_PREFIX.DONE} Running gates in worktree: ${worktreePath}`);
815
+ // Check for stale node_modules before running gates (prevents confusing failures)
816
+ checkNodeModulesStaleness(worktreePath);
817
+ const gatesCmd = isDocsOnly
818
+ ? `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`
819
+ : `${PKG_MANAGER} ${SCRIPTS.GATES}`;
820
+ if (isDocsOnly) {
821
+ console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
822
+ }
823
+ const startTime = Date.now();
824
+ try {
825
+ // WU-1230: Pass WU_ID to validator for context-aware validation
826
+ execSync(gatesCmd, {
827
+ cwd: worktreePath,
828
+ stdio: 'inherit',
829
+ env: { ...process.env, WU_ID: id },
830
+ });
831
+ const duration = Date.now() - startTime;
832
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Gates passed in ${prettyMs(duration)}`);
833
+ emitTelemetry({ script: 'wu-done', wu_id: id, step: 'gates', ok: true, duration_ms: duration });
834
+ return true;
835
+ }
836
+ catch {
837
+ const duration = Date.now() - startTime;
838
+ emitTelemetry({
839
+ script: 'wu-done',
840
+ wu_id: id,
841
+ step: 'gates',
842
+ ok: false,
843
+ duration_ms: duration,
844
+ });
845
+ // WU-1280: Prominent error summary box (visible after ~130k chars of gate output)
846
+ // WU-1281: Extracted to helper using pretty-ms for duration formatting
847
+ printGateFailureBox({ id, location: worktreePath, durationMs: duration, isWorktreeMode: true });
848
+ die(`Gates failed in ${worktreePath}. Fix issues in the worktree and try again.`);
849
+ }
850
+ }
851
+ async function validateStagedFiles(id, isDocsOnly = false) {
852
+ const staged = await listStaged();
853
+ // WU-1740: Include wu-events.jsonl to persist state store events
854
+ const whitelist = [
855
+ `docs/04-operations/tasks/wu/${id}.yaml`,
856
+ 'docs/04-operations/tasks/status.md',
857
+ 'docs/04-operations/tasks/backlog.md',
858
+ WU_EVENTS_PATH,
859
+ ];
860
+ if (isDocsOnly) {
861
+ // For docs-only WUs, validate that all staged files are in allowed paths
862
+ const docsResult = validateDocsOnly(staged);
863
+ if (!docsResult.valid) {
864
+ die(`Docs-only WU cannot modify code files:\n ${docsResult.violations.join(`${STRING_LITERALS.NEWLINE} `)}\n\n${getAllowedPathsDescription()}`);
865
+ }
866
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Docs-only path validation passed`);
867
+ return;
868
+ }
869
+ const unexpected = staged.filter((file) => {
870
+ // Whitelist exact matches
871
+ if (whitelist.includes(file))
872
+ return false;
873
+ // Whitelist .beacon/stamps/** pattern
874
+ if (file.startsWith('.beacon/stamps/'))
875
+ return false;
876
+ return true;
877
+ });
878
+ if (unexpected.length > 0) {
879
+ const otherWuYamlOnly = unexpected.every((f) => /^docs\/04-operations\/tasks\/wu\/WU-\d+\.yaml$/.test(f));
880
+ if (otherWuYamlOnly) {
881
+ console.warn(`${LOG_PREFIX.DONE} Warning: other WU YAMLs are staged; proceeding and committing only current WU files.`);
882
+ }
883
+ else {
884
+ die(`Unexpected files staged (only current WU YAML, status.md, backlog.md, .beacon/stamps/<id>.done allowed):\n ${unexpected.join(`${STRING_LITERALS.NEWLINE} `)}`);
885
+ }
886
+ }
887
+ }
888
+ // Note: updateStatusRemoveInProgress, addToStatusCompleted, and moveWUToDoneBacklog
889
+ // have been extracted to tools/lib/wu-status-updater.mjs and imported above (WU-1163)
890
+ //
891
+ // Note: ensureStamp has been replaced with createStamp from tools/lib/stamp-utils.mjs (WU-1163)
892
+ //
893
+ // Note: readWUPreferWorktree, detectCurrentWorktree, defaultWorktreeFrom, detectWorkspaceMode,
894
+ // defaultBranchFrom, branchExists, runCleanup have been extracted to
895
+ // tools/lib/wu-done-validators.mjs and imported above (WU-1215)
896
+ /**
897
+ * Validate Branch-Only mode requirements before proceeding
898
+ * @param {string} laneBranch - Expected lane branch name
899
+ * @returns {{valid: boolean, error: string|null}}
900
+ */
901
+ async function validateBranchOnlyMode(laneBranch) {
902
+ // Check we're on the correct lane branch
903
+ const gitAdapter = getGitForCwd();
904
+ const currentBranch = await gitAdapter.getCurrentBranch();
905
+ if (currentBranch !== laneBranch) {
906
+ return {
907
+ valid: false,
908
+ error: `Branch-Only mode error: Not on the lane branch.\n\n` +
909
+ `Expected branch: ${laneBranch}\n` +
910
+ `Current branch: ${currentBranch}\n\n` +
911
+ `Fix: git checkout ${laneBranch}`,
912
+ };
913
+ }
914
+ // Check working directory is clean
915
+ const status = await gitAdapter.getStatus();
916
+ if (status) {
917
+ return {
918
+ valid: false,
919
+ error: `Branch-Only mode error: Working directory is not clean.\n\n` +
920
+ `Uncommitted changes detected:\n${status}\n\n` +
921
+ `Fix: Commit all changes before running wu:done\n` +
922
+ ` git add -A\n` +
923
+ ` git commit -m "wu(wu-xxx): ..."\n` +
924
+ ` git push origin ${laneBranch}`,
925
+ };
926
+ }
927
+ return { valid: true, error: null };
928
+ }
929
+ /**
930
+ * WU-755 + WU-1230: Record transaction state for rollback
931
+ * @param {string} id - WU ID
932
+ * @param {string} wuPath - Path to WU YAML
933
+ * @param {string} stampPath - Path to stamp file
934
+ * @param {string} backlogPath - Path to backlog.md (WU-1230)
935
+ * @param {string} statusPath - Path to status.md (WU-1230)
936
+ * @returns {object} - Transaction state for rollback
937
+ */
938
+ function recordTransactionState(id, wuPath, stampPath, backlogPath, statusPath) {
939
+ const gitAdapter = getGitForCwd();
940
+ return {
941
+ id,
942
+ timestamp: new Date().toISOString(),
943
+ wuYamlContent: existsSync(wuPath)
944
+ ? readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 })
945
+ : null,
946
+ stampExisted: existsSync(stampPath),
947
+ backlogContent: existsSync(backlogPath)
948
+ ? readFileSync(backlogPath, { encoding: FILE_SYSTEM.UTF8 })
949
+ : null,
950
+ statusContent: existsSync(statusPath)
951
+ ? readFileSync(statusPath, { encoding: FILE_SYSTEM.UTF8 })
952
+ : null,
953
+ mainSHA: gitAdapter.getCommitHash(),
954
+ laneBranch: gitAdapter.getCurrentBranch(),
955
+ };
956
+ }
957
+ /**
958
+ * WU-755 + WU-1230: Rollback transaction on failure
959
+ * @param {object} txState - Transaction state from recordTransactionState
960
+ * @param {string} wuPath - Path to WU YAML
961
+ * @param {string} stampPath - Path to stamp file
962
+ * @param {string} backlogPath - Path to backlog.md (WU-1230)
963
+ * @param {string} statusPath - Path to status.md (WU-1230)
964
+ */
965
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
966
+ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, statusPath) {
967
+ console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} ROLLING BACK TRANSACTION (WU-755 + WU-1230 + WU-1255 + WU-1280)...`);
968
+ // WU-1280: ATOMIC ROLLBACK - Clean git state FIRST, then restore files
969
+ // Previous order (restore → git checkout) caused issues:
970
+ // - git checkout -- . would UNDO file restorations
971
+ // - Left messy state with staged + unstaged conflicts
972
+ //
973
+ // New order:
974
+ // 1. Unstage everything (git reset HEAD)
975
+ // 2. Discard working tree changes (git checkout -- .)
976
+ // 3. Remove stamp if created
977
+ // 4. THEN restore files from txState
978
+ // Step 1: Unstage any staged files FIRST
979
+ // Emergency fix Session 2: Use git-adapter instead of raw execSync
980
+ try {
981
+ const gitAdapter = getGitForCwd();
982
+ await gitAdapter.raw(['reset', 'HEAD']);
983
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Unstaged all files`);
984
+ }
985
+ catch {
986
+ // Ignore - may not have anything staged
987
+ }
988
+ // Step 2: Discard working directory changes (reset to last commit)
989
+ // Emergency fix Session 2: Use git-adapter instead of raw execSync
990
+ try {
991
+ const gitAdapter = getGitForCwd();
992
+ await gitAdapter.raw(['checkout', '--', '.']);
993
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Reset working tree to HEAD`);
994
+ }
995
+ catch {
996
+ // Ignore - may not have anything to discard
997
+ }
998
+ // Step 3: Remove stamp unconditionally if it exists (WU-1440)
999
+ // Previous behavior only removed if !stampExisted, but that flag could be wrong
1000
+ // due to edge cases. Unconditional removal ensures clean rollback state.
1001
+ if (existsSync(stampPath)) {
1002
+ try {
1003
+ unlinkSync(stampPath);
1004
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Removed ${stampPath}`);
1005
+ }
1006
+ catch (err) {
1007
+ console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to remove stamp: ${err.message}`);
1008
+ }
1009
+ }
1010
+ // Step 4: Restore files from txState (AFTER git cleanup)
1011
+ // Build list of files to restore with per-file error tracking (ref: WU-1255)
1012
+ const filesToRestore = [];
1013
+ // Restore backlog.md (ref: WU-1230)
1014
+ if (txState.backlogContent && existsSync(backlogPath)) {
1015
+ filesToRestore.push({ name: 'backlog.md', path: backlogPath, content: txState.backlogContent });
1016
+ }
1017
+ // Restore status.md (ref: WU-1230)
1018
+ if (txState.statusContent && existsSync(statusPath)) {
1019
+ filesToRestore.push({ name: 'status.md', path: statusPath, content: txState.statusContent });
1020
+ }
1021
+ // Restore WU YAML if it was modified
1022
+ if (txState.wuYamlContent && existsSync(wuPath)) {
1023
+ filesToRestore.push({ name: 'WU YAML', path: wuPath, content: txState.wuYamlContent });
1024
+ }
1025
+ // WU-1255: Use rollbackFiles utility for per-file error tracking
1026
+ const restoreResult = rollbackFiles(filesToRestore);
1027
+ // Log results
1028
+ for (const name of restoreResult.restored) {
1029
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Restored ${name}`);
1030
+ }
1031
+ for (const err of restoreResult.errors) {
1032
+ console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to restore ${err.name}: ${err.error}`);
1033
+ }
1034
+ // Reset main to original SHA if we're on main
1035
+ try {
1036
+ const gitAdapter = getGitForCwd();
1037
+ const currentBranch = await gitAdapter.getCurrentBranch();
1038
+ if (currentBranch === BRANCHES.MAIN) {
1039
+ const currentSHA = await gitAdapter.getCommitHash();
1040
+ if (currentSHA !== txState.mainSHA) {
1041
+ await gitAdapter.reset(txState.mainSHA, { hard: true });
1042
+ // Emergency fix Session 2: Use GIT.SHA_SHORT_LENGTH constant
1043
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Reset main to ${txState.mainSHA.slice(0, GIT.SHA_SHORT_LENGTH)}`);
1044
+ }
1045
+ }
1046
+ }
1047
+ catch (e) {
1048
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not reset main: ${e.message}`);
1049
+ }
1050
+ // WU-1280: Verify clean git status after rollback
1051
+ // WU-1281: Extracted to helper to fix repeated parsing and magic number
1052
+ // Emergency fix Session 2: Use git-adapter instead of raw execSync
1053
+ try {
1054
+ const gitAdapter = getGitForCwd();
1055
+ const statusOutput = (await gitAdapter.raw(['status', '--porcelain'])).trim();
1056
+ if (statusOutput) {
1057
+ printStatusPreview(statusOutput);
1058
+ }
1059
+ else {
1060
+ console.log(`${LOG_PREFIX.DONE} ✅ Working tree is clean`);
1061
+ }
1062
+ }
1063
+ catch {
1064
+ // Ignore - git status may fail in edge cases
1065
+ }
1066
+ // WU-1255: Report final status with all errors
1067
+ if (restoreResult.errors.length > 0) {
1068
+ console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Rollback completed with ${restoreResult.errors.length} error(s):`);
1069
+ for (const err of restoreResult.errors) {
1070
+ console.error(` - ${err.name}: ${err.error}`);
1071
+ }
1072
+ console.error(`${LOG_PREFIX.DONE} Manual intervention required for failed files`);
1073
+ console.error(`${LOG_PREFIX.DONE} See playbook.md section 12 "Scenario D" for recovery steps`);
1074
+ }
1075
+ else {
1076
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Rollback complete - WU state fully reverted (no infinite loop)`);
1077
+ }
1078
+ }
1079
+ /**
1080
+ * Validate WU code paths for incomplete work markers and Mock classes
1081
+ * @param {object} doc - WU YAML document
1082
+ * @param {string} id - WU ID
1083
+ * @param {boolean} allowTodo - Allow incomplete work markers (with warning)
1084
+ * @param {string|null} worktreePath - Path to worktree to validate files from
1085
+ */
1086
+ function runWUValidator(doc, id, allowTodo = false, worktreePath = null) {
1087
+ console.log(`\n${LOG_PREFIX.DONE} Running WU validator for ${id}...`);
1088
+ // Check if WU has code_paths defined
1089
+ const codePaths = doc.code_paths || [];
1090
+ if (codePaths.length === 0) {
1091
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} No code_paths defined in WU YAML, skipping validator`);
1092
+ return;
1093
+ }
1094
+ // Check if incomplete work flag requires justification in notes
1095
+ if (allowTodo) {
1096
+ // Handle both string and array formats for notes (WU-654)
1097
+ let notesText = '';
1098
+ if (typeof doc.notes === 'string') {
1099
+ notesText = doc.notes;
1100
+ }
1101
+ else if (Array.isArray(doc.notes)) {
1102
+ notesText = doc.notes.join(STRING_LITERALS.NEWLINE);
1103
+ }
1104
+ const hasJustification = notesText.toLowerCase().includes('todo') || notesText.toLowerCase().includes('allow-todo');
1105
+ if (!hasJustification) {
1106
+ die('--allow-todo flag requires justification in WU YAML notes field.\n' +
1107
+ 'Add a note explaining why TODOs are acceptable for this WU.');
1108
+ }
1109
+ }
1110
+ // Validate from worktree if available (ensures we check the lane branch code)
1111
+ const validateOptions = { allowTodos: allowTodo };
1112
+ if (worktreePath && existsSync(worktreePath)) {
1113
+ validateOptions.worktreePath = worktreePath;
1114
+ console.log(`${LOG_PREFIX.DONE} Validating code paths from worktree: ${worktreePath}`);
1115
+ }
1116
+ // Run validation
1117
+ const result = validateWUCodePaths(codePaths, validateOptions);
1118
+ // Display warnings
1119
+ if (result.warnings.length > 0) {
1120
+ console.log('\n⚠️ WU VALIDATOR WARNINGS:');
1121
+ result.warnings.forEach((warning) => console.log(warning));
1122
+ }
1123
+ // Handle errors
1124
+ if (!result.valid) {
1125
+ console.log('\n❌ WU VALIDATOR FAILED:');
1126
+ result.errors.forEach((error) => console.log(error));
1127
+ console.log('\nFix these issues before marking WU as done.');
1128
+ console.log('Alternatively, use --allow-todo if TODOs are acceptable (requires justification in notes).');
1129
+ die('WU validation failed. See errors above.');
1130
+ }
1131
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU validator passed`);
1132
+ }
1133
+ /**
1134
+ * GUARDRAIL 2: Enforce ownership semantics in wu:done
1135
+ *
1136
+ * Validates that the current user owns the WU before allowing completion.
1137
+ * Prevents agents/humans from finishing WUs they do not own.
1138
+ *
1139
+ * @param {string} id - WU ID
1140
+ * @param {object} doc - WU YAML document
1141
+ * @param {string|null} worktreePath - Expected worktree path
1142
+ * @param {boolean} overrideOwner - Override flag (requires reason)
1143
+ * @param {string|null} overrideReason - Reason for override
1144
+ * @returns {{valid: boolean, error: string|null, auditEntry: object|null}}
1145
+ */
1146
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1147
+ async function checkOwnership(id, doc, worktreePath, overrideOwner = false, overrideReason = null) {
1148
+ // Missing worktree means WU was not claimed properly (unless escape hatch applies)
1149
+ if (!worktreePath || !existsSync(worktreePath)) {
1150
+ return {
1151
+ valid: false,
1152
+ error: `Missing worktree for ${id}.\n\n` +
1153
+ `Expected worktree at: ${worktreePath || 'unknown'}\n\n` +
1154
+ `Worktrees are required for proper WU completion in Worktree mode.\n` +
1155
+ `If the worktree was removed, recreate it and retry, or use --skip-gates with justification.`,
1156
+ auditEntry: null,
1157
+ };
1158
+ }
1159
+ // Get assigned owner from WU YAML - read directly from worktree to ensure we get the lane branch version
1160
+ let assignedTo = doc.assigned_to || null;
1161
+ if (!assignedTo && worktreePath) {
1162
+ // Fallback: Read directly from worktree YAML if not present in doc (fixes WU-1106)
1163
+ const wtWUPath = path.join(worktreePath, WU_PATHS.WU(id));
1164
+ if (existsSync(wtWUPath)) {
1165
+ try {
1166
+ const text = readFileSync(wtWUPath, { encoding: FILE_SYSTEM.UTF8 });
1167
+ const wtDoc = parseYAML(text);
1168
+ assignedTo = wtDoc?.assigned_to || null;
1169
+ if (assignedTo) {
1170
+ console.log(`${LOG_PREFIX.DONE} Note: Read assigned_to from worktree YAML (not found in main)`);
1171
+ }
1172
+ }
1173
+ catch (err) {
1174
+ console.warn(`${LOG_PREFIX.DONE} Warning: Failed to read assigned_to from worktree: ${err.message}`);
1175
+ }
1176
+ }
1177
+ }
1178
+ if (!assignedTo) {
1179
+ return {
1180
+ valid: false,
1181
+ error: `WU ${id} has no assigned_to field.\n\n` +
1182
+ `This WU was claimed before ownership tracking was implemented.\n` +
1183
+ `To complete this WU:\n` +
1184
+ ` 1. Add assigned_to: <your-email> to ${id}.yaml\n` +
1185
+ ` 2. Commit the change\n` +
1186
+ ` 3. Re-run: pnpm wu:done --id ${id}`,
1187
+ auditEntry: null,
1188
+ };
1189
+ }
1190
+ // Get current user identity
1191
+ let currentUser = null;
1192
+ try {
1193
+ currentUser = (await getGitForCwd().getConfigValue(GIT_CONFIG_USER_EMAIL)).trim();
1194
+ }
1195
+ catch {
1196
+ // Fallback to environment variable
1197
+ currentUser = process.env.GIT_USER || process.env.USER || null;
1198
+ }
1199
+ if (!currentUser) {
1200
+ return {
1201
+ valid: false,
1202
+ error: `Cannot determine current user identity.\n\n` +
1203
+ `Set git user.email or GIT_USER environment variable.`,
1204
+ auditEntry: null,
1205
+ };
1206
+ }
1207
+ // WU-1234: Normalize usernames for comparison (allows email vs username match)
1208
+ // e.g., tom@hellm.ai matches 'tom' assigned_to field
1209
+ const normalizedAssigned = normalizeUsername(assignedTo);
1210
+ const normalizedCurrent = normalizeUsername(currentUser);
1211
+ const isOwner = normalizedAssigned === normalizedCurrent;
1212
+ if (isOwner) {
1213
+ // Owner is completing their own WU - allow
1214
+ if (assignedTo !== currentUser) {
1215
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Ownership match via normalization: "${assignedTo}" == "${currentUser}"`);
1216
+ }
1217
+ return { valid: true, error: null, auditEntry: null };
1218
+ }
1219
+ // Not the owner - check for override
1220
+ if (overrideOwner) {
1221
+ if (!overrideReason) {
1222
+ return {
1223
+ valid: false,
1224
+ error: `--override-owner requires --reason "<why you're completing someone else's WU>"`,
1225
+ auditEntry: null,
1226
+ };
1227
+ }
1228
+ // Create audit entry
1229
+ const auditEntry = {
1230
+ timestamp: new Date().toISOString(),
1231
+ wu_id: id,
1232
+ assigned_to: assignedTo,
1233
+ completed_by: currentUser,
1234
+ reason: overrideReason,
1235
+ git_commit: (await getGitForCwd().getCommitHash()).trim(),
1236
+ };
1237
+ console.log(`\n⚠️ --override-owner: Completing WU assigned to someone else`);
1238
+ console.log(` Assigned to: ${assignedTo}`);
1239
+ console.log(` Completed by: ${currentUser}`);
1240
+ console.log(` Reason: ${overrideReason}\n`);
1241
+ return { valid: true, error: null, auditEntry };
1242
+ }
1243
+ // Not the owner and no override - block
1244
+ return {
1245
+ valid: false,
1246
+ error: `\n❌ OWNERSHIP VIOLATION: ${id} is assigned to someone else\n\n` +
1247
+ ` Assigned to: ${assignedTo}\n` +
1248
+ ` Current user: ${currentUser}\n\n` +
1249
+ ` You cannot complete WUs you do not own.\n\n` +
1250
+ ` 📋 Options:\n` +
1251
+ ` 1. Contact ${assignedTo} to complete the WU\n` +
1252
+ ` 2. Reassign the WU to yourself in ${id}.yaml (requires approval)\n` +
1253
+ ` 3. Add co_assigned field for pairing (requires approval)\n\n` +
1254
+ ` ⚠️ To override (use with extreme caution):\n` +
1255
+ ` pnpm wu:done --id ${id} --override-owner --reason "<why>"\n\n` +
1256
+ ` AGENTS: NEVER use --override-owner without explicit instruction.\n` +
1257
+ ` Language protocol: "pick up WU-${id.replace('WU-', '')}" = READ ONLY.\n`,
1258
+ auditEntry: null,
1259
+ };
1260
+ }
1261
+ /**
1262
+ * Log ownership override to audit trail
1263
+ * @param {object} auditEntry - Audit entry to log
1264
+ */
1265
+ function auditOwnershipOverride(auditEntry) {
1266
+ const auditPath = path.join('.beacon', 'ownership-override-audit.log');
1267
+ const auditDir = path.dirname(auditPath);
1268
+ if (!existsSync(auditDir))
1269
+ mkdirSync(auditDir, { recursive: true });
1270
+ const line = JSON.stringify(auditEntry);
1271
+ appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
1272
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Ownership override logged to ${auditPath}`);
1273
+ }
1274
+ /**
1275
+ * Execute pre-flight checks before gates
1276
+ * Extracted from main() to reduce complexity (WU-1215 Phase 2 Extraction #3)
1277
+ * @param {object} params - Parameters
1278
+ * @param {string} params.id - WU ID
1279
+ * @param {object} params.args - Parsed CLI arguments
1280
+ * @param {boolean} params.isBranchOnly - Whether in branch-only mode
1281
+ * @param {boolean} params.isDocsOnly - Whether this is a docs-only WU
1282
+ * @param {object} params.docMain - Main WU YAML document
1283
+ * @param {object} params.docForValidation - WU YAML document to validate (worktree or main)
1284
+ * @param {string|null} params.derivedWorktree - Derived worktree path
1285
+ * @returns {Promise<{title: string, docForValidation: object}>} Updated title and doc
1286
+ */
1287
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1288
+ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docMain, docForValidation, derivedWorktree, }) {
1289
+ // YAML schema validation
1290
+ console.log(`${LOG_PREFIX.DONE} Validating WU YAML structure...`);
1291
+ const schemaResult = validateWU(docForValidation);
1292
+ if (!schemaResult.success) {
1293
+ const errors = schemaResult.error.issues
1294
+ .map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)
1295
+ .join(STRING_LITERALS.NEWLINE);
1296
+ die(`❌ WU YAML validation failed:\n\n${errors}\n\nFix these issues before running wu:done`);
1297
+ }
1298
+ // Additional done-specific validation
1299
+ if (docForValidation.status === WU_STATUS.DONE) {
1300
+ const doneResult = validateDoneWU(schemaResult.data);
1301
+ if (!doneResult.valid) {
1302
+ die(`❌ WU not ready for done status:\n\n${doneResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE)}`);
1303
+ }
1304
+ }
1305
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU YAML validation passed`);
1306
+ // WU-2079: Approval gate validation
1307
+ // Ensures required approvals are present before allowing completion
1308
+ console.log(`${LOG_PREFIX.DONE} Checking approval gates...`);
1309
+ const approvalResult = validateApprovalGates(schemaResult.data);
1310
+ if (!approvalResult.valid) {
1311
+ die(`❌ Approval gates not satisfied:\n\n${approvalResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE)}\n\n` +
1312
+ `📋 To fix:\n` +
1313
+ ` 1. Request approval from the required role(s)\n` +
1314
+ ` 2. Add their email(s) to the 'approved_by' field in the WU YAML\n` +
1315
+ ` 3. Re-run: pnpm wu:done --id ${id}\n\n` +
1316
+ ` See docs/04-operations/governance/project-governance.md for role definitions.`);
1317
+ }
1318
+ // Log advisory warnings (non-blocking)
1319
+ if (approvalResult.warnings.length > 0) {
1320
+ approvalResult.warnings.forEach((w) => {
1321
+ console.warn(`${LOG_PREFIX.DONE} ⚠️ ${w}`);
1322
+ });
1323
+ }
1324
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Approval gates passed`);
1325
+ // WU-1805: Preflight code_paths and test_paths validation
1326
+ // Run BEFORE gates to catch YAML mismatches early (saves time vs. discovering after full gate run)
1327
+ const preflightResult = await executePreflightCodePathValidation(id, {
1328
+ rootDir: process.cwd(),
1329
+ worktreePath: derivedWorktree,
1330
+ });
1331
+ if (!preflightResult.valid) {
1332
+ const errorMessage = buildPreflightCodePathErrorMessage(id, preflightResult);
1333
+ die(errorMessage);
1334
+ }
1335
+ // WU-2310: Preflight type vs code_paths validation
1336
+ // Run BEFORE transaction to prevent documentation WUs with code paths from failing at git commit
1337
+ console.log(`${LOG_PREFIX.DONE} Validating type vs code_paths (WU-2310)...`);
1338
+ const typeVsCodePathsResult = validateTypeVsCodePathsPreflight(docForValidation);
1339
+ if (!typeVsCodePathsResult.valid) {
1340
+ const errorMessage = buildTypeVsCodePathsErrorMessage(id, typeVsCodePathsResult.blockedPaths);
1341
+ die(errorMessage);
1342
+ }
1343
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Type vs code_paths validation passed`);
1344
+ // Tripwire: Scan commands log for violations
1345
+ runTripwireCheck();
1346
+ // WU-1234: Pre-flight backlog consistency check
1347
+ // Fail fast if WU is in both Done and In Progress sections
1348
+ console.log(`${LOG_PREFIX.DONE} Checking backlog consistency...`);
1349
+ const backlogPath = WU_PATHS.BACKLOG();
1350
+ const backlogConsistency = checkBacklogConsistencyForWU(id, backlogPath);
1351
+ if (!backlogConsistency.valid) {
1352
+ die(backlogConsistency.error);
1353
+ }
1354
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Backlog consistency check passed`);
1355
+ // WU-1276: Pre-flight WU state consistency check
1356
+ // Layer 2 defense-in-depth: fail fast if WU has pre-existing inconsistencies
1357
+ console.log(`${LOG_PREFIX.DONE} Checking WU state consistency...`);
1358
+ const stateCheck = await checkWUConsistency(id);
1359
+ if (!stateCheck.valid) {
1360
+ const errors = stateCheck.errors
1361
+ .map((e) => ` - ${e.type}: ${e.description}`)
1362
+ .join(STRING_LITERALS.NEWLINE);
1363
+ die(`Pre-existing inconsistencies for ${id}:\n${errors}\n\n` +
1364
+ `Fix with: pnpm wu:repair --id ${id}`);
1365
+ }
1366
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU state consistency check passed`);
1367
+ // Branch-Only mode validation
1368
+ if (isBranchOnly) {
1369
+ const laneBranch = await defaultBranchFrom(docMain);
1370
+ if (!laneBranch)
1371
+ die('Cannot determine lane branch from WU YAML');
1372
+ const validation = await validateBranchOnlyMode(laneBranch);
1373
+ if (!validation.valid) {
1374
+ die(validation.error);
1375
+ }
1376
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Branch-Only mode validation passed`);
1377
+ console.log(`${LOG_PREFIX.DONE} Working on branch: ${laneBranch}`);
1378
+ }
1379
+ else {
1380
+ // Worktree mode: must be on main
1381
+ await ensureOnMain(getGitForCwd());
1382
+ // Prevent multi-agent data loss by ensuring clean working tree
1383
+ await ensureCleanWorkingTree();
1384
+ // Prevent coordination failures by ensuring main is up-to-date
1385
+ await ensureMainUpToDate();
1386
+ // P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
1387
+ // Previous wu:done runs or memory layer writes may have left this file dirty,
1388
+ // which causes the auto-rebase to fail with "You have unstaged changes"
1389
+ if (derivedWorktree) {
1390
+ try {
1391
+ execSync(`git -C "${derivedWorktree}" restore "${WU_EVENTS_PATH}"`);
1392
+ }
1393
+ catch {
1394
+ // Non-fatal: file might not exist or already clean
1395
+ }
1396
+ }
1397
+ // WU-1382: Detect parallel completions and warn
1398
+ // WU-1584 Fix #3: Trigger auto-rebase instead of just warning
1399
+ console.log(`${LOG_PREFIX.DONE} Checking for parallel WU completions...`);
1400
+ const parallelResult = await detectParallelCompletions(id, docForValidation);
1401
+ if (parallelResult.hasParallelCompletions) {
1402
+ console.warn(parallelResult.warning);
1403
+ // Emit telemetry for parallel detection
1404
+ emitTelemetry({
1405
+ script: 'wu-done',
1406
+ wu_id: id,
1407
+ step: 'parallel_detection',
1408
+ parallel_wus: parallelResult.completedWUs,
1409
+ count: parallelResult.completedWUs.length,
1410
+ });
1411
+ // WU-1588: Check inbox for recent signals from parallel agents
1412
+ // Non-blocking: failures handled internally by checkInboxForRecentSignals
1413
+ await checkInboxForRecentSignals(id);
1414
+ // WU-1584: Instead of proceeding with warning, trigger auto-rebase
1415
+ // This prevents merge conflicts that would fail downstream
1416
+ if (derivedWorktree && !args.noAutoRebase) {
1417
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1584: Triggering auto-rebase to incorporate parallel completions...`);
1418
+ const laneBranch = await defaultBranchFrom(docForValidation);
1419
+ if (laneBranch) {
1420
+ const rebaseResult = await autoRebaseBranch(laneBranch, derivedWorktree, id);
1421
+ if (rebaseResult.success) {
1422
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Auto-rebase complete - parallel completions incorporated`);
1423
+ emitTelemetry({
1424
+ script: MICRO_WORKTREE_OPERATIONS.WU_DONE,
1425
+ wu_id: id,
1426
+ step: TELEMETRY_STEPS.PARALLEL_AUTO_REBASE,
1427
+ parallel_wus: parallelResult.completedWUs,
1428
+ count: parallelResult.completedWUs.length,
1429
+ });
1430
+ }
1431
+ else {
1432
+ // Rebase failed - provide detailed instructions
1433
+ console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Auto-rebase failed`);
1434
+ console.error(rebaseResult.error);
1435
+ die(`WU-1584: Auto-rebase failed after detecting parallel completions.\n` +
1436
+ `Manual resolution required - see instructions above.`);
1437
+ }
1438
+ }
1439
+ }
1440
+ else if (!args.noAutoRebase) {
1441
+ // No worktree path available - warn and proceed (legacy behavior)
1442
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cannot auto-rebase (no worktree path) - proceeding with caution`);
1443
+ }
1444
+ else {
1445
+ // Auto-rebase disabled - warn and proceed
1446
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Auto-rebase disabled (--no-auto-rebase) - proceeding with caution`);
1447
+ }
1448
+ }
1449
+ // WU-1381: Detect background processes that might interfere with gates
1450
+ // Non-blocking warning - helps agents understand mixed stdout/stderr output
1451
+ if (derivedWorktree) {
1452
+ await runBackgroundProcessCheck(derivedWorktree);
1453
+ }
1454
+ // WU-1804: Fail fast before gates with comprehensive claim metadata check.
1455
+ // Validates both YAML status AND state store BEFORE gates, not just one of them.
1456
+ // Provides actionable guidance to run wu:repair-claim if validation fails.
1457
+ if (derivedWorktree) {
1458
+ await validateClaimMetadataBeforeGates(id, derivedWorktree, docForValidation.status);
1459
+ }
1460
+ }
1461
+ // Use worktree title for commit message (not stale main title)
1462
+ const title = docForValidation.title || docMain.title || '';
1463
+ if (isDocsOnly) {
1464
+ console.log('\n📝 Docs-only WU detected');
1465
+ console.log(' - Gates will skip lint/typecheck/tests');
1466
+ console.log(' - Only docs/markdown paths allowed\n');
1467
+ }
1468
+ if (isBranchOnly) {
1469
+ console.log('\n🌿 Branch-Only mode detected');
1470
+ console.log(' - Gates run in main checkout on lane branch');
1471
+ console.log(' - No worktree to remove\n');
1472
+ }
1473
+ // Ownership check (skip in branch-only mode)
1474
+ if (!isBranchOnly) {
1475
+ const ownershipCheck = await checkOwnership(id, docForValidation, derivedWorktree, args.overrideOwner, args.reason);
1476
+ if (!ownershipCheck.valid) {
1477
+ die(ownershipCheck.error);
1478
+ }
1479
+ // If override was used, log to audit trail and add to WU notes
1480
+ if (ownershipCheck.auditEntry) {
1481
+ auditOwnershipOverride(ownershipCheck.auditEntry);
1482
+ // Add override reason to WU notes (schema requires string, not array)
1483
+ const overrideNote = `Ownership override: Completed by ${ownershipCheck.auditEntry.completed_by} (assigned to ${ownershipCheck.auditEntry.assigned_to}). Reason: ${args.reason}`;
1484
+ appendNote(docForValidation, overrideNote);
1485
+ // Write updated WU YAML back to worktree
1486
+ if (derivedWorktree) {
1487
+ const wtWUPath = path.join(derivedWorktree, 'docs', '04-operations', 'tasks', 'wu', `${id}.yaml`);
1488
+ if (existsSync(wtWUPath)) {
1489
+ writeWU(wtWUPath, docForValidation);
1490
+ }
1491
+ }
1492
+ }
1493
+ }
1494
+ // WU-1280: Early spec completeness validation (before gates)
1495
+ // Catches missing tests.manual, empty code_paths, etc. BEFORE 2min gate run
1496
+ console.log(`\n${LOG_PREFIX.DONE} Validating spec completeness for ${id}...`);
1497
+ const specResult = validateSpecCompleteness(docForValidation, id);
1498
+ if (!specResult.valid) {
1499
+ console.error(`\n❌ Spec completeness validation failed for ${id}:\n`);
1500
+ specResult.errors.forEach((err) => console.error(` - ${err}`));
1501
+ console.error(`\nFix these issues before running wu:done:\n` +
1502
+ ` 1. Update docs/04-operations/tasks/wu/${id}.yaml\n` +
1503
+ ` 2. Fill description with Context/Problem/Solution\n` +
1504
+ ` 3. Replace ${PLACEHOLDER_SENTINEL} text with specific criteria\n` +
1505
+ ` 4. List all modified files in code_paths\n` +
1506
+ ` 5. Add at least one test path (unit, e2e, integration, or manual)\n` +
1507
+ ` 6. Re-run: pnpm wu:done --id ${id}\n\n` +
1508
+ `See: CLAUDE.md §2.7 "WUs are specs, not code"\n`);
1509
+ die(`Cannot mark ${id} as done - spec incomplete`);
1510
+ }
1511
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spec completeness check passed`);
1512
+ // WU-1351: Validate code_paths files exist (prevents false completions)
1513
+ // In worktree mode: validate files exist in worktree (will be merged)
1514
+ // In branch-only mode: validate files exist on current branch
1515
+ console.log(`\n${LOG_PREFIX.DONE} Validating code_paths existence for ${id}...`);
1516
+ const codePathsResult = await validateCodePathsExist(docForValidation, id, {
1517
+ worktreePath: derivedWorktree,
1518
+ targetBranch: isBranchOnly ? 'HEAD' : BRANCHES.MAIN,
1519
+ });
1520
+ if ('valid' in codePathsResult && !codePathsResult.valid) {
1521
+ console.error(`\n❌ code_paths validation failed for ${id}:\n`);
1522
+ if ('errors' in codePathsResult) {
1523
+ codePathsResult.errors.forEach((err) => console.error(err));
1524
+ }
1525
+ die(`Cannot mark ${id} as done - code_paths missing from target branch`);
1526
+ }
1527
+ // WU-1324 + WU-1542: Check mandatory agent compliance
1528
+ // WU-1542: --require-agents makes this a BLOCKING check
1529
+ const codePaths = docForValidation.code_paths || [];
1530
+ const compliance = checkMandatoryAgentsComplianceBlocking(codePaths, id, {
1531
+ blocking: Boolean(args.requireAgents),
1532
+ });
1533
+ if (compliance.blocking && compliance.errorMessage) {
1534
+ // WU-1542: Blocking mode - fail wu:done with detailed error
1535
+ die(compliance.errorMessage);
1536
+ }
1537
+ else if (!compliance.compliant) {
1538
+ // Non-blocking mode - show warning (original WU-1324 behavior)
1539
+ console.warn(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} MANDATORY AGENT WARNING`);
1540
+ console.warn(`The following mandatory agents were not confirmed as invoked:`);
1541
+ for (const agent of compliance.missing) {
1542
+ console.warn(` • ${agent}`);
1543
+ }
1544
+ console.warn(`\nThis is a NON-BLOCKING warning.`);
1545
+ console.warn(`Use --require-agents to make this a blocking error.\n`);
1546
+ }
1547
+ // WU-1999: Exposure validation (NON-BLOCKING warning)
1548
+ printExposureWarnings(docForValidation, { skipExposureCheck: args.skipExposureCheck });
1549
+ // WU-2022: Feature accessibility validation (BLOCKING)
1550
+ validateAccessibilityOrDie(docForValidation, {
1551
+ skipAccessibilityCheck: args.skipAccessibilityCheck,
1552
+ });
1553
+ // Run WU validator
1554
+ runWUValidator(docForValidation, id, args.allowTodo, derivedWorktree);
1555
+ // Validate skip-gates requirements
1556
+ if (args.skipGates) {
1557
+ if (!args.reason) {
1558
+ die('--skip-gates requires --reason "<explanation of why gates are being skipped>"');
1559
+ }
1560
+ if (!args.fixWu) {
1561
+ die('--skip-gates requires --fix-wu WU-{id} (the WU that will fix the failing tests)');
1562
+ }
1563
+ if (!PATTERNS.WU_ID.test(args.fixWu.toUpperCase())) {
1564
+ die(`Invalid --fix-wu value '${args.fixWu}'. Expected format: WU-123`);
1565
+ }
1566
+ }
1567
+ return { title, docForValidation };
1568
+ }
1569
+ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath, branchName, }) {
1570
+ // WU-1747: Check if gates can be skipped based on valid checkpoint
1571
+ // This allows resuming wu:done without re-running gates if nothing changed
1572
+ const skipResult = canSkipGates(id, { currentHeadSha: undefined });
1573
+ if (skipResult.canSkip) {
1574
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${CHECKPOINT_MESSAGES.SKIPPING_GATES_VALID}`);
1575
+ console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.CHECKPOINT_LABEL}: ${skipResult.checkpoint.checkpointId}`);
1576
+ console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.GATES_PASSED_AT}: ${skipResult.checkpoint.gatesPassedAt}`);
1577
+ emitTelemetry({
1578
+ script: TELEMETRY_STEPS.GATES,
1579
+ wu_id: id,
1580
+ step: TELEMETRY_STEPS.GATES,
1581
+ skipped: true,
1582
+ reason: SKIP_GATES_REASONS.CHECKPOINT_VALID,
1583
+ checkpoint_id: skipResult.checkpoint.checkpointId,
1584
+ });
1585
+ return; // Skip gates entirely
1586
+ }
1587
+ // WU-1747: Create checkpoint before gates for resumption on failure
1588
+ if (worktreePath && branchName) {
1589
+ try {
1590
+ await createWU1747Checkpoint({ wuId: id, worktreePath, branchName }, { gatesPassed: false });
1591
+ }
1592
+ catch (err) {
1593
+ // Non-blocking: checkpoint failure should not block wu:done
1594
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} ${CHECKPOINT_MESSAGES.COULD_NOT_CREATE}: ${err.message}`);
1595
+ }
1596
+ }
1597
+ // WU-1588: Create pre-gates checkpoint for recovery if gates fail
1598
+ // Non-blocking: failures handled internally by createPreGatesCheckpoint
1599
+ // WU-1749 Bug 5: Pass worktreePath as baseDir to write to worktree's wu-events.jsonl, not main's
1600
+ await createPreGatesCheckpoint(id, worktreePath, worktreePath);
1601
+ // P0 EMERGENCY FIX: Restore wu-events.jsonl after checkpoint creation
1602
+ // WU-1748 added checkpoint persistence to wu-events.jsonl but doesn't commit it,
1603
+ // leaving unstaged changes that cause "git rebase" to fail with "You have unstaged changes"
1604
+ // This restores the file to HEAD state - checkpoint data is preserved in memory store
1605
+ if (worktreePath) {
1606
+ try {
1607
+ execSync(`git -C "${worktreePath}" restore "${WU_EVENTS_PATH}"`);
1608
+ }
1609
+ catch {
1610
+ // Non-fatal: file might not exist or already clean
1611
+ }
1612
+ }
1613
+ // Step 0a: Run invariants check (WU-2252: NON-BYPASSABLE, runs even with --skip-gates)
1614
+ // This ensures repo invariants are never violated, regardless of skip-gates flag
1615
+ // WU-2253: Run against worktreePath (when present) to catch violations that only exist in the worktree
1616
+ // WU-2425: Pass wuId to scope WU-specific invariants to just the completing WU
1617
+ const invariantsBaseDir = worktreePath || process.cwd();
1618
+ console.log(`\n${LOG_PREFIX.DONE} Running invariants check (non-bypassable)...`);
1619
+ console.log(`${LOG_PREFIX.DONE} Checking invariants in: ${invariantsBaseDir}`);
1620
+ const { runInvariants } = await import('@lumenflow/core/dist/invariants-runner.js');
1621
+ const invariantsResult = runInvariants({ baseDir: invariantsBaseDir, silent: false, wuId: id });
1622
+ if (!invariantsResult.success) {
1623
+ emitTelemetry({
1624
+ script: 'wu-done',
1625
+ wu_id: id,
1626
+ step: 'invariants',
1627
+ ok: false,
1628
+ });
1629
+ die(`Invariants check failed. Fix violations before completing WU.\n\n${invariantsResult.formatted}`);
1630
+ }
1631
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Invariants check passed`);
1632
+ emitTelemetry({
1633
+ script: 'wu-done',
1634
+ wu_id: id,
1635
+ step: 'invariants',
1636
+ ok: true,
1637
+ });
1638
+ // Step 0b: Run gates BEFORE merge (or skip with audit trail)
1639
+ if (args.skipGates) {
1640
+ console.log(`\n${EMOJI.WARNING} ${EMOJI.WARNING} ${EMOJI.WARNING} SKIP-GATES MODE ACTIVE ${EMOJI.WARNING} ${EMOJI.WARNING} ${EMOJI.WARNING}\n`);
1641
+ console.log(`${LOG_PREFIX.DONE} Skipping gates check as requested`);
1642
+ console.log(`${LOG_PREFIX.DONE} Reason: ${args.reason}`);
1643
+ console.log(`${LOG_PREFIX.DONE} Fix WU: ${args.fixWu}`);
1644
+ console.log(`${LOG_PREFIX.DONE} Worktree: ${worktreePath || 'Branch-Only mode (no worktree)'}`);
1645
+ await auditSkipGates(id, args.reason, args.fixWu, worktreePath);
1646
+ console.log('\n⚠️ Ensure test failures are truly pre-existing!\n');
1647
+ emitTelemetry({
1648
+ script: 'wu-done',
1649
+ wu_id: id,
1650
+ step: 'gates',
1651
+ skipped: true,
1652
+ reason: args.reason,
1653
+ fix_wu: args.fixWu,
1654
+ });
1655
+ }
1656
+ else if (isBranchOnly) {
1657
+ // Branch-Only mode: run gates in-place (current directory on lane branch)
1658
+ console.log(`\n${LOG_PREFIX.DONE} Running gates in Branch-Only mode (in-place on lane branch)`);
1659
+ const gatesCmd = isDocsOnly
1660
+ ? `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`
1661
+ : `${PKG_MANAGER} ${SCRIPTS.GATES}`;
1662
+ if (isDocsOnly) {
1663
+ console.log(`${LOG_PREFIX.DONE} Using docs-only gates (skipping lint/typecheck/tests)`);
1664
+ }
1665
+ const startTime = Date.now();
1666
+ try {
1667
+ execSync(gatesCmd, { stdio: 'inherit' });
1668
+ const duration = Date.now() - startTime;
1669
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Gates passed in ${prettyMs(duration)}`);
1670
+ emitTelemetry({
1671
+ script: 'wu-done',
1672
+ wu_id: id,
1673
+ step: 'gates',
1674
+ ok: true,
1675
+ duration_ms: duration,
1676
+ });
1677
+ }
1678
+ catch {
1679
+ const duration = Date.now() - startTime;
1680
+ emitTelemetry({
1681
+ script: 'wu-done',
1682
+ wu_id: id,
1683
+ step: 'gates',
1684
+ ok: false,
1685
+ duration_ms: duration,
1686
+ });
1687
+ // WU-1280: Prominent error summary box (Branch-Only mode)
1688
+ // WU-1281: Extracted to helper using pretty-ms for duration formatting
1689
+ printGateFailureBox({
1690
+ id,
1691
+ location: 'Branch-Only',
1692
+ durationMs: duration,
1693
+ isWorktreeMode: false,
1694
+ });
1695
+ die(`Gates failed in Branch-Only mode. Fix issues and try again.`);
1696
+ }
1697
+ }
1698
+ else if (worktreePath && existsSync(worktreePath)) {
1699
+ // Worktree mode: run gates in the dedicated worktree
1700
+ runGatesInWorktree(worktreePath, id, isDocsOnly);
1701
+ }
1702
+ else {
1703
+ die(`Worktree not found (${worktreePath || 'unknown'}). Gates must run in the lane worktree.\n` +
1704
+ `If the worktree was removed, recreate it and retry, or use --skip-gates with justification.`);
1705
+ }
1706
+ // Step 0.75: Run COS governance gates (WU-614, COS v1.3 §7)
1707
+ if (!args.skipCosGates) {
1708
+ console.log(`\n${LOG_PREFIX.DONE} Running COS governance gates...`);
1709
+ const startTime = Date.now();
1710
+ try {
1711
+ execSync(`${PKG_MANAGER} ${SCRIPTS.COS_GATES} ${CLI_FLAGS.WU} ${id}`, {
1712
+ stdio: 'inherit',
1713
+ });
1714
+ const duration = Date.now() - startTime;
1715
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} COS gates passed in ${prettyMs(duration)}`);
1716
+ emitTelemetry({
1717
+ script: 'wu-done',
1718
+ wu_id: id,
1719
+ step: 'cos-gates',
1720
+ ok: true,
1721
+ duration_ms: duration,
1722
+ });
1723
+ }
1724
+ catch {
1725
+ const duration = Date.now() - startTime;
1726
+ emitTelemetry({
1727
+ script: 'wu-done',
1728
+ wu_id: id,
1729
+ step: 'cos-gates',
1730
+ ok: false,
1731
+ duration_ms: duration,
1732
+ });
1733
+ console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} COS governance gates failed`);
1734
+ console.error('\nTo fix:');
1735
+ console.error(' 1. Add required evidence to governance.evidence field in WU YAML');
1736
+ console.error(' 2. See: docs/04-operations/_frameworks/cos/evidence-format.md');
1737
+ console.error('\nEmergency bypass (creates audit trail):');
1738
+ console.error(` pnpm wu:done --id ${id} --skip-cos-gates --reason "explanation"`);
1739
+ die('Abort: WU not completed. Fix governance evidence and retry pnpm wu:done.');
1740
+ }
1741
+ }
1742
+ else {
1743
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Skipping COS governance gates as requested`);
1744
+ console.log(`${LOG_PREFIX.DONE} Reason: ${args.reason || DEFAULT_NO_REASON}`);
1745
+ await auditSkipCosGates(id, args.reason);
1746
+ emitTelemetry({
1747
+ script: 'wu-done',
1748
+ wu_id: id,
1749
+ step: 'cos-gates',
1750
+ skipped: true,
1751
+ reason: args.reason,
1752
+ });
1753
+ }
1754
+ // WU-1747: Mark checkpoint as gates passed for resumption on failure
1755
+ // This allows subsequent wu:done attempts to skip gates if nothing changed
1756
+ markGatesPassed(id);
1757
+ }
1758
+ /**
1759
+ * Print State HUD for visibility
1760
+ * Extracted from main() to reduce complexity (WU-1215 Phase 2 Extraction #4)
1761
+ * @param {object} params - Parameters
1762
+ * @param {string} params.id - WU ID
1763
+ * @param {object} params.docMain - Main WU YAML document
1764
+ * @param {boolean} params.isBranchOnly - Whether in branch-only mode
1765
+ * @param {boolean} params.isDocsOnly - Whether this is a docs-only WU
1766
+ * @param {string|null} params.derivedWorktree - Derived worktree path
1767
+ * @param {string} params.STAMPS_DIR - Stamps directory path
1768
+ */
1769
+ function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR }) {
1770
+ const stampExists = existsSync(path.join(STAMPS_DIR, `${id}.done`)) ? 'yes' : 'no';
1771
+ const yamlStatus = docMain.status || 'unknown';
1772
+ const yamlLocked = docMain.locked === true ? 'true' : 'false';
1773
+ const mode = isBranchOnly ? 'branch-only' : isDocsOnly ? 'docs-only' : 'worktree';
1774
+ const branch = defaultBranchFrom(docMain) || 'n/a';
1775
+ const worktreeDisplay = isBranchOnly ? 'none' : derivedWorktree || 'none';
1776
+ console.log(`\n${LOG_PREFIX.DONE} HUD: WU=${id} status=${yamlStatus} stamp=${stampExists} locked=${yamlLocked} mode=${mode} branch=${branch} worktree=${worktreeDisplay}`);
1777
+ }
1778
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1779
+ async function main() {
1780
+ // Validate CLI arguments and WU ID format (extracted to wu-done-validators.mjs)
1781
+ const { args, id } = validateInputs(process.argv);
1782
+ // Detect workspace mode and calculate paths (WU-1215: extracted to validators module)
1783
+ const pathInfo = await detectModeAndPaths(id, args);
1784
+ const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain, isBranchOnly, derivedWorktree, docForValidation: initialDocForValidation, isDocsOnly, } = pathInfo;
1785
+ // Capture main checkout path once. process.cwd() may drift later during recovery flows.
1786
+ const mainCheckoutPath = process.cwd();
1787
+ // Pre-flight checks (WU-1215: extracted to executePreFlightChecks function)
1788
+ const preFlightResult = await executePreFlightChecks({
1789
+ id,
1790
+ args,
1791
+ isBranchOnly,
1792
+ isDocsOnly,
1793
+ docMain,
1794
+ docForValidation: initialDocForValidation,
1795
+ derivedWorktree,
1796
+ });
1797
+ const title = preFlightResult.title;
1798
+ // Note: docForValidation is returned but not used after pre-flight checks
1799
+ // The metadata transaction uses docForUpdate instead
1800
+ // Step 0: Run gates (WU-1215: extracted to executeGates function)
1801
+ const worktreePath = derivedWorktree && !isBranchOnly
1802
+ ? path.isAbsolute(derivedWorktree)
1803
+ ? derivedWorktree
1804
+ : path.resolve(mainCheckoutPath, derivedWorktree)
1805
+ : null;
1806
+ // WU-1943: Check if any checkpoints exist for this WU session
1807
+ // Warn (don't block) if no checkpoints - agent should have been checkpointing periodically
1808
+ try {
1809
+ const wuNodes = await queryByWu(worktreePath || mainCheckoutPath, id);
1810
+ if (!hasSessionCheckpoints(id, wuNodes)) {
1811
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1943: No checkpoints found for ${id} session.`);
1812
+ console.log(`${LOG_PREFIX.DONE} Consider using 'pnpm mem:checkpoint --wu ${id}' periodically for crash recovery.`);
1813
+ console.log(`${LOG_PREFIX.DONE} Checkpoint triggers: after each acceptance criterion, before gates, every 30 tool calls.\n`);
1814
+ }
1815
+ }
1816
+ catch {
1817
+ // Non-blocking: checkpoint check failure should not block wu:done
1818
+ }
1819
+ await executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath });
1820
+ // Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
1821
+ printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree, STAMPS_DIR });
1822
+ // Step 0.5: Pre-flight validation - run ALL pre-commit hooks BEFORE merge
1823
+ // This prevents partial completion states where merge succeeds but commit fails
1824
+ // Validates all 8 gates: secrets, file size, ESLint, Prettier, TypeScript, audit, architecture, tasks
1825
+ // WU-2308: Pass worktreePath to run audit from worktree (checks fixed deps, not stale main deps)
1826
+ const hookResult = validateAllPreCommitHooks(id, worktreePath);
1827
+ if (!hookResult.valid) {
1828
+ die('Pre-flight validation failed. Fix hook issues and try again.');
1829
+ }
1830
+ // Step 0.6: WU-1781 - Run tasks:validate preflight BEFORE any merge/push operations
1831
+ // This prevents deadlocks where validation fails after merge, leaving local main ahead of origin
1832
+ // Specifically catches stamp-status mismatches from legacy WUs that would block pre-push hooks
1833
+ const tasksValidationResult = runPreflightTasksValidation(id);
1834
+ if (!tasksValidationResult.valid) {
1835
+ const errorMessage = buildPreflightErrorMessage(id, tasksValidationResult.errors);
1836
+ console.error(errorMessage);
1837
+ die('Preflight tasks:validate failed. See errors above for fix options.');
1838
+ }
1839
+ // Step 1: Execute mode-specific completion workflow (WU-1215: extracted to mode modules)
1840
+ // Worktree mode: Update metadata in worktree → commit → merge to main
1841
+ // Branch-Only mode: Merge to main → update metadata on main → commit
1842
+ // WU-1811: Track cleanupSafe flag to conditionally skip worktree removal on failure
1843
+ let completionResult = { cleanupSafe: true }; // Default to safe for no-auto mode
1844
+ if (!args.noAuto) {
1845
+ // Build context for mode-specific execution
1846
+ // WU-1369: Worktree mode uses atomic transaction pattern (no recordTransactionState/rollbackTransaction)
1847
+ // Branch-only mode still uses the old rollback mechanism
1848
+ const baseContext = {
1849
+ id,
1850
+ args,
1851
+ docMain,
1852
+ title,
1853
+ isDocsOnly,
1854
+ maxCommitLength: getCommitHeaderLimit(),
1855
+ validateStagedFiles,
1856
+ };
1857
+ try {
1858
+ if (isBranchOnly) {
1859
+ // Branch-Only mode: merge first, then update metadata on main
1860
+ // NOTE: Branch-only still uses old rollback mechanism
1861
+ const branchOnlyContext = {
1862
+ ...baseContext,
1863
+ recordTransactionState,
1864
+ rollbackTransaction,
1865
+ };
1866
+ completionResult = await executeBranchOnlyCompletion(branchOnlyContext);
1867
+ }
1868
+ else {
1869
+ // Worktree mode: update in worktree, commit, then merge or create PR
1870
+ // WU-1369: Uses atomic transaction pattern
1871
+ const worktreeContext = {
1872
+ ...baseContext,
1873
+ worktreePath,
1874
+ };
1875
+ completionResult = await executeWorktreeCompletion(worktreeContext);
1876
+ }
1877
+ // Handle recovery mode (zombie state cleanup completed)
1878
+ if ('recovered' in completionResult && completionResult.recovered) {
1879
+ // P0 FIX: Release lane lock before early exit
1880
+ try {
1881
+ const lane = docMain.lane;
1882
+ if (lane)
1883
+ releaseLaneLock(lane, { wuId: id });
1884
+ }
1885
+ catch { }
1886
+ process.exit(EXIT_CODES.SUCCESS);
1887
+ }
1888
+ }
1889
+ catch (err) {
1890
+ // P0 FIX: Release lane lock before error exit
1891
+ try {
1892
+ const lane = docMain.lane;
1893
+ if (lane)
1894
+ releaseLaneLock(lane, { wuId: id });
1895
+ }
1896
+ catch { }
1897
+ // WU-1811: Check if cleanup is safe before removing worktree
1898
+ // If cleanupSafe is false (or undefined), preserve worktree for recovery
1899
+ if (err.cleanupSafe === false) {
1900
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1811: Worktree preserved - rerun wu:done to recover`);
1901
+ }
1902
+ // Mode modules handle rollback internally, we just need to exit
1903
+ // Exit code 1 = recoverable (rebase/fix and retry)
1904
+ process.exit(EXIT_CODES.ERROR);
1905
+ }
1906
+ }
1907
+ else {
1908
+ await ensureNoAutoStagedOrNoop([WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR]);
1909
+ }
1910
+ // Step 6 & 7: Cleanup (remove worktree, delete branch) - WU-1215
1911
+ // WU-1811: Only run cleanup if all completion steps succeeded
1912
+ if (completionResult.cleanupSafe !== false) {
1913
+ await runCleanup(docMain, args);
1914
+ }
1915
+ else {
1916
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1811: Skipping worktree cleanup - metadata/push incomplete`);
1917
+ }
1918
+ // WU-1603: Release lane lock after successful completion
1919
+ // This allows the lane to be claimed by another WU
1920
+ try {
1921
+ const lane = docMain.lane;
1922
+ if (lane) {
1923
+ const releaseResult = releaseLaneLock(lane, { wuId: id });
1924
+ if (releaseResult.released && !releaseResult.notFound) {
1925
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
1926
+ }
1927
+ // Silent if notFound - lock may not exist (older WUs, manual cleanup)
1928
+ }
1929
+ }
1930
+ catch (err) {
1931
+ // Non-blocking: lock release failure should not block completion
1932
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${err.message}`);
1933
+ }
1934
+ // WU-1438: Auto-end agent session
1935
+ try {
1936
+ const sessionResult = endSessionForWU();
1937
+ if (sessionResult.ended) {
1938
+ // Emergency fix Session 2: Use SESSION.ID_DISPLAY_LENGTH constant
1939
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Agent session ended (${sessionResult.summary.session_id.slice(0, SESSION.ID_DISPLAY_LENGTH)}...)`);
1940
+ }
1941
+ // No warning if no active session - silent no-op is expected
1942
+ }
1943
+ catch (err) {
1944
+ // Non-blocking: session end failure should not block completion
1945
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not end agent session: ${err.message}`);
1946
+ }
1947
+ // WU-1588: Broadcast completion signal after session end
1948
+ // Non-blocking: failures handled internally by broadcastCompletionSignal
1949
+ await broadcastCompletionSignal(id, title);
1950
+ // WU-1946: Update spawn registry to mark WU as completed
1951
+ // Non-blocking: failures handled internally by updateSpawnRegistryOnCompletion
1952
+ // Works in both worktree and branch-only modes (called after completionResult)
1953
+ await updateSpawnRegistryOnCompletion(id, mainCheckoutPath);
1954
+ // WU-1747: Clear checkpoint on successful completion
1955
+ // Checkpoint is no longer needed once WU is fully complete
1956
+ clearCheckpoint(id);
1957
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction COMMIT - all steps succeeded (WU-755)`);
1958
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked done, pushed, and cleaned up.`);
1959
+ console.log(`- WU: ${id} — ${title}`);
1960
+ // WU-1763: Print lifecycle nudges (conditional, non-blocking)
1961
+ // Discovery summary nudge - only if discoveries exist
1962
+ const discoveries = await loadDiscoveriesForWU(mainCheckoutPath, id);
1963
+ printDiscoveryNudge(id, discoveries.count, discoveries.ids);
1964
+ // Documentation validation nudge - only if docs changed
1965
+ // Use worktreePath if available, otherwise skip (branch-only mode has no worktree)
1966
+ if (worktreePath) {
1967
+ const changedDocs = await detectChangedDocPaths(worktreePath, BRANCHES.MAIN);
1968
+ printDocValidationNudge(id, changedDocs);
1969
+ }
1970
+ // WU-1983: Migration deployment nudge - only if supabase paths in code_paths
1971
+ const codePaths = docMain.code_paths || [];
1972
+ await printMigrationDeploymentNudge(codePaths, mainCheckoutPath);
1973
+ }
1974
+ /**
1975
+ * WU-1983: Print migration deployment nudge when WU includes supabase changes.
1976
+ * Notifies agent to deploy new migrations to production via MCP tools.
1977
+ * Conditional output - only prints when migrations are in scope and new migrations exist.
1978
+ *
1979
+ * @param {string[]} codePaths - WU code_paths array
1980
+ * @param {string} baseDir - Base directory for migration discovery
1981
+ * @returns {Promise<void>}
1982
+ */
1983
+ export async function printMigrationDeploymentNudge(codePaths, baseDir) {
1984
+ // Only check if WU includes supabase paths
1985
+ if (!hasMigrationChanges(codePaths)) {
1986
+ return;
1987
+ }
1988
+ try {
1989
+ const { files: migrations, errors } = discoverLocalMigrations(baseDir);
1990
+ if (errors.length > 0) {
1991
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Migration discovery errors: ${errors.join(', ')}`);
1992
+ }
1993
+ if (migrations.length > 0) {
1994
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.INFO} WU includes supabase migrations.`);
1995
+ console.log(`${LOG_PREFIX.DONE} ${migrations.length} local migration(s) detected.`);
1996
+ console.log(`${LOG_PREFIX.DONE} To sync with production:`);
1997
+ console.log(` 1. mcp__supabase__list_migrations (check production state)`);
1998
+ console.log(` 2. pnpm db:sync --production-file <output.json> (detect drift)`);
1999
+ console.log(` 3. mcp__supabase__apply_migration (deploy new migrations)`);
2000
+ console.log(` See: docs/02-technical/database/migration-workflow.md\n`);
2001
+ }
2002
+ }
2003
+ catch (err) {
2004
+ // Non-blocking: migration check failure should not block wu:done
2005
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not check migrations: ${err.message}`);
2006
+ }
2007
+ }
2008
+ /**
2009
+ * WU-1763: Print discovery summary nudge when discoveries exist for this WU.
2010
+ * Conditional output - only prints when discoveryCount > 0.
2011
+ * Non-blocking, single-line output to avoid flooding the console.
2012
+ *
2013
+ * @param {string} id - WU ID being completed
2014
+ * @param {number} discoveryCount - Number of open discoveries for this WU
2015
+ * @param {string[]} discoveryIds - List of discovery IDs (limited to 5 in output)
2016
+ */
2017
+ export function printDiscoveryNudge(id, discoveryCount, discoveryIds) {
2018
+ if (discoveryCount > 0) {
2019
+ const displayIds = discoveryIds.slice(0, 5).join(', ');
2020
+ const moreText = discoveryCount > 5 ? ` (+${discoveryCount - 5} more)` : '';
2021
+ console.log(`\n${LOG_PREFIX.DONE} 💡 ${discoveryCount} open discoveries: ${displayIds}${moreText}`);
2022
+ console.log(` Triage with: pnpm mem:triage --wu ${id}`);
2023
+ }
2024
+ }
2025
+ /**
2026
+ * WU-1763: Print documentation validation nudge when docs changed.
2027
+ * Conditional output - only prints when changedDocPaths.length > 0.
2028
+ * Non-blocking, single-line output to avoid flooding the console.
2029
+ *
2030
+ * @param {string} id - WU ID being completed
2031
+ * @param {string[]} changedDocPaths - List of documentation paths that changed
2032
+ */
2033
+ export function printDocValidationNudge(id, changedDocPaths) {
2034
+ if (changedDocPaths.length > 0) {
2035
+ console.log(`\n${LOG_PREFIX.DONE} 💡 Documentation changed (${changedDocPaths.length} files).`);
2036
+ console.log(` Consider: pnpm validate:context && pnpm docs:linkcheck`);
2037
+ }
2038
+ }
2039
+ /**
2040
+ * WU-1763: Load discoveries for a WU from memory store.
2041
+ * Non-blocking - returns empty array on errors.
2042
+ *
2043
+ * @param {string} baseDir - Base directory containing .beacon/memory/
2044
+ * @param {string} wuId - WU ID to load discoveries for
2045
+ * @returns {Promise<{count: number, ids: string[]}>} Discovery count and IDs
2046
+ */
2047
+ async function loadDiscoveriesForWU(baseDir, wuId) {
2048
+ try {
2049
+ const memory = await loadMemory(path.join(baseDir, '.beacon/memory'));
2050
+ const wuNodes = memory.byWu.get(wuId) || [];
2051
+ const discoveries = wuNodes.filter((node) => node.type === 'discovery');
2052
+ return {
2053
+ count: discoveries.length,
2054
+ ids: discoveries.map((d) => d.id),
2055
+ };
2056
+ }
2057
+ catch {
2058
+ // Non-blocking: return empty on errors
2059
+ return { count: 0, ids: [] };
2060
+ }
2061
+ }
2062
+ /**
2063
+ * WU-1763: Detect documentation paths from changed files.
2064
+ * Non-blocking - returns empty array on errors.
2065
+ *
2066
+ * @param {string} worktreePath - Path to worktree
2067
+ * @param {string} baseBranch - Base branch to compare against
2068
+ * @returns {Promise<string[]>} List of changed documentation paths
2069
+ */
2070
+ async function detectChangedDocPaths(worktreePath, baseBranch) {
2071
+ try {
2072
+ const git = getGitForCwd();
2073
+ // Get files changed in this branch vs base
2074
+ const diff = await git.raw(['diff', '--name-only', baseBranch]);
2075
+ const changedFiles = diff.split('\n').filter(Boolean);
2076
+ // Filter to docs: ai/onboarding/, docs/, CLAUDE.md, README.md, *.md in root
2077
+ const docPatterns = [
2078
+ /^ai\/onboarding\//,
2079
+ /^docs\//,
2080
+ /^\.claude\//,
2081
+ /^CLAUDE\.md$/,
2082
+ /^README\.md$/,
2083
+ ];
2084
+ return changedFiles.filter((f) => docPatterns.some((p) => p.test(f)));
2085
+ }
2086
+ catch {
2087
+ // Non-blocking: return empty on errors
2088
+ return [];
2089
+ }
2090
+ }
2091
+ // Guard main() execution for testability (WU-1366)
2092
+ // When imported as a module for testing, main() should not auto-run
2093
+ import { fileURLToPath } from 'node:url';
2094
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
2095
+ main();
2096
+ }