@lumenflow/core 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 (263) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +119 -0
  3. package/dist/active-wu-detector.d.ts +33 -0
  4. package/dist/active-wu-detector.js +106 -0
  5. package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
  6. package/dist/adapters/filesystem-metrics.adapter.js +519 -0
  7. package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
  8. package/dist/adapters/terminal-renderer.adapter.js +337 -0
  9. package/dist/arg-parser.d.ts +63 -0
  10. package/dist/arg-parser.js +560 -0
  11. package/dist/backlog-editor.d.ts +98 -0
  12. package/dist/backlog-editor.js +179 -0
  13. package/dist/backlog-generator.d.ts +111 -0
  14. package/dist/backlog-generator.js +381 -0
  15. package/dist/backlog-parser.d.ts +45 -0
  16. package/dist/backlog-parser.js +102 -0
  17. package/dist/backlog-sync-validator.d.ts +78 -0
  18. package/dist/backlog-sync-validator.js +294 -0
  19. package/dist/branch-drift.d.ts +34 -0
  20. package/dist/branch-drift.js +51 -0
  21. package/dist/cleanup-install-config.d.ts +33 -0
  22. package/dist/cleanup-install-config.js +37 -0
  23. package/dist/cleanup-lock.d.ts +139 -0
  24. package/dist/cleanup-lock.js +313 -0
  25. package/dist/code-path-validator.d.ts +146 -0
  26. package/dist/code-path-validator.js +537 -0
  27. package/dist/code-paths-overlap.d.ts +55 -0
  28. package/dist/code-paths-overlap.js +245 -0
  29. package/dist/commands-logger.d.ts +77 -0
  30. package/dist/commands-logger.js +254 -0
  31. package/dist/commit-message-utils.d.ts +25 -0
  32. package/dist/commit-message-utils.js +41 -0
  33. package/dist/compliance-parser.d.ts +150 -0
  34. package/dist/compliance-parser.js +507 -0
  35. package/dist/constants/backlog-patterns.d.ts +20 -0
  36. package/dist/constants/backlog-patterns.js +23 -0
  37. package/dist/constants/dora-constants.d.ts +49 -0
  38. package/dist/constants/dora-constants.js +53 -0
  39. package/dist/constants/gate-constants.d.ts +15 -0
  40. package/dist/constants/gate-constants.js +15 -0
  41. package/dist/constants/linter-constants.d.ts +16 -0
  42. package/dist/constants/linter-constants.js +16 -0
  43. package/dist/constants/tokenizer-constants.d.ts +15 -0
  44. package/dist/constants/tokenizer-constants.js +15 -0
  45. package/dist/core/scope-checker.d.ts +97 -0
  46. package/dist/core/scope-checker.js +163 -0
  47. package/dist/core/tool-runner.d.ts +161 -0
  48. package/dist/core/tool-runner.js +393 -0
  49. package/dist/core/tool.constants.d.ts +105 -0
  50. package/dist/core/tool.constants.js +101 -0
  51. package/dist/core/tool.schemas.d.ts +226 -0
  52. package/dist/core/tool.schemas.js +226 -0
  53. package/dist/core/worktree-guard.d.ts +130 -0
  54. package/dist/core/worktree-guard.js +242 -0
  55. package/dist/coverage-gate.d.ts +108 -0
  56. package/dist/coverage-gate.js +196 -0
  57. package/dist/date-utils.d.ts +75 -0
  58. package/dist/date-utils.js +140 -0
  59. package/dist/dependency-graph.d.ts +142 -0
  60. package/dist/dependency-graph.js +550 -0
  61. package/dist/dependency-guard.d.ts +54 -0
  62. package/dist/dependency-guard.js +142 -0
  63. package/dist/dependency-validator.d.ts +105 -0
  64. package/dist/dependency-validator.js +154 -0
  65. package/dist/docs-path-validator.d.ts +36 -0
  66. package/dist/docs-path-validator.js +95 -0
  67. package/dist/domain/orchestration.constants.d.ts +99 -0
  68. package/dist/domain/orchestration.constants.js +97 -0
  69. package/dist/domain/orchestration.schemas.d.ts +280 -0
  70. package/dist/domain/orchestration.schemas.js +211 -0
  71. package/dist/domain/orchestration.types.d.ts +133 -0
  72. package/dist/domain/orchestration.types.js +12 -0
  73. package/dist/error-handler.d.ts +116 -0
  74. package/dist/error-handler.js +136 -0
  75. package/dist/file-classifiers.d.ts +62 -0
  76. package/dist/file-classifiers.js +108 -0
  77. package/dist/gates-agent-mode.d.ts +81 -0
  78. package/dist/gates-agent-mode.js +94 -0
  79. package/dist/generate-traceability.d.ts +107 -0
  80. package/dist/generate-traceability.js +411 -0
  81. package/dist/git-adapter.d.ts +395 -0
  82. package/dist/git-adapter.js +649 -0
  83. package/dist/git-staged-validator.d.ts +32 -0
  84. package/dist/git-staged-validator.js +48 -0
  85. package/dist/hardcoded-strings.d.ts +61 -0
  86. package/dist/hardcoded-strings.js +270 -0
  87. package/dist/incremental-lint.d.ts +78 -0
  88. package/dist/incremental-lint.js +129 -0
  89. package/dist/incremental-test.d.ts +39 -0
  90. package/dist/incremental-test.js +61 -0
  91. package/dist/index.d.ts +42 -0
  92. package/dist/index.js +61 -0
  93. package/dist/invariants/check-automated-tests.d.ts +50 -0
  94. package/dist/invariants/check-automated-tests.js +166 -0
  95. package/dist/invariants-runner.d.ts +103 -0
  96. package/dist/invariants-runner.js +527 -0
  97. package/dist/lane-checker.d.ts +50 -0
  98. package/dist/lane-checker.js +319 -0
  99. package/dist/lane-inference.d.ts +39 -0
  100. package/dist/lane-inference.js +195 -0
  101. package/dist/lane-lock.d.ts +211 -0
  102. package/dist/lane-lock.js +474 -0
  103. package/dist/lane-validator.d.ts +48 -0
  104. package/dist/lane-validator.js +114 -0
  105. package/dist/logs-lib.d.ts +104 -0
  106. package/dist/logs-lib.js +207 -0
  107. package/dist/lumenflow-config-schema.d.ts +272 -0
  108. package/dist/lumenflow-config-schema.js +207 -0
  109. package/dist/lumenflow-config.d.ts +95 -0
  110. package/dist/lumenflow-config.js +236 -0
  111. package/dist/manual-test-validator.d.ts +80 -0
  112. package/dist/manual-test-validator.js +200 -0
  113. package/dist/merge-lock.d.ts +115 -0
  114. package/dist/merge-lock.js +251 -0
  115. package/dist/micro-worktree.d.ts +159 -0
  116. package/dist/micro-worktree.js +427 -0
  117. package/dist/migration-deployer.d.ts +69 -0
  118. package/dist/migration-deployer.js +151 -0
  119. package/dist/orchestration-advisory-loader.d.ts +28 -0
  120. package/dist/orchestration-advisory-loader.js +87 -0
  121. package/dist/orchestration-advisory.d.ts +58 -0
  122. package/dist/orchestration-advisory.js +94 -0
  123. package/dist/orchestration-di.d.ts +48 -0
  124. package/dist/orchestration-di.js +57 -0
  125. package/dist/orchestration-rules.d.ts +57 -0
  126. package/dist/orchestration-rules.js +201 -0
  127. package/dist/orphan-detector.d.ts +131 -0
  128. package/dist/orphan-detector.js +226 -0
  129. package/dist/path-classifiers.d.ts +57 -0
  130. package/dist/path-classifiers.js +93 -0
  131. package/dist/piped-command-detector.d.ts +34 -0
  132. package/dist/piped-command-detector.js +64 -0
  133. package/dist/ports/dashboard-renderer.port.d.ts +112 -0
  134. package/dist/ports/dashboard-renderer.port.js +25 -0
  135. package/dist/ports/metrics-collector.port.d.ts +132 -0
  136. package/dist/ports/metrics-collector.port.js +26 -0
  137. package/dist/process-detector.d.ts +84 -0
  138. package/dist/process-detector.js +172 -0
  139. package/dist/prompt-linter.d.ts +72 -0
  140. package/dist/prompt-linter.js +312 -0
  141. package/dist/prompt-monitor.d.ts +15 -0
  142. package/dist/prompt-monitor.js +205 -0
  143. package/dist/rebase-artifact-cleanup.d.ts +145 -0
  144. package/dist/rebase-artifact-cleanup.js +433 -0
  145. package/dist/retry-strategy.d.ts +189 -0
  146. package/dist/retry-strategy.js +283 -0
  147. package/dist/risk-detector.d.ts +108 -0
  148. package/dist/risk-detector.js +252 -0
  149. package/dist/rollback-utils.d.ts +76 -0
  150. package/dist/rollback-utils.js +104 -0
  151. package/dist/section-headings.d.ts +43 -0
  152. package/dist/section-headings.js +49 -0
  153. package/dist/spawn-escalation.d.ts +90 -0
  154. package/dist/spawn-escalation.js +253 -0
  155. package/dist/spawn-monitor.d.ts +229 -0
  156. package/dist/spawn-monitor.js +672 -0
  157. package/dist/spawn-recovery.d.ts +82 -0
  158. package/dist/spawn-recovery.js +298 -0
  159. package/dist/spawn-registry-schema.d.ts +98 -0
  160. package/dist/spawn-registry-schema.js +108 -0
  161. package/dist/spawn-registry-store.d.ts +146 -0
  162. package/dist/spawn-registry-store.js +273 -0
  163. package/dist/spawn-tree.d.ts +121 -0
  164. package/dist/spawn-tree.js +285 -0
  165. package/dist/stamp-status-validator.d.ts +84 -0
  166. package/dist/stamp-status-validator.js +134 -0
  167. package/dist/stamp-utils.d.ts +100 -0
  168. package/dist/stamp-utils.js +229 -0
  169. package/dist/state-machine.d.ts +26 -0
  170. package/dist/state-machine.js +83 -0
  171. package/dist/system-map-validator.d.ts +80 -0
  172. package/dist/system-map-validator.js +272 -0
  173. package/dist/telemetry.d.ts +80 -0
  174. package/dist/telemetry.js +213 -0
  175. package/dist/token-counter.d.ts +51 -0
  176. package/dist/token-counter.js +145 -0
  177. package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
  178. package/dist/usecases/get-dashboard-data.usecase.js +61 -0
  179. package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
  180. package/dist/usecases/get-suggestions.usecase.js +153 -0
  181. package/dist/user-normalizer.d.ts +41 -0
  182. package/dist/user-normalizer.js +141 -0
  183. package/dist/validators/phi-constants.d.ts +97 -0
  184. package/dist/validators/phi-constants.js +152 -0
  185. package/dist/validators/phi-scanner.d.ts +58 -0
  186. package/dist/validators/phi-scanner.js +215 -0
  187. package/dist/worktree-ownership.d.ts +50 -0
  188. package/dist/worktree-ownership.js +74 -0
  189. package/dist/worktree-scanner.d.ts +103 -0
  190. package/dist/worktree-scanner.js +168 -0
  191. package/dist/worktree-symlink.d.ts +99 -0
  192. package/dist/worktree-symlink.js +359 -0
  193. package/dist/wu-backlog-updater.d.ts +17 -0
  194. package/dist/wu-backlog-updater.js +37 -0
  195. package/dist/wu-checkpoint.d.ts +124 -0
  196. package/dist/wu-checkpoint.js +233 -0
  197. package/dist/wu-claim-helpers.d.ts +26 -0
  198. package/dist/wu-claim-helpers.js +63 -0
  199. package/dist/wu-claim-resume.d.ts +106 -0
  200. package/dist/wu-claim-resume.js +276 -0
  201. package/dist/wu-consistency-checker.d.ts +95 -0
  202. package/dist/wu-consistency-checker.js +567 -0
  203. package/dist/wu-constants.d.ts +1275 -0
  204. package/dist/wu-constants.js +1382 -0
  205. package/dist/wu-create-validators.d.ts +42 -0
  206. package/dist/wu-create-validators.js +93 -0
  207. package/dist/wu-done-branch-only.d.ts +63 -0
  208. package/dist/wu-done-branch-only.js +191 -0
  209. package/dist/wu-done-messages.d.ts +119 -0
  210. package/dist/wu-done-messages.js +185 -0
  211. package/dist/wu-done-pr.d.ts +72 -0
  212. package/dist/wu-done-pr.js +174 -0
  213. package/dist/wu-done-retry-helpers.d.ts +85 -0
  214. package/dist/wu-done-retry-helpers.js +172 -0
  215. package/dist/wu-done-ui.d.ts +37 -0
  216. package/dist/wu-done-ui.js +69 -0
  217. package/dist/wu-done-validators.d.ts +411 -0
  218. package/dist/wu-done-validators.js +1229 -0
  219. package/dist/wu-done-worktree.d.ts +182 -0
  220. package/dist/wu-done-worktree.js +1097 -0
  221. package/dist/wu-helpers.d.ts +128 -0
  222. package/dist/wu-helpers.js +248 -0
  223. package/dist/wu-lint.d.ts +70 -0
  224. package/dist/wu-lint.js +234 -0
  225. package/dist/wu-paths.d.ts +171 -0
  226. package/dist/wu-paths.js +178 -0
  227. package/dist/wu-preflight-validators.d.ts +86 -0
  228. package/dist/wu-preflight-validators.js +251 -0
  229. package/dist/wu-recovery.d.ts +138 -0
  230. package/dist/wu-recovery.js +341 -0
  231. package/dist/wu-repair-core.d.ts +131 -0
  232. package/dist/wu-repair-core.js +669 -0
  233. package/dist/wu-schema-normalization.d.ts +17 -0
  234. package/dist/wu-schema-normalization.js +82 -0
  235. package/dist/wu-schema.d.ts +793 -0
  236. package/dist/wu-schema.js +881 -0
  237. package/dist/wu-spawn-helpers.d.ts +121 -0
  238. package/dist/wu-spawn-helpers.js +271 -0
  239. package/dist/wu-spawn.d.ts +158 -0
  240. package/dist/wu-spawn.js +1306 -0
  241. package/dist/wu-state-schema.d.ts +213 -0
  242. package/dist/wu-state-schema.js +156 -0
  243. package/dist/wu-state-store.d.ts +264 -0
  244. package/dist/wu-state-store.js +691 -0
  245. package/dist/wu-status-transition.d.ts +63 -0
  246. package/dist/wu-status-transition.js +382 -0
  247. package/dist/wu-status-updater.d.ts +25 -0
  248. package/dist/wu-status-updater.js +116 -0
  249. package/dist/wu-transaction-collectors.d.ts +116 -0
  250. package/dist/wu-transaction-collectors.js +272 -0
  251. package/dist/wu-transaction.d.ts +170 -0
  252. package/dist/wu-transaction.js +273 -0
  253. package/dist/wu-validation-constants.d.ts +60 -0
  254. package/dist/wu-validation-constants.js +66 -0
  255. package/dist/wu-validation.d.ts +118 -0
  256. package/dist/wu-validation.js +243 -0
  257. package/dist/wu-validator.d.ts +62 -0
  258. package/dist/wu-validator.js +325 -0
  259. package/dist/wu-yaml-fixer.d.ts +97 -0
  260. package/dist/wu-yaml-fixer.js +264 -0
  261. package/dist/wu-yaml.d.ts +86 -0
  262. package/dist/wu-yaml.js +222 -0
  263. package/package.json +114 -0
@@ -0,0 +1,1097 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Worktree mode completion workflow for wu:done
4
+ * Extracted from wu-done.mjs (WU-1215 refactoring)
5
+ * Updated in WU-1369 to use atomic transaction pattern.
6
+ *
7
+ * Flow (WU-1369 Atomic Pattern):
8
+ * 1. cd into worktree
9
+ * 2. Read and validate WU state
10
+ * 3. Run ALL validations FIRST (before any file writes)
11
+ * 4. Collect all metadata changes into transaction (in memory)
12
+ * 5. Commit transaction (atomic write)
13
+ * 6. Stage and format files
14
+ * 7. Git commit in worktree
15
+ * 8. Return to main checkout
16
+ * 9. Either merge (default) OR create PR (pr-mode)
17
+ * 10. Push to origin
18
+ *
19
+ * Key guarantee: If any validation fails, NO files are modified.
20
+ */
21
+ import path from 'node:path';
22
+ import { writeFile } from 'node:fs/promises';
23
+ import { generateCommitMessage, collectMetadataToTransaction, stageAndFormatMetadata, defaultBranchFrom, branchExists, validatePostMutation, } from './wu-done-validators.js';
24
+ import { getGitForCwd } from './git-adapter.js';
25
+ import { readWU, writeWU } from './wu-yaml.js';
26
+ import { WU_PATHS } from './wu-paths.js';
27
+ import { BRANCHES, REMOTES, THRESHOLDS, LOG_PREFIX, EMOJI, COMMIT_FORMATS, BOX, STRING_LITERALS, WU_STATUS, GIT_COMMANDS, GIT_FLAGS, } from './wu-constants.js';
28
+ import { RECOVERY, REBASE, PREFLIGHT, MERGE } from './wu-done-messages.js';
29
+ import { getDriftLevel, DRIFT_LEVELS } from './branch-drift.js';
30
+ import { createError, ErrorCodes } from './error-handler.js';
31
+ import { validateDoneWU, validateAndNormalizeWUYAML } from './wu-schema.js';
32
+ import { assertTransition } from './state-machine.js';
33
+ import { detectZombieState, resetWorktreeYAMLForRecovery, getRecoveryAttemptCount, incrementRecoveryAttempt, clearRecoveryAttempts, shouldEscalateToManualIntervention, MAX_RECOVERY_ATTEMPTS, } from './wu-recovery.js';
34
+ import { isPRModeEnabled, createPR, printPRCreatedMessage } from './wu-done-pr.js';
35
+ // WU-1371: Import rebase artifact cleanup functions
36
+ import { detectRebasedArtifacts, cleanupRebasedArtifacts } from './rebase-artifact-cleanup.js';
37
+ import { WUTransaction, createTransactionSnapshot, restoreFromSnapshot } from './wu-transaction.js';
38
+ // WU-1506: Import backlog invariant repair
39
+ // WU-1574: Removed repairBacklogInvariants - no longer needed with state store architecture
40
+ // Backlog.md is now always regenerated from wu-events.jsonl, so duplicates cannot occur
41
+ // WU-1584: Import retry helpers for squashing duplicate commits
42
+ // WU-1749: Added prepareRecoveryWithSquash for zombie recovery flow
43
+ import { countPreviousCompletionAttempts, squashPreviousCompletionAttempts, prepareRecoveryWithSquash, } from './wu-done-retry-helpers.js';
44
+ // WU-1747: Import retry, lock, and checkpoint modules for concurrent load resilience
45
+ import { withRetry, createRetryConfig } from './retry-strategy.js';
46
+ import { withMergeLock } from './merge-lock.js';
47
+ // WU-1749: Import state store constant for append-only file path
48
+ import { WU_EVENTS_FILE_NAME } from './wu-state-store.js';
49
+ import { validateWUEvent } from './wu-state-schema.js';
50
+ /**
51
+ * @typedef {Object} WorktreeContext
52
+ * @property {string} id - WU ID (e.g., "WU-1215")
53
+ * @property {Object} args - Parsed CLI arguments
54
+ * @property {Object} docMain - WU YAML document from main checkout
55
+ * @property {string} title - WU title for commit message
56
+ * @property {boolean} isDocsOnly - Whether this is a docs-only WU
57
+ * @property {string} worktreePath - Path to worktree
58
+ * @property {number} maxCommitLength - Max commit header length from commitlint
59
+ * @property {function} validateStagedFiles - Staged files validator
60
+ * NOTE: recordTransactionState/rollbackTransaction removed in WU-1369 (atomic pattern)
61
+ */
62
+ /**
63
+ * @typedef {Object} WorktreeResult
64
+ * @property {boolean} success - Whether completion succeeded
65
+ * @property {boolean} committed - Whether changes were committed
66
+ * @property {boolean} pushed - Whether changes were pushed
67
+ * @property {boolean} merged - Whether lane branch was merged (vs PR created)
68
+ * @property {string|null} prUrl - PR URL if PR mode was used
69
+ * @property {boolean} [recovered] - Whether zombie state was recovered
70
+ * @property {boolean} [cleanupSafe] - WU-1811: Whether worktree cleanup is safe (all steps succeeded)
71
+ */
72
+ /**
73
+ * Execute worktree mode completion
74
+ *
75
+ * @param {WorktreeContext} context - Worktree mode context
76
+ * @returns {Promise<WorktreeResult>} Completion result
77
+ * @throws {Error} On validation or git operation failure
78
+ */
79
+ export async function executeWorktreeCompletion(context) {
80
+ // Save original cwd for returning after any worktree operations.
81
+ // This must be captured BEFORE zombie recovery, which temporarily chdirs into the worktree.
82
+ const originalCwd = process.cwd();
83
+ const { id, args, docMain, title, isDocsOnly, worktreePath, maxCommitLength, validateStagedFiles,
84
+ // NOTE: recordTransactionState/rollbackTransaction removed in WU-1369 (atomic pattern)
85
+ } = context;
86
+ // Calculate WU path relative to repo root (before chdir)
87
+ // Other paths are recalculated inside the worktree using WU_PATHS
88
+ const metadataWUPath = path.join(worktreePath, 'docs', '04-operations', 'tasks', 'wu', `${id}.yaml`);
89
+ // Read WU YAML and validate current state
90
+ const docForUpdate = readWU(metadataWUPath, id);
91
+ // Check for zombie state (recovery mode)
92
+ // WU-1440: If zombie state detected, reset worktree YAML to in_progress
93
+ // and continue with normal flow (don't commit directly to main)
94
+ if (detectZombieState(docForUpdate, worktreePath)) {
95
+ console.log(`\n${RECOVERY.DETECTED}`);
96
+ // WU-1335: Check recovery attempt count to prevent infinite loops
97
+ const attemptCount = getRecoveryAttemptCount(id);
98
+ if (shouldEscalateToManualIntervention(attemptCount)) {
99
+ console.log(`\n${BOX.TOP}`);
100
+ console.log(`${BOX.SIDE} RECOVERY LOOP DETECTED - MANUAL INTERVENTION REQUIRED`);
101
+ console.log(BOX.MID);
102
+ console.log(`${BOX.SIDE} WU: ${id}`);
103
+ console.log(`${BOX.SIDE} Recovery attempts: ${attemptCount} (max: ${MAX_RECOVERY_ATTEMPTS})`);
104
+ console.log(BOX.SIDE);
105
+ console.log(`${BOX.SIDE} Automatic recovery has failed multiple times.`);
106
+ console.log(`${BOX.SIDE} Manual steps required:`);
107
+ console.log(BOX.SIDE);
108
+ console.log(`${BOX.SIDE} 1. cd ${worktreePath}`);
109
+ console.log(`${BOX.SIDE} 2. Reset WU YAML status to in_progress manually`);
110
+ console.log(`${BOX.SIDE} 3. git add && git commit`);
111
+ console.log(`${BOX.SIDE} 4. Return to main and retry wu:done`);
112
+ console.log(BOX.SIDE);
113
+ console.log(`${BOX.SIDE} Or reset the recovery counter:`);
114
+ console.log(`${BOX.SIDE} rm .beacon/recovery/${id}.recovery`);
115
+ console.log(BOX.BOT);
116
+ throw createError(ErrorCodes.RECOVERY_ERROR, `Recovery loop detected for ${id} after ${attemptCount} attempts. Manual intervention required.`, { wuId: id, attemptCount, maxAttempts: MAX_RECOVERY_ATTEMPTS });
117
+ }
118
+ // Increment attempt counter before trying recovery
119
+ const newAttemptCount = incrementRecoveryAttempt(id);
120
+ console.log(`${LOG_PREFIX.DONE} Recovery attempt ${newAttemptCount}/${MAX_RECOVERY_ATTEMPTS} (WU-1335)`);
121
+ console.log(RECOVERY.RESUMING);
122
+ // WU-1749: Squash previous completion attempts before recovery
123
+ // This prevents "rebase hell" where multiple completion commits accumulate
124
+ // during failed retry attempts
125
+ try {
126
+ const prevCwd = process.cwd();
127
+ try {
128
+ process.chdir(worktreePath);
129
+ const gitCwd = getGitForCwd();
130
+ const squashResult = await prepareRecoveryWithSquash(id, gitCwd);
131
+ if (squashResult.squashedCount > 0) {
132
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Squashed ${squashResult.squashedCount} previous completion attempt(s)`);
133
+ }
134
+ }
135
+ finally {
136
+ process.chdir(prevCwd);
137
+ }
138
+ }
139
+ catch (squashError) {
140
+ // Non-fatal: Log and continue with recovery
141
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not squash previous attempts: ${squashError.message}`);
142
+ }
143
+ console.log(`${LOG_PREFIX.DONE} WU-1440: Resetting worktree YAML to in_progress for recovery flow...`);
144
+ // Reset the worktree YAML to in_progress (mutates docForUpdate)
145
+ resetWorktreeYAMLForRecovery({ worktreePath, id, doc: docForUpdate });
146
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Recovery reset complete - continuing normal flow`);
147
+ // Continue with normal flow - don't return early
148
+ // docForUpdate is now status=in_progress, so normal flow will work
149
+ }
150
+ // Capture status AFTER potential zombie recovery reset
151
+ const currentStatus = docForUpdate.status || WU_STATUS.IN_PROGRESS;
152
+ // Validate state transition
153
+ assertTransition(currentStatus, WU_STATUS.DONE, id);
154
+ // WU-1369: Create atomic transaction for metadata updates
155
+ // This ensures NO files are modified if any validation fails
156
+ const transaction = new WUTransaction(id);
157
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction BEGIN - atomic pattern (WU-1369)`);
158
+ // Save original cwd for returning after worktree operations
159
+ let merged = false;
160
+ let prUrl = null;
161
+ // WU-1943: Track pre-commit SHA and git commit state for rollback on merge failure
162
+ let preCommitSha = null;
163
+ let gitCommitMade = false;
164
+ // WU-2310: Track snapshot for file rollback on git commit failure
165
+ /** @type {Map<string, string|null>|null} */
166
+ let transactionSnapshot = null;
167
+ try {
168
+ // cd into worktree for metadata updates
169
+ console.log(`\n${LOG_PREFIX.DONE} Updating metadata in worktree: ${worktreePath}`);
170
+ process.chdir(worktreePath);
171
+ // Recalculate paths relative to worktree (now current dir)
172
+ const workingWUPath = WU_PATHS.WU(id);
173
+ const workingStatusPath = WU_PATHS.STATUS();
174
+ const workingBacklogPath = WU_PATHS.BACKLOG();
175
+ const workingStampsDir = WU_PATHS.STAMPS_DIR();
176
+ const workingStampPath = path.join(workingStampsDir, `${id}.done`);
177
+ // ======================================================================
178
+ // PHASE 1: RUN ALL VALIDATIONS FIRST (before any file writes)
179
+ // WU-1369: This ensures no partial state on validation failure
180
+ // WU-1811: Validate and normalize YAML before gates/merge
181
+ // ======================================================================
182
+ console.log(`${LOG_PREFIX.DONE} Running validations (no writes until all pass)...`);
183
+ // WU-1811: Validate and normalize WU YAML schema with fixable corrections
184
+ // This catches schema issues early and auto-fixes normalizable problems
185
+ const normalizeResult = validateAndNormalizeWUYAML(docForUpdate);
186
+ if (!normalizeResult.valid) {
187
+ throw createError(ErrorCodes.VALIDATION_ERROR, `WU YAML validation failed:\n - ${normalizeResult.errors.join('\n - ')}\n\nNext step: Fix the validation errors in ${workingWUPath} and rerun wu:done`, { wuId: id });
188
+ }
189
+ // WU-1811: If normalizations were applied, write back to YAML file
190
+ if (normalizeResult.wasNormalized) {
191
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1811: Applying auto-normalisations to WU YAML...`);
192
+ writeWU(workingWUPath, normalizeResult.normalized);
193
+ // Update docForUpdate to use normalized data for subsequent processing
194
+ Object.assign(docForUpdate, normalizeResult.normalized);
195
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU YAML normalised and saved`);
196
+ }
197
+ // Validate done-specific completeness (uses normalized data)
198
+ const completenessResult = validateDoneWU(normalizeResult.normalized);
199
+ if (!completenessResult.valid) {
200
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Cannot mark WU as done - spec incomplete:\n ${completenessResult.errors.join('\n ')}\n\nNext step: Update ${workingWUPath} to meet completion requirements and rerun wu:done`, { wuId: id });
201
+ }
202
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All validations passed`);
203
+ // ======================================================================
204
+ // PHASE 2: COLLECT ALL CHANGES TO TRANSACTION (in memory, no writes)
205
+ // WU-1369: Atomic collection - all changes gathered before any writes
206
+ // ======================================================================
207
+ // WU-1574: Now async
208
+ await collectMetadataToTransaction({
209
+ id,
210
+ title,
211
+ doc: docForUpdate,
212
+ wuPath: workingWUPath,
213
+ statusPath: workingStatusPath,
214
+ backlogPath: workingBacklogPath,
215
+ stampPath: workingStampPath,
216
+ transaction,
217
+ });
218
+ // Validate the transaction itself
219
+ const txValidation = transaction.validate();
220
+ if (!txValidation.valid) {
221
+ throw createError(ErrorCodes.TRANSACTION_ERROR, `Transaction validation failed:\n ${txValidation.errors.join('\n ')}`, { wuId: id });
222
+ }
223
+ // ======================================================================
224
+ // PHASE 3: ATOMIC COMMIT (write all files at once)
225
+ // WU-1369: This is the only point where files are written
226
+ // WU-2310: Capture snapshot BEFORE commit for rollback on git commit failure
227
+ // ======================================================================
228
+ // WU-2310: Capture file state before transaction commit
229
+ // This allows rollback if git commit fails AFTER files are written
230
+ // Note: We use the relative paths since we're already chdir'd into the worktree
231
+ const workingEventsPath = path.join('.beacon', 'state', WU_EVENTS_FILE_NAME);
232
+ const pathsToSnapshot = [
233
+ workingWUPath,
234
+ workingStatusPath,
235
+ workingBacklogPath,
236
+ workingStampPath,
237
+ workingEventsPath,
238
+ ];
239
+ transactionSnapshot = createTransactionSnapshot(pathsToSnapshot);
240
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-2310: Snapshot captured for rollback`);
241
+ const commitResult = transaction.commit();
242
+ if (!commitResult.success) {
243
+ throw createError(ErrorCodes.TRANSACTION_ERROR, `Transaction commit failed - some files not written:\n ${commitResult.failed.map((f) => f.path).join('\n ')}`, { wuId: id, written: commitResult.written, failed: commitResult.failed });
244
+ }
245
+ // ======================================================================
246
+ // WU-1617: POST-MUTATION VALIDATION
247
+ // Verify files written by tx.commit() are valid (completed_at, locked, stamp)
248
+ // ======================================================================
249
+ const postMutationResult = validatePostMutation({
250
+ id,
251
+ wuPath: workingWUPath,
252
+ stampPath: workingStampPath,
253
+ });
254
+ if (!postMutationResult.valid) {
255
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Post-mutation validation failed:\n ${postMutationResult.errors.join('\n ')}`, { wuId: id, errors: postMutationResult.errors });
256
+ }
257
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Post-mutation validation passed (WU-1617)`);
258
+ // ======================================================================
259
+ // PHASE 4: GIT OPERATIONS (stage, format, commit)
260
+ // Files are now written - proceed with git operations
261
+ // ======================================================================
262
+ // Stage and format files
263
+ await stageAndFormatMetadata({
264
+ id,
265
+ wuPath: workingWUPath,
266
+ statusPath: workingStatusPath,
267
+ backlogPath: workingBacklogPath,
268
+ stampsDir: workingStampsDir,
269
+ });
270
+ // Validate staged files
271
+ await validateStagedFiles(id, isDocsOnly);
272
+ // ======================================================================
273
+ // WU-1584 Fix #1: Squash previous completion attempts before new commit
274
+ // This prevents N duplicate commits when wu:done is retried N times
275
+ // ======================================================================
276
+ const gitCwd = getGitForCwd();
277
+ const previousAttempts = await countPreviousCompletionAttempts(id, gitCwd);
278
+ if (previousAttempts > 0) {
279
+ const squashResult = await squashPreviousCompletionAttempts(id, previousAttempts, gitCwd);
280
+ if (squashResult.squashed) {
281
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Squashed ${squashResult.count} previous attempt(s) - single commit will be created`);
282
+ }
283
+ }
284
+ // Generate commit message and commit
285
+ const msg = generateCommitMessage(id, title, maxCommitLength);
286
+ // WU-1943: Capture pre-commit SHA for rollback on merge failure
287
+ preCommitSha = await gitCwd.getCommitHash('HEAD');
288
+ await gitCwd.commit(msg);
289
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Metadata committed in worktree`);
290
+ // WU-1943: Track that git commit was made (needed for rollback decision)
291
+ gitCommitMade = true;
292
+ // Return to main checkout
293
+ process.chdir(originalCwd);
294
+ // Determine if PR mode is enabled
295
+ const prModeEnabled = isPRModeEnabled(docMain, args);
296
+ if (!args.noMerge) {
297
+ // Use docForUpdate (from worktree) for branch calculation - docMain may be incomplete (ref: WU-1280)
298
+ const laneBranch = await defaultBranchFrom(docForUpdate);
299
+ if (laneBranch && (await branchExists(laneBranch))) {
300
+ if (prModeEnabled) {
301
+ // PR mode: Create PR instead of auto-merge
302
+ const prResult = await createPR({
303
+ branch: laneBranch,
304
+ id,
305
+ title,
306
+ doc: docMain,
307
+ draft: args.prDraft,
308
+ });
309
+ if (prResult.success && prResult.prUrl) {
310
+ printPRCreatedMessage(prResult.prUrl, id);
311
+ prUrl = prResult.prUrl;
312
+ }
313
+ }
314
+ else {
315
+ // Default mode: Auto-merge with pre-flight checks
316
+ console.log(PREFLIGHT.RUNNING);
317
+ // Check branch drift (WU-1370: graduated warnings)
318
+ const commitsBehind = await checkBranchDrift(laneBranch);
319
+ if (commitsBehind > 0) {
320
+ const driftLevel = getDriftLevel(commitsBehind);
321
+ if (driftLevel === DRIFT_LEVELS.WARNING) {
322
+ console.log(PREFLIGHT.BRANCH_DRIFT_WARNING(commitsBehind));
323
+ }
324
+ else if (driftLevel === DRIFT_LEVELS.INFO) {
325
+ console.log(PREFLIGHT.BRANCH_DRIFT_INFO(commitsBehind));
326
+ }
327
+ else if (driftLevel === DRIFT_LEVELS.OK) {
328
+ // No message needed for OK level
329
+ console.log(PREFLIGHT.BRANCH_BEHIND(commitsBehind, THRESHOLDS.BRANCH_DRIFT_MAX));
330
+ }
331
+ // ERROR level is handled by checkBranchDrift throwing an error
332
+ }
333
+ // Check if branch is already merged
334
+ const alreadyMerged = await isBranchAlreadyMerged(laneBranch);
335
+ if (alreadyMerged) {
336
+ console.log(PREFLIGHT.ALREADY_MERGED);
337
+ console.log(PREFLIGHT.ALREADY_MERGED_EXPLANATION);
338
+ }
339
+ else {
340
+ // Check for divergence and conflicts (auto-rebase if enabled - WU-1303)
341
+ // noAutoRebase is true when --no-auto-rebase flag is passed
342
+ // WU-1371: Pass wuId for post-rebase artifact cleanup
343
+ await checkBranchDivergence(laneBranch, {
344
+ autoRebase: args.noAutoRebase !== true,
345
+ worktreePath,
346
+ wuId: id,
347
+ });
348
+ // WU-1384: Check for merge commits (GitHub requires linear history)
349
+ // Must run AFTER divergence check, as divergence rebase may eliminate merge commits
350
+ // Catches case where user did 'git merge main' instead of rebase
351
+ // WU-1371: Pass wuId for post-rebase artifact cleanup
352
+ await checkMergeCommits(laneBranch, {
353
+ autoRebase: args.noAutoRebase !== true,
354
+ worktreePath,
355
+ wuId: id,
356
+ });
357
+ await checkMergeConflicts(laneBranch);
358
+ // WU-1456: Check for empty merge (warn if no work commits)
359
+ // WU-1460: Pass doc to enable code_paths blocker
360
+ await checkEmptyMerge(laneBranch, docForUpdate);
361
+ // WU-1574: Backlog repair removed - state store architecture eliminates duplicates
362
+ // Backlog.md is always regenerated from wu-events.jsonl, not parsed/modified
363
+ console.log(MERGE.STARTING(laneBranch));
364
+ // WU-1747: Wrap merge with lock for atomic operation under concurrent load
365
+ // WU-1749 Bug 2: Pass worktreePath and wuId for auto-rebase on retry
366
+ await withMergeLock(id, async () => {
367
+ await mergeLaneBranch(laneBranch, { worktreePath, wuId: id });
368
+ });
369
+ console.log(MERGE.ATOMIC_SUCCESS);
370
+ merged = true;
371
+ }
372
+ // Push from main
373
+ await getGitForCwd().push(REMOTES.ORIGIN, BRANCHES.MAIN);
374
+ console.log(MERGE.PUSHED(REMOTES.ORIGIN, BRANCHES.MAIN));
375
+ }
376
+ }
377
+ else {
378
+ // Branch not found - fail loudly (use docForUpdate which has complete lane info)
379
+ console.error(`\n${BOX.TOP}`);
380
+ console.error(`${BOX.SIDE} MERGE FAILED: Lane branch not found`);
381
+ console.error(BOX.MID);
382
+ console.error(`${BOX.SIDE} Expected branch: ${laneBranch || '(null)'}`);
383
+ console.error(`${BOX.SIDE} WU lane: "${docForUpdate.lane}"`);
384
+ console.error(`${BOX.SIDE} WU id: "${docForUpdate.id}"`);
385
+ console.error(BOX.BOT);
386
+ throw createError(ErrorCodes.BRANCH_ERROR, `Lane branch not found: ${laneBranch}`, {
387
+ laneBranch,
388
+ wuId: docForUpdate.id,
389
+ });
390
+ }
391
+ }
392
+ // WU-1335: Clear recovery attempts on successful completion
393
+ clearRecoveryAttempts(id);
394
+ // WU-1811: All steps succeeded - worktree cleanup is safe
395
+ return {
396
+ success: true,
397
+ committed: true,
398
+ pushed: !prModeEnabled,
399
+ merged,
400
+ prUrl,
401
+ cleanupSafe: true,
402
+ };
403
+ }
404
+ catch (err) {
405
+ // Restore original directory
406
+ try {
407
+ process.chdir(originalCwd);
408
+ }
409
+ catch {
410
+ // Ignore chdir errors during cleanup
411
+ }
412
+ // WU-1369: Atomic transaction pattern
413
+ // - If error occurred BEFORE transaction.commit() → no files were written
414
+ // - If error occurred AFTER transaction.commit() → files written, need manual recovery
415
+ const wasCommitted = transaction.isCommitted;
416
+ // WU-1811: Provide actionable single next step based on failure state
417
+ if (!wasCommitted) {
418
+ // Abort transaction (discards pending changes, no files were written)
419
+ transaction.abort();
420
+ console.log(`\n${BOX.TOP}`);
421
+ console.log(`${BOX.SIDE} WU:DONE FAILED - NO FILES MODIFIED (atomic pattern)`);
422
+ console.log(BOX.MID);
423
+ console.log(`${BOX.SIDE} Error: ${err.message}`);
424
+ console.log(BOX.SIDE);
425
+ console.log(`${BOX.SIDE} WU-1369: Transaction aborted before any writes.`);
426
+ console.log(`${BOX.SIDE} WU-1811: Worktree preserved for recovery.`);
427
+ console.log(`${BOX.SIDE} Worktree: ${worktreePath}`);
428
+ console.log(BOX.MID);
429
+ console.log(`${BOX.SIDE} NEXT STEP: Fix the error and rerun:`);
430
+ console.log(`${BOX.SIDE} pnpm wu:done --id ${id}`);
431
+ console.log(BOX.BOT);
432
+ }
433
+ else {
434
+ // Transaction was committed but git operations failed
435
+ // Files were written - need rollback or recovery
436
+ // WU-2310: Rollback file changes if git commit failed (before branch commit was made)
437
+ // This prevents zombie states where status=done but commit never happened
438
+ let fileRollbackSuccess = false;
439
+ if (!gitCommitMade && transactionSnapshot) {
440
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2310: Git commit failed after transaction - rolling back files...`);
441
+ try {
442
+ // cd into worktree for rollback
443
+ process.chdir(worktreePath);
444
+ const rollbackResult = restoreFromSnapshot(transactionSnapshot);
445
+ if (rollbackResult.errors.length === 0) {
446
+ fileRollbackSuccess = true;
447
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-2310: File rollback complete - ${rollbackResult.restored.length} files restored`);
448
+ }
449
+ else {
450
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2310: Partial file rollback - ${rollbackResult.restored.length} restored, ${rollbackResult.errors.length} failed`);
451
+ for (const e of rollbackResult.errors) {
452
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} ${e.path}: ${e.error}`);
453
+ }
454
+ }
455
+ // Return to main checkout
456
+ process.chdir(originalCwd);
457
+ }
458
+ catch (rollbackErr) {
459
+ // Log but don't fail - rollback is best-effort
460
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2310: File rollback error: ${rollbackErr.message}`);
461
+ try {
462
+ process.chdir(originalCwd);
463
+ }
464
+ catch {
465
+ // Ignore chdir errors
466
+ }
467
+ }
468
+ }
469
+ // WU-1943: If git commit was made but merge failed, rollback the branch
470
+ // This prevents zombie states where branch shows "done" but wasn't merged
471
+ if (gitCommitMade && preCommitSha) {
472
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Merge failed after git commit - attempting branch rollback...`);
473
+ try {
474
+ // cd into worktree for rollback
475
+ process.chdir(worktreePath);
476
+ const gitCwd = getGitForCwd();
477
+ await rollbackBranchOnMergeFailure(gitCwd, preCommitSha, id);
478
+ // Return to main checkout
479
+ process.chdir(originalCwd);
480
+ }
481
+ catch (rollbackErr) {
482
+ // Log but don't fail - rollback is best-effort
483
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Rollback error: ${rollbackErr.message}`);
484
+ try {
485
+ process.chdir(originalCwd);
486
+ }
487
+ catch {
488
+ // Ignore chdir errors
489
+ }
490
+ }
491
+ }
492
+ console.log(`\n${BOX.TOP}`);
493
+ if (fileRollbackSuccess) {
494
+ console.log(`${BOX.SIDE} WU:DONE FAILED - FILES ROLLED BACK (WU-2310)`);
495
+ console.log(BOX.MID);
496
+ console.log(`${BOX.SIDE} Error: ${err.message}`);
497
+ console.log(BOX.SIDE);
498
+ console.log(`${BOX.SIDE} WU-2310: Transaction files were rolled back to pre-commit state.`);
499
+ console.log(`${BOX.SIDE} Worktree is now consistent (status=in_progress, no stamp).`);
500
+ }
501
+ else {
502
+ console.log(`${BOX.SIDE} WU:DONE FAILED - PARTIAL STATE (post-transaction)`);
503
+ console.log(BOX.MID);
504
+ console.log(`${BOX.SIDE} Error: ${err.message}`);
505
+ console.log(BOX.SIDE);
506
+ console.log(`${BOX.SIDE} Metadata files were written, but git operations failed.`);
507
+ if (gitCommitMade && preCommitSha) {
508
+ console.log(`${BOX.SIDE} WU-1943: Branch rolled back to pre-commit state.`);
509
+ }
510
+ }
511
+ console.log(`${BOX.SIDE} WU-1811: Worktree preserved for recovery.`);
512
+ console.log(`${BOX.SIDE} Worktree: ${worktreePath}`);
513
+ console.log(BOX.MID);
514
+ console.log(`${BOX.SIDE} NEXT STEP: Rerun wu:done (idempotent recovery):`);
515
+ console.log(`${BOX.SIDE} pnpm wu:done --id ${id}`);
516
+ console.log(BOX.BOT);
517
+ }
518
+ // WU-1811: Attach cleanupSafe flag to error for caller to check
519
+ err.cleanupSafe = false;
520
+ throw err;
521
+ }
522
+ }
523
+ /**
524
+ * Check for branch drift (commits behind main)
525
+ * WU-755 pre-flight check
526
+ *
527
+ * @param {string} branch - Lane branch name
528
+ * @returns {Promise<number>} Number of commits behind main
529
+ */
530
+ export async function checkBranchDrift(branch) {
531
+ const gitAdapter = getGitForCwd();
532
+ try {
533
+ const counts = await gitAdapter.revList([
534
+ '--left-right',
535
+ '--count',
536
+ `${BRANCHES.MAIN}...${branch}`,
537
+ ]);
538
+ const [mainAhead] = counts.split(/\s+/).map(Number);
539
+ if (mainAhead > THRESHOLDS.BRANCH_DRIFT_MAX) {
540
+ throw createError(ErrorCodes.GIT_ERROR, PREFLIGHT.BRANCH_DRIFT_ERROR(mainAhead, THRESHOLDS.BRANCH_DRIFT_MAX, REMOTES.ORIGIN, BRANCHES.MAIN), { branch, commitsBehind: mainAhead, threshold: THRESHOLDS.BRANCH_DRIFT_MAX });
541
+ }
542
+ return mainAhead;
543
+ }
544
+ catch (e) {
545
+ if (e.code === ErrorCodes.GIT_ERROR)
546
+ throw e;
547
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not check branch drift: ${e.message}`);
548
+ return 0;
549
+ }
550
+ }
551
+ /**
552
+ * List of append-only files that can be auto-resolved during rebase
553
+ * WU-1749 Bug 3: These files can safely have conflicts resolved by keeping both additions
554
+ *
555
+ * Uses WU_PATHS constants and WU_EVENTS_FILE_NAME to avoid hardcoded path strings
556
+ * that would break if paths are rearranged.
557
+ */
558
+ const APPEND_ONLY_FILES = [
559
+ // State store events file (append-only by design)
560
+ path.join('.beacon', 'state', WU_EVENTS_FILE_NAME),
561
+ // Status and backlog are generated from state store but may conflict during rebase
562
+ WU_PATHS.STATUS(),
563
+ WU_PATHS.BACKLOG(),
564
+ ];
565
+ const WU_EVENTS_PATH = path.join('.beacon', 'state', WU_EVENTS_FILE_NAME);
566
+ function normalizeEventForKey(event) {
567
+ const normalized = {};
568
+ for (const key of Object.keys(event).sort()) {
569
+ // eslint-disable-next-line security/detect-object-injection -- keys derived from object keys
570
+ normalized[key] = event[key];
571
+ }
572
+ return normalized;
573
+ }
574
+ function parseWuEventsJsonl(content, sourceLabel) {
575
+ const lines = String(content)
576
+ .split('\n')
577
+ .map((l) => l.trim())
578
+ .filter(Boolean);
579
+ return lines.map((line, index) => {
580
+ let parsed;
581
+ try {
582
+ parsed = JSON.parse(line);
583
+ }
584
+ catch (error) {
585
+ throw new Error(`wu-events.jsonl ${sourceLabel} has malformed JSON on line ${index + 1}: ${error.message}`);
586
+ }
587
+ const validation = validateWUEvent(parsed);
588
+ if (!validation.success) {
589
+ const issues = validation.error.issues
590
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
591
+ .join(', ');
592
+ throw new Error(`wu-events.jsonl ${sourceLabel} has invalid event on line ${index + 1}: ${issues}`);
593
+ }
594
+ return { event: validation.data, line };
595
+ });
596
+ }
597
+ async function resolveWuEventsJsonlConflict(gitCwd, filePath) {
598
+ const ours = await gitCwd.raw(['show', `:2:${filePath}`]);
599
+ const theirs = await gitCwd.raw(['show', `:3:${filePath}`]);
600
+ const theirsEvents = parseWuEventsJsonl(theirs, 'theirs');
601
+ const oursEvents = parseWuEventsJsonl(ours, 'ours');
602
+ const seen = new Set();
603
+ const mergedLines = [];
604
+ for (const { event, line } of theirsEvents) {
605
+ const key = JSON.stringify(normalizeEventForKey(event));
606
+ if (seen.has(key))
607
+ continue;
608
+ seen.add(key);
609
+ mergedLines.push(line);
610
+ }
611
+ for (const { event, line } of oursEvents) {
612
+ const key = JSON.stringify(normalizeEventForKey(event));
613
+ if (seen.has(key))
614
+ continue;
615
+ seen.add(key);
616
+ mergedLines.push(line);
617
+ }
618
+ await writeFile(filePath, mergedLines.join('\n') + '\n', 'utf-8');
619
+ await gitCwd.add(filePath);
620
+ }
621
+ /**
622
+ * Auto-resolve conflicts in append-only files during rebase
623
+ * WU-1749 Bug 3: Keeps both additions for append-only files
624
+ *
625
+ * @param {object} gitCwd - Git adapter instance
626
+ * @returns {Promise<{resolved: boolean, files: string[]}>} Resolution result
627
+ */
628
+ async function autoResolveAppendOnlyConflicts(gitCwd) {
629
+ const resolvedFiles = [];
630
+ try {
631
+ // Get list of conflicted files
632
+ const status = await gitCwd.getStatus();
633
+ const conflictLines = status.split('\n').filter((line) => line.startsWith('UU '));
634
+ for (const line of conflictLines) {
635
+ const filePath = line.substring(3).trim();
636
+ // Check if this is an append-only file
637
+ const isAppendOnly = APPEND_ONLY_FILES.some((appendFile) => filePath.endsWith(appendFile) || filePath.includes(appendFile));
638
+ if (isAppendOnly) {
639
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Auto-resolving append-only conflict: ${filePath}`);
640
+ if (filePath.endsWith(WU_EVENTS_PATH) || filePath.includes(WU_EVENTS_PATH)) {
641
+ // For the event log we must keep BOTH sides (loss breaks state machine).
642
+ // Merge strategy: union by event identity (validated), prefer theirs ordering then ours additions.
643
+ await resolveWuEventsJsonlConflict(gitCwd, filePath);
644
+ }
645
+ else {
646
+ // Backlog/status are derived; prefer main's version during rebase and regenerate later.
647
+ await gitCwd.raw(['checkout', '--theirs', filePath]);
648
+ await gitCwd.add(filePath);
649
+ }
650
+ resolvedFiles.push(filePath);
651
+ }
652
+ }
653
+ return { resolved: resolvedFiles.length > 0, files: resolvedFiles };
654
+ }
655
+ catch (error) {
656
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not auto-resolve conflicts: ${error.message}`);
657
+ return { resolved: false, files: [] };
658
+ }
659
+ }
660
+ /**
661
+ * Auto-rebase branch onto main
662
+ * WU-1303: Auto-rebase on wu:done to handle diverged branches automatically
663
+ * WU-1371: Added wuId parameter for post-rebase artifact cleanup
664
+ * WU-1749 Bug 3: Auto-resolve append-only file conflicts during rebase
665
+ *
666
+ * @param {string} branch - Lane branch name
667
+ * @param {string} worktreePath - Path to worktree
668
+ * @param {string} [wuId] - WU ID for artifact cleanup (e.g., 'WU-1371')
669
+ * @returns {Promise<{success: boolean, error?: string}>} Rebase result
670
+ */
671
+ export async function autoRebaseBranch(branch, worktreePath, wuId) {
672
+ console.log(REBASE.STARTING(branch, BRANCHES.MAIN));
673
+ // Save original cwd
674
+ const originalCwd = process.cwd();
675
+ const previousEditor = process.env.GIT_EDITOR;
676
+ process.env.GIT_EDITOR = 'true';
677
+ try {
678
+ // cd into worktree for rebase
679
+ process.chdir(worktreePath);
680
+ const gitCwd = getGitForCwd();
681
+ // Fetch latest main
682
+ await gitCwd.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
683
+ // Attempt rebase
684
+ try {
685
+ await gitCwd.rebase(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
686
+ }
687
+ catch (rebaseError) {
688
+ // WU-1749 Bug 3: Check if conflicts are in append-only files that can be auto-resolved
689
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Rebase hit conflicts, checking for auto-resolvable...`);
690
+ const resolution = await autoResolveAppendOnlyConflicts(gitCwd);
691
+ if (resolution.resolved) {
692
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Auto-resolved ${resolution.files.length} append-only conflict(s)`);
693
+ // Continue the rebase after resolving conflicts
694
+ try {
695
+ await gitCwd.raw(['rebase', '--continue']);
696
+ }
697
+ catch (continueError) {
698
+ // May need multiple rounds of conflict resolution
699
+ // For simplicity, we'll try once more
700
+ const secondResolution = await autoResolveAppendOnlyConflicts(gitCwd);
701
+ if (secondResolution.resolved) {
702
+ await gitCwd.raw(['rebase', '--continue']);
703
+ }
704
+ else {
705
+ // Still have non-auto-resolvable conflicts
706
+ throw continueError;
707
+ }
708
+ }
709
+ }
710
+ else {
711
+ // No auto-resolvable conflicts - rethrow original error
712
+ throw rebaseError;
713
+ }
714
+ }
715
+ // WU-1371: Detect and cleanup rebased completion artifacts
716
+ // After rebase, check if main's completion artifacts (stamps, status=done)
717
+ // were pulled into the worktree. These must be cleaned before continuing.
718
+ // WU-1817: Now passes gitCwd to verify artifacts exist on origin/main
719
+ if (wuId) {
720
+ const artifacts = await detectRebasedArtifacts(worktreePath, wuId, gitCwd);
721
+ if (artifacts.hasArtifacts) {
722
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Detected rebased completion artifacts`);
723
+ const cleanup = await cleanupRebasedArtifacts(worktreePath, wuId);
724
+ if (cleanup.cleaned) {
725
+ // Stage and commit the cleanup
726
+ await gitCwd.add('.');
727
+ await gitCwd.commit(COMMIT_FORMATS.REBASE_ARTIFACT_CLEANUP(wuId));
728
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Cleaned rebased artifacts and committed`);
729
+ }
730
+ }
731
+ }
732
+ // Force-push lane branch with lease (safe force push)
733
+ await gitCwd.raw(['push', '--force-with-lease', REMOTES.ORIGIN, branch]);
734
+ console.log(REBASE.SUCCESS);
735
+ return { success: true };
736
+ }
737
+ catch (e) {
738
+ // Rebase failed (likely conflicts) - abort and report
739
+ console.error(REBASE.FAILED(e.message));
740
+ try {
741
+ // Abort the failed rebase to leave worktree clean
742
+ const gitCwd = getGitForCwd();
743
+ await gitCwd.raw(['rebase', '--abort']);
744
+ console.log(REBASE.ABORTED);
745
+ }
746
+ catch {
747
+ // Ignore abort errors - may already be clean
748
+ }
749
+ return {
750
+ success: false,
751
+ error: REBASE.MANUAL_FIX(worktreePath, REMOTES.ORIGIN, BRANCHES.MAIN, branch),
752
+ };
753
+ }
754
+ finally {
755
+ if (previousEditor === undefined) {
756
+ delete process.env.GIT_EDITOR;
757
+ }
758
+ else {
759
+ process.env.GIT_EDITOR = previousEditor;
760
+ }
761
+ // Always return to original directory
762
+ try {
763
+ process.chdir(originalCwd);
764
+ }
765
+ catch {
766
+ // Ignore chdir errors
767
+ }
768
+ }
769
+ }
770
+ export async function checkBranchDivergence(branch, options = {}) {
771
+ const { autoRebase = true, worktreePath = null, wuId = null } = options;
772
+ const gitAdapter = getGitForCwd();
773
+ try {
774
+ const mergeBase = await gitAdapter.mergeBase(BRANCHES.MAIN, branch);
775
+ const mainHead = await gitAdapter.getCommitHash(BRANCHES.MAIN);
776
+ if (mergeBase !== mainHead) {
777
+ const mainCommitsAhead = await gitAdapter.revList([
778
+ '--count',
779
+ `${mergeBase}..${BRANCHES.MAIN}`,
780
+ ]);
781
+ const commitCount = Number(mainCommitsAhead);
782
+ console.log(PREFLIGHT.DIVERGENCE_DETECTED(commitCount));
783
+ // Attempt auto-rebase if enabled and worktree path provided
784
+ if (autoRebase && worktreePath) {
785
+ const rebaseResult = await autoRebaseBranch(branch, worktreePath, wuId);
786
+ if (rebaseResult.success) {
787
+ // Rebase succeeded - continue with wu:done
788
+ return;
789
+ }
790
+ // Rebase failed - throw with detailed instructions
791
+ throw createError(ErrorCodes.GIT_ERROR, rebaseResult.error, {
792
+ branch,
793
+ mergeBase,
794
+ mainHead,
795
+ mainCommitsAhead: commitCount,
796
+ autoRebaseAttempted: true,
797
+ });
798
+ }
799
+ // Auto-rebase disabled or no worktree path - throw with manual instructions
800
+ throw createError(ErrorCodes.GIT_ERROR, PREFLIGHT.DIVERGENCE_ERROR(commitCount, REMOTES.ORIGIN, BRANCHES.MAIN, branch), { branch, mergeBase, mainHead, mainCommitsAhead: commitCount });
801
+ }
802
+ console.log(PREFLIGHT.NO_DIVERGENCE);
803
+ }
804
+ catch (e) {
805
+ if (e.code === ErrorCodes.GIT_ERROR)
806
+ throw e;
807
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not check branch divergence: ${e.message}`);
808
+ }
809
+ }
810
+ /**
811
+ * Check for merge commits in lane branch that would violate linear history
812
+ * WU-1384: GitHub requires linear history; merge commits in lane branches must be eliminated
813
+ * WU-1371: Added wuId option for post-rebase artifact cleanup
814
+ *
815
+ * If merge commits are found, triggers auto-rebase to linearize history.
816
+ *
817
+ * @param {string} branch - Lane branch name
818
+ * @param {CheckBranchOptions} [options] - Check options
819
+ * @throws {Error} If merge commits found and auto-rebase fails or is disabled
820
+ */
821
+ export async function checkMergeCommits(branch, options = {}) {
822
+ const { autoRebase = true, worktreePath = null, wuId = null } = options;
823
+ const gitAdapter = getGitForCwd();
824
+ try {
825
+ // Find merge commits in lane branch that are not in main
826
+ // --merges: only merge commits
827
+ // main..branch: commits in branch not reachable from main
828
+ const mergeCommitsRaw = await gitAdapter.raw([
829
+ 'rev-list',
830
+ '--merges',
831
+ `${BRANCHES.MAIN}..${branch}`,
832
+ ]);
833
+ const mergeCommits = mergeCommitsRaw.trim().split(STRING_LITERALS.NEWLINE).filter(Boolean);
834
+ const mergeCount = mergeCommits.length;
835
+ if (mergeCount > 0) {
836
+ console.log(PREFLIGHT.MERGE_COMMITS_DETECTED(mergeCount));
837
+ // Trigger rebase to eliminate merge commits
838
+ if (autoRebase && worktreePath) {
839
+ console.log(PREFLIGHT.MERGE_COMMITS_REBASING);
840
+ const rebaseResult = await autoRebaseBranch(branch, worktreePath, wuId);
841
+ if (rebaseResult.success) {
842
+ // Rebase succeeded - merge commits eliminated
843
+ return;
844
+ }
845
+ // Rebase failed - throw with detailed instructions
846
+ throw createError(ErrorCodes.GIT_ERROR, rebaseResult.error, {
847
+ branch,
848
+ mergeCommitCount: mergeCount,
849
+ autoRebaseAttempted: true,
850
+ });
851
+ }
852
+ // Auto-rebase disabled or no worktree path - throw with manual instructions
853
+ throw createError(ErrorCodes.GIT_ERROR, `Branch ${branch} contains ${mergeCount} merge commit(s).\n\n` +
854
+ `GitHub requires linear history. Merge commits must be eliminated.\n\n` +
855
+ `REQUIRED: Rebase your branch to linearize history:\n` +
856
+ ` 1. cd into your worktree\n` +
857
+ ` 2. git fetch ${REMOTES.ORIGIN} ${BRANCHES.MAIN}\n` +
858
+ ` 3. git rebase ${REMOTES.ORIGIN}/${BRANCHES.MAIN}\n` +
859
+ ` 4. git push --force-with-lease ${REMOTES.ORIGIN} ${branch}\n` +
860
+ ` 5. Return to main checkout and retry`, { branch, mergeCommitCount: mergeCount });
861
+ }
862
+ console.log(PREFLIGHT.NO_MERGE_COMMITS);
863
+ }
864
+ catch (e) {
865
+ if (e.code === ErrorCodes.GIT_ERROR)
866
+ throw e;
867
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not check for merge commits: ${e.message}`);
868
+ }
869
+ }
870
+ /**
871
+ * Check for merge conflicts using git merge-tree
872
+ * WU-755 pre-flight check
873
+ *
874
+ * @param {string} branch - Lane branch name
875
+ */
876
+ export async function checkMergeConflicts(branch) {
877
+ const gitAdapter = getGitForCwd();
878
+ try {
879
+ const mergeBase = await gitAdapter.mergeBase(BRANCHES.MAIN, branch);
880
+ const result = await gitAdapter.mergeTree(mergeBase, BRANCHES.MAIN, branch);
881
+ if (result.includes('<<<<<<<') || result.includes('>>>>>>>')) {
882
+ throw createError(ErrorCodes.GIT_ERROR, PREFLIGHT.CONFLICT_ERROR, {
883
+ branch,
884
+ operation: 'merge-tree',
885
+ });
886
+ }
887
+ console.log(PREFLIGHT.NO_CONFLICTS);
888
+ }
889
+ catch (e) {
890
+ if (e.code === ErrorCodes.GIT_ERROR)
891
+ throw e;
892
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not check merge conflicts: ${e.message}`);
893
+ }
894
+ }
895
+ /**
896
+ * WU-1456: Check for empty merge (no work commits beyond claim)
897
+ * WU-1460: Upgraded to BLOCK when code_paths defined but files not modified
898
+ *
899
+ * Detects when an agent runs wu:done without committing actual work.
900
+ * - If code_paths defined: BLOCK if those files weren't modified
901
+ * - If no code_paths: WARNING only (docs-only or metadata updates are valid)
902
+ *
903
+ * @param {string} branch - Lane branch name
904
+ * @param {object} [doc] - WU document with code_paths array (optional for backwards compatibility)
905
+ * @returns {Promise<void>}
906
+ * @throws {Error} When code_paths defined but files not modified in commits
907
+ */
908
+ export async function checkEmptyMerge(branch, doc = null) {
909
+ const gitAdapter = getGitForCwd();
910
+ try {
911
+ // Count commits on lane branch that are not in main
912
+ const commitCountRaw = await gitAdapter.raw([
913
+ 'rev-list',
914
+ '--count',
915
+ `${BRANCHES.MAIN}..${branch}`,
916
+ ]);
917
+ const commitCount = Number(commitCountRaw.trim());
918
+ // WU-1460: If code_paths defined, verify those files were modified
919
+ const codePaths = doc?.code_paths || [];
920
+ const hasCodePaths = Array.isArray(codePaths) && codePaths.length > 0;
921
+ if (hasCodePaths) {
922
+ // Get list of files modified in lane branch commits
923
+ const modifiedFilesRaw = await gitAdapter.raw([
924
+ 'diff',
925
+ '--name-only',
926
+ `${BRANCHES.MAIN}...${branch}`,
927
+ ]);
928
+ const modifiedFiles = modifiedFilesRaw.trim().split('\n').filter(Boolean);
929
+ // Check if any code_paths files are in the modified list
930
+ const missingCodePaths = codePaths.filter((codePath) => !modifiedFiles.some((modified) => modified.includes(codePath) || codePath.includes(modified)));
931
+ if (missingCodePaths.length > 0) {
932
+ // BLOCK: code_paths defined but files not modified
933
+ throw createError(ErrorCodes.VALIDATION_ERROR, PREFLIGHT.CODE_PATHS_NOT_MODIFIED(missingCodePaths), { branch, codePaths, missingCodePaths, modifiedFiles });
934
+ }
935
+ // All code_paths files were modified
936
+ console.log(PREFLIGHT.CODE_PATHS_VERIFIED);
937
+ }
938
+ else if (commitCount <= 1) {
939
+ // No code_paths - just warn (backwards compatible behaviour)
940
+ // If only 0-1 commits beyond main, this is likely the claim commit only
941
+ console.log(PREFLIGHT.EMPTY_MERGE_WARNING(commitCount));
942
+ }
943
+ else {
944
+ console.log(PREFLIGHT.EMPTY_MERGE_CHECK);
945
+ }
946
+ }
947
+ catch (e) {
948
+ // Re-throw validation errors (WU-1460 blocker)
949
+ if (e.code === ErrorCodes.VALIDATION_ERROR)
950
+ throw e;
951
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not check for empty merge: ${e.message}`);
952
+ }
953
+ }
954
+ /**
955
+ * Check if branch is already merged to main
956
+ *
957
+ * @param {string} branch - Lane branch name
958
+ * @returns {Promise<boolean>} Whether branch is already merged
959
+ */
960
+ /** @constant {number} SHA_SHORT_LENGTH - Length of shortened git SHA hashes for display */
961
+ const SHA_SHORT_LENGTH = 8;
962
+ export async function isBranchAlreadyMerged(branch) {
963
+ const gitAdapter = getGitForCwd();
964
+ try {
965
+ const branchTip = (await gitAdapter.getCommitHash(branch)).trim();
966
+ const mergeBase = (await gitAdapter.mergeBase(BRANCHES.MAIN, branch)).trim();
967
+ const mainHead = (await gitAdapter.getCommitHash(BRANCHES.MAIN)).trim();
968
+ if (branchTip === mergeBase) {
969
+ console.log(PREFLIGHT.BRANCH_INFO(branch, branchTip.substring(0, SHA_SHORT_LENGTH), mergeBase.substring(0, SHA_SHORT_LENGTH), mainHead.substring(0, SHA_SHORT_LENGTH)));
970
+ return true;
971
+ }
972
+ return false;
973
+ }
974
+ catch (e) {
975
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not check if branch is merged: ${e.message}`);
976
+ return false;
977
+ }
978
+ }
979
+ async function isMainAncestorOfBranch(gitAdapter, branch) {
980
+ try {
981
+ await gitAdapter.raw([GIT_COMMANDS.MERGE_BASE, GIT_FLAGS.IS_ANCESTOR, BRANCHES.MAIN, branch]);
982
+ return true;
983
+ }
984
+ catch {
985
+ return false;
986
+ }
987
+ }
988
+ export async function mergeLaneBranch(branch, options = {}) {
989
+ const gitAdapter = getGitForCwd();
990
+ console.log(MERGE.BRANCH_MERGE(branch));
991
+ // WU-1747: Use exponential backoff retry for merge operations
992
+ // WU-1749 Bug 2: Now rebases lane branch on retry instead of just pulling main
993
+ const retryConfig = createRetryConfig('wu_done', {
994
+ maxAttempts: options.maxAttempts,
995
+ onRetry: async (attempt, error, delay) => {
996
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Merge attempt ${attempt} failed: ${error.message}`);
997
+ // WU-1749 Bug 2: Rebase lane branch onto new main instead of just pulling
998
+ // This is required because ff-only merge will always fail if the lane branch
999
+ // is still based on the old main after main has advanced
1000
+ if (options.worktreePath) {
1001
+ const mainIsAncestor = await isMainAncestorOfBranch(gitAdapter, branch);
1002
+ if (mainIsAncestor) {
1003
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Main is already an ancestor - skipping auto-rebase`);
1004
+ return;
1005
+ }
1006
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Auto-rebasing lane branch onto latest main...`);
1007
+ const rebaseResult = await autoRebaseBranch(branch, options.worktreePath, options.wuId);
1008
+ if (rebaseResult.success) {
1009
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane branch rebased - ff-only merge should succeed`);
1010
+ }
1011
+ else {
1012
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Auto-rebase failed: ${rebaseResult.error}`);
1013
+ // Fall back to pulling main (won't help ff-only but maintains old behaviour)
1014
+ try {
1015
+ await gitAdapter.pull(REMOTES.ORIGIN, BRANCHES.MAIN);
1016
+ console.log(MERGE.UPDATED_MAIN(REMOTES.ORIGIN));
1017
+ }
1018
+ catch (pullErr) {
1019
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Pull also failed: ${pullErr.message}`);
1020
+ }
1021
+ }
1022
+ }
1023
+ else {
1024
+ // No worktree path - fall back to old behaviour (pull only)
1025
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Pulling latest main before retry...`);
1026
+ try {
1027
+ await gitAdapter.pull(REMOTES.ORIGIN, BRANCHES.MAIN);
1028
+ console.log(MERGE.UPDATED_MAIN(REMOTES.ORIGIN));
1029
+ }
1030
+ catch (pullErr) {
1031
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Pull failed: ${pullErr.message} - will retry anyway`);
1032
+ }
1033
+ }
1034
+ },
1035
+ });
1036
+ try {
1037
+ await withRetry(async () => {
1038
+ await gitAdapter.merge(branch, { ffOnly: true });
1039
+ }, retryConfig);
1040
+ console.log(MERGE.SUCCESS(branch));
1041
+ }
1042
+ catch (e) {
1043
+ // All retries exhausted
1044
+ const mainIsAncestor = await isMainAncestorOfBranch(gitAdapter, branch);
1045
+ const message = mainIsAncestor
1046
+ ? MERGE.FF_FAILED_NON_DIVERGED_ERROR(branch, e.message)
1047
+ : MERGE.FF_DIVERGED_ERROR(branch, e.message);
1048
+ throw createError(ErrorCodes.GIT_ERROR, message, {
1049
+ branch,
1050
+ originalError: e.message,
1051
+ retriesExhausted: true,
1052
+ mainIsAncestor,
1053
+ });
1054
+ }
1055
+ }
1056
+ /**
1057
+ * WU-1943: Check if the session has checkpoints for the given WU
1058
+ *
1059
+ * Used to warn agents when they're completing a WU without any checkpoints,
1060
+ * which means no recovery data if the session crashes.
1061
+ *
1062
+ * @param {string} wuId - WU ID to check
1063
+ * @param {Array|null} nodes - Memory nodes for the WU (from queryByWu)
1064
+ * @returns {boolean} True if checkpoints exist, false otherwise
1065
+ */
1066
+ export function hasSessionCheckpoints(wuId, nodes) {
1067
+ if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
1068
+ return false;
1069
+ }
1070
+ return nodes.some((node) => node.type === 'checkpoint');
1071
+ }
1072
+ /**
1073
+ * WU-1943: Rollback branch to pre-commit SHA when merge fails
1074
+ *
1075
+ * When wu:done commits metadata to the lane branch but the subsequent merge
1076
+ * to main fails, this function rolls back the branch to its pre-commit state.
1077
+ * This prevents "zombie" states where the branch shows done but wasn't merged.
1078
+ *
1079
+ * @param {object} gitAdapter - Git adapter instance (must be in worktree context)
1080
+ * @param {string} preCommitSha - SHA to reset to (captured before metadata commit)
1081
+ * @param {string} wuId - WU ID for logging
1082
+ * @returns {Promise<{success: boolean, error?: string}>} Rollback result
1083
+ */
1084
+ export async function rollbackBranchOnMergeFailure(gitAdapter, preCommitSha, wuId) {
1085
+ try {
1086
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1943: Rolling back ${wuId} branch to pre-commit state...`);
1087
+ // WU-2236: GitAdapter.reset expects (ref: string, options?: { hard?: boolean })
1088
+ // NOT an array like ['--hard', sha]
1089
+ await gitAdapter.reset(preCommitSha, { hard: true });
1090
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1943: Branch rollback complete for ${wuId}`);
1091
+ return { success: true };
1092
+ }
1093
+ catch (error) {
1094
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1943: Could not rollback branch for ${wuId}: ${error.message}`);
1095
+ return { success: false, error: error.message };
1096
+ }
1097
+ }