@lumenflow/core 2.18.2 → 2.19.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 (167) hide show
  1. package/dist/adapters/terminal-renderer.adapter.d.ts.map +1 -1
  2. package/dist/adapters/terminal-renderer.adapter.js +6 -4
  3. package/dist/adapters/terminal-renderer.adapter.js.map +1 -1
  4. package/dist/atomic-merge.d.ts +21 -0
  5. package/dist/atomic-merge.d.ts.map +1 -0
  6. package/dist/atomic-merge.js +83 -0
  7. package/dist/atomic-merge.js.map +1 -0
  8. package/dist/delegation-escalation.d.ts +91 -0
  9. package/dist/delegation-escalation.d.ts.map +1 -0
  10. package/dist/delegation-escalation.js +258 -0
  11. package/dist/delegation-escalation.js.map +1 -0
  12. package/dist/delegation-monitor.d.ts +230 -0
  13. package/dist/delegation-monitor.d.ts.map +1 -0
  14. package/dist/delegation-monitor.js +675 -0
  15. package/dist/delegation-monitor.js.map +1 -0
  16. package/dist/delegation-recovery.d.ts +83 -0
  17. package/dist/delegation-recovery.d.ts.map +1 -0
  18. package/dist/delegation-recovery.js +299 -0
  19. package/dist/delegation-recovery.js.map +1 -0
  20. package/dist/delegation-registry-schema.d.ts +80 -0
  21. package/dist/delegation-registry-schema.d.ts.map +1 -0
  22. package/dist/delegation-registry-schema.js +91 -0
  23. package/dist/delegation-registry-schema.js.map +1 -0
  24. package/dist/delegation-registry-store.d.ts +159 -0
  25. package/dist/delegation-registry-store.d.ts.map +1 -0
  26. package/dist/delegation-registry-store.js +299 -0
  27. package/dist/delegation-registry-store.js.map +1 -0
  28. package/dist/delegation-tree.d.ts +57 -0
  29. package/dist/delegation-tree.d.ts.map +1 -0
  30. package/dist/delegation-tree.js +203 -0
  31. package/dist/delegation-tree.js.map +1 -0
  32. package/dist/gates-agent-mode.d.ts +25 -0
  33. package/dist/gates-agent-mode.d.ts.map +1 -1
  34. package/dist/gates-agent-mode.js +41 -0
  35. package/dist/gates-agent-mode.js.map +1 -1
  36. package/dist/index.d.ts +10 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +14 -9
  39. package/dist/index.js.map +1 -1
  40. package/dist/lumenflow-config-schema.d.ts +9 -3
  41. package/dist/lumenflow-config-schema.d.ts.map +1 -1
  42. package/dist/lumenflow-config-schema.js +18 -3
  43. package/dist/lumenflow-config-schema.js.map +1 -1
  44. package/dist/lumenflow-config.d.ts +2 -0
  45. package/dist/lumenflow-config.d.ts.map +1 -1
  46. package/dist/lumenflow-config.js +1 -0
  47. package/dist/lumenflow-config.js.map +1 -1
  48. package/dist/micro-worktree-shared.d.ts +134 -0
  49. package/dist/micro-worktree-shared.d.ts.map +1 -0
  50. package/dist/micro-worktree-shared.js +350 -0
  51. package/dist/micro-worktree-shared.js.map +1 -0
  52. package/dist/micro-worktree.d.ts +4 -273
  53. package/dist/micro-worktree.d.ts.map +1 -1
  54. package/dist/micro-worktree.js +17 -549
  55. package/dist/micro-worktree.js.map +1 -1
  56. package/dist/rollback-utils.d.ts +52 -0
  57. package/dist/rollback-utils.d.ts.map +1 -1
  58. package/dist/rollback-utils.js +111 -0
  59. package/dist/rollback-utils.js.map +1 -1
  60. package/dist/schemas/index.d.ts +3 -3
  61. package/dist/schemas/index.d.ts.map +1 -1
  62. package/dist/schemas/index.js +6 -6
  63. package/dist/schemas/index.js.map +1 -1
  64. package/dist/schemas/initiative-arg-validators.d.ts +1 -0
  65. package/dist/schemas/initiative-arg-validators.d.ts.map +1 -1
  66. package/dist/schemas/initiative-schemas.d.ts +3 -1
  67. package/dist/schemas/initiative-schemas.d.ts.map +1 -1
  68. package/dist/schemas/initiative-schemas.js +2 -1
  69. package/dist/schemas/initiative-schemas.js.map +1 -1
  70. package/dist/schemas/setup-arg-validators.d.ts +4 -4
  71. package/dist/schemas/setup-arg-validators.d.ts.map +1 -1
  72. package/dist/schemas/setup-arg-validators.js +6 -6
  73. package/dist/schemas/setup-arg-validators.js.map +1 -1
  74. package/dist/schemas/setup-schemas.d.ts +7 -7
  75. package/dist/schemas/setup-schemas.d.ts.map +1 -1
  76. package/dist/schemas/setup-schemas.js +10 -10
  77. package/dist/schemas/setup-schemas.js.map +1 -1
  78. package/dist/schemas/wu-lifecycle-arg-validators.d.ts +2 -1
  79. package/dist/schemas/wu-lifecycle-arg-validators.d.ts.map +1 -1
  80. package/dist/schemas/wu-lifecycle-schemas.d.ts +5 -3
  81. package/dist/schemas/wu-lifecycle-schemas.d.ts.map +1 -1
  82. package/dist/schemas/wu-lifecycle-schemas.js +5 -1
  83. package/dist/schemas/wu-lifecycle-schemas.js.map +1 -1
  84. package/dist/template-loader.d.ts +7 -3
  85. package/dist/template-loader.d.ts.map +1 -1
  86. package/dist/template-loader.js +22 -6
  87. package/dist/template-loader.js.map +1 -1
  88. package/dist/wu-consistency-checker.d.ts +1 -0
  89. package/dist/wu-consistency-checker.d.ts.map +1 -1
  90. package/dist/wu-consistency-checker.js +31 -2
  91. package/dist/wu-consistency-checker.js.map +1 -1
  92. package/dist/wu-context-constants.d.ts +0 -2
  93. package/dist/wu-context-constants.d.ts.map +1 -1
  94. package/dist/wu-context-constants.js +0 -2
  95. package/dist/wu-context-constants.js.map +1 -1
  96. package/dist/wu-done-branch-only.d.ts +2 -11
  97. package/dist/wu-done-branch-only.d.ts.map +1 -1
  98. package/dist/wu-done-branch-only.js +81 -45
  99. package/dist/wu-done-branch-only.js.map +1 -1
  100. package/dist/wu-done-cleanup.js +33 -1
  101. package/dist/wu-done-cleanup.js.map +1 -1
  102. package/dist/wu-done-initiative-sync.d.ts.map +1 -1
  103. package/dist/wu-done-initiative-sync.js +20 -5
  104. package/dist/wu-done-initiative-sync.js.map +1 -1
  105. package/dist/wu-done-machine.d.ts +175 -0
  106. package/dist/wu-done-machine.d.ts.map +1 -0
  107. package/dist/wu-done-machine.js +225 -0
  108. package/dist/wu-done-machine.js.map +1 -0
  109. package/dist/wu-done-metadata.d.ts.map +1 -1
  110. package/dist/wu-done-metadata.js +3 -1
  111. package/dist/wu-done-metadata.js.map +1 -1
  112. package/dist/wu-done-validation.d.ts +0 -37
  113. package/dist/wu-done-validation.d.ts.map +1 -1
  114. package/dist/wu-done-validation.js +1 -155
  115. package/dist/wu-done-validation.js.map +1 -1
  116. package/dist/wu-done-validators.d.ts +1 -2
  117. package/dist/wu-done-validators.d.ts.map +1 -1
  118. package/dist/wu-done-validators.js +1 -3
  119. package/dist/wu-done-validators.js.map +1 -1
  120. package/dist/wu-done-worktree-services.d.ts +191 -0
  121. package/dist/wu-done-worktree-services.d.ts.map +1 -0
  122. package/dist/wu-done-worktree-services.js +273 -0
  123. package/dist/wu-done-worktree-services.js.map +1 -0
  124. package/dist/wu-done-worktree.d.ts +0 -19
  125. package/dist/wu-done-worktree.d.ts.map +1 -1
  126. package/dist/wu-done-worktree.js +165 -118
  127. package/dist/wu-done-worktree.js.map +1 -1
  128. package/dist/wu-git-constants.d.ts +4 -0
  129. package/dist/wu-git-constants.d.ts.map +1 -1
  130. package/dist/wu-git-constants.js +4 -0
  131. package/dist/wu-git-constants.js.map +1 -1
  132. package/dist/wu-helpers.d.ts +5 -1
  133. package/dist/wu-helpers.d.ts.map +1 -1
  134. package/dist/wu-helpers.js +5 -1
  135. package/dist/wu-helpers.js.map +1 -1
  136. package/dist/wu-lint.d.ts +24 -0
  137. package/dist/wu-lint.d.ts.map +1 -1
  138. package/dist/wu-lint.js +48 -1
  139. package/dist/wu-lint.js.map +1 -1
  140. package/dist/wu-paths-constants.d.ts +3 -3
  141. package/dist/wu-paths-constants.d.ts.map +1 -1
  142. package/dist/wu-paths-constants.js +3 -3
  143. package/dist/wu-paths-constants.js.map +1 -1
  144. package/dist/wu-recovery.d.ts +89 -0
  145. package/dist/wu-recovery.d.ts.map +1 -1
  146. package/dist/wu-recovery.js +118 -0
  147. package/dist/wu-recovery.js.map +1 -1
  148. package/dist/wu-schema.d.ts +6 -6
  149. package/dist/wu-spawn-context.d.ts +1 -1
  150. package/dist/wu-spawn-context.d.ts.map +1 -1
  151. package/dist/wu-spawn-context.js +8 -2
  152. package/dist/wu-spawn-context.js.map +1 -1
  153. package/dist/wu-spawn-helpers.js +2 -2
  154. package/dist/wu-spawn-helpers.js.map +1 -1
  155. package/dist/wu-state-schema.d.ts +12 -12
  156. package/dist/wu-state-schema.d.ts.map +1 -1
  157. package/dist/wu-state-schema.js +10 -10
  158. package/dist/wu-state-schema.js.map +1 -1
  159. package/dist/wu-state-store.d.ts +10 -4
  160. package/dist/wu-state-store.d.ts.map +1 -1
  161. package/dist/wu-state-store.js +309 -11
  162. package/dist/wu-state-store.js.map +1 -1
  163. package/dist/wu-transaction.d.ts +21 -0
  164. package/dist/wu-transaction.d.ts.map +1 -1
  165. package/dist/wu-transaction.js +17 -0
  166. package/dist/wu-transaction.js.map +1 -1
  167. package/package.json +15 -9
@@ -28,438 +28,19 @@
28
28
  * @see {@link packages/@lumenflow/cli/src/initiative-create.ts} - Initiative creation (WU-1439)
29
29
  */
30
30
  import { getGitForCwd, createGitForPath } from './git-adapter.js';
31
- import { existsSync, rmSync, mkdtempSync } from 'node:fs';
32
31
  import { execSync } from 'node:child_process';
33
- import { tmpdir } from 'node:os';
34
32
  import { join } from 'node:path';
35
33
  import pRetry from 'p-retry';
36
34
  import { BRANCHES, REMOTES, GIT_REFS, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, STDIO_MODES, } from './wu-constants.js';
37
35
  import { getConfig } from './lumenflow-config.js';
36
+ import { MAX_MERGE_RETRIES, MAX_PUSH_RETRIES, DEFAULT_PUSH_RETRY_CONFIG, resolvePushRetryConfig, LUMENFLOW_FORCE_ENV, LUMENFLOW_FORCE_REASON_ENV, LUMENFLOW_WU_TOOL_ENV, DEFAULT_LOG_PREFIX, RetryExhaustionError, isRetryExhaustionError, formatRetryExhaustionError, shouldSkipRemoteOperations, getTempBranchName, createMicroWorktreeDir, findWorktreeByBranch, cleanupOrphanedMicroWorktree, cleanupMicroWorktree, pushRefspecWithForce, pushRefspecWithRetry, } from './micro-worktree-shared.js';
38
37
  /**
39
38
  * Maximum retry attempts for ff-only merge when main moves
40
39
  *
41
40
  * This handles race conditions when multiple agents run wu:create or wu:edit
42
41
  * concurrently. Each retry fetches latest main and rebases.
43
42
  */
44
- export const MAX_MERGE_RETRIES = 3;
45
- /**
46
- * Maximum retry attempts for push when origin/main advances
47
- *
48
- * WU-1179: When push fails due to race condition (origin advanced while we
49
- * were working), rollback local main to origin/main and retry.
50
- * Each retry: fetch -> rebase temp branch -> re-merge -> push.
51
- *
52
- * @deprecated Use DEFAULT_PUSH_RETRY_CONFIG.retries instead (WU-1332)
53
- */
54
- export const MAX_PUSH_RETRIES = 3;
55
- /**
56
- * WU-1332: Default push retry configuration
57
- *
58
- * Provides sensible defaults for micro-worktree push operations.
59
- * Can be overridden via .lumenflow.config.yaml git.push_retry section.
60
- */
61
- export const DEFAULT_PUSH_RETRY_CONFIG = {
62
- enabled: true,
63
- retries: 3,
64
- min_delay_ms: 100,
65
- max_delay_ms: 1000,
66
- jitter: true,
67
- };
68
- /**
69
- * Resolve effective push retry config from defaults + global config + operation override.
70
- *
71
- * Priority (lowest to highest):
72
- * 1. DEFAULT_PUSH_RETRY_CONFIG
73
- * 2. Global config from `.lumenflow.config.yaml` (`git.push_retry`)
74
- * 3. Operation-specific override from caller
75
- */
76
- export function resolvePushRetryConfig(globalConfig, operationOverride) {
77
- return {
78
- ...DEFAULT_PUSH_RETRY_CONFIG,
79
- ...(globalConfig || {}),
80
- ...(operationOverride || {}),
81
- };
82
- }
83
- /**
84
- * Environment variable name for LUMENFLOW_FORCE bypass
85
- *
86
- * WU-1081: Exported for use in micro-worktree push operations.
87
- */
88
- export const LUMENFLOW_FORCE_ENV = 'LUMENFLOW_FORCE';
89
- /**
90
- * Environment variable name for LUMENFLOW_FORCE_REASON audit trail
91
- *
92
- * WU-1081: Exported for use in micro-worktree push operations.
93
- */
94
- export const LUMENFLOW_FORCE_REASON_ENV = 'LUMENFLOW_FORCE_REASON';
95
- /**
96
- * Environment variable name for LUMENFLOW_WU_TOOL
97
- *
98
- * WU-1365: Exported for use by CLI commands that use micro-worktree operations.
99
- * The pre-push hook checks this env var to allow micro-worktree pushes to main.
100
- * Valid values are: wu-create, wu-edit, wu-done, wu-delete, wu-claim, wu-block,
101
- * wu-unblock, initiative-create, initiative-edit, release, lumenflow-upgrade
102
- */
103
- export const LUMENFLOW_WU_TOOL_ENV = 'LUMENFLOW_WU_TOOL';
104
- /**
105
- * Default log prefix for micro-worktree operations
106
- *
107
- * Extracted to constant to satisfy sonarjs/no-duplicate-string rule.
108
- */
109
- export const DEFAULT_LOG_PREFIX = '[micro-wt]';
110
- /**
111
- * WU-1336: Pattern to detect retry exhaustion errors from error messages
112
- *
113
- * Matches error messages like "Push failed after N attempts"
114
- * Used for backwards compatibility with legacy error messages.
115
- */
116
- const RETRY_EXHAUSTION_PATTERN = /Push failed after \d+ attempts/;
117
- /**
118
- * WU-1336: Typed error for retry exhaustion in micro-worktree operations
119
- *
120
- * Thrown when push retries are exhausted due to race conditions with parallel agents.
121
- * CLI commands should use `isRetryExhaustionError` to detect this error type and
122
- * `formatRetryExhaustionError` to generate actionable user-facing messages.
123
- *
124
- * This centralizes retry exhaustion handling so CLI commands do not need to
125
- * duplicate detection logic or error formatting.
126
- *
127
- * @example
128
- * ```typescript
129
- * import { RetryExhaustionError, isRetryExhaustionError, formatRetryExhaustionError } from '@lumenflow/core';
130
- *
131
- * try {
132
- * await withMicroWorktree({ ... });
133
- * } catch (error) {
134
- * if (isRetryExhaustionError(error)) {
135
- * console.error(formatRetryExhaustionError(error, { command: 'pnpm initiative:add-wu ...' }));
136
- * } else {
137
- * throw error;
138
- * }
139
- * }
140
- * ```
141
- */
142
- export class RetryExhaustionError extends Error {
143
- /** Name of the error class (for instanceof checks across module boundaries) */
144
- name = 'RetryExhaustionError';
145
- /** Operation that was being performed (e.g., 'initiative-add-wu') */
146
- operation;
147
- /** Number of retry attempts that were exhausted */
148
- retries;
149
- constructor(operation, retries) {
150
- super(`Push failed after ${retries} attempts. ` +
151
- `Origin main may have significant traffic during ${operation}.`);
152
- this.operation = operation;
153
- this.retries = retries;
154
- // Maintain proper prototype chain for instanceof checks
155
- Object.setPrototypeOf(this, RetryExhaustionError.prototype);
156
- }
157
- }
158
- /**
159
- * WU-1336: Type guard to check if an error is a retry exhaustion error
160
- *
161
- * Detects both the typed `RetryExhaustionError` class and legacy error messages
162
- * that match the "Push failed after N attempts" pattern.
163
- *
164
- * @param {unknown} error - Error to check
165
- * @returns {boolean} True if this is a retry exhaustion error
166
- *
167
- * @example
168
- * ```typescript
169
- * if (isRetryExhaustionError(error)) {
170
- * // Handle retry exhaustion
171
- * }
172
- * ```
173
- */
174
- export function isRetryExhaustionError(error) {
175
- if (error instanceof RetryExhaustionError) {
176
- return true;
177
- }
178
- // Also detect legacy error messages for backwards compatibility
179
- if (error instanceof Error) {
180
- return RETRY_EXHAUSTION_PATTERN.test(error.message);
181
- }
182
- return false;
183
- }
184
- /**
185
- * WU-1336: Format retry exhaustion error with actionable next steps
186
- *
187
- * When push retries are exhausted, provides clear guidance on how to proceed.
188
- * CLI commands should use this instead of duplicating error formatting logic.
189
- *
190
- * @param {Error} error - The retry exhaustion error
191
- * @param {FormatRetryExhaustionOptions} options - Formatting options
192
- * @returns {string} Formatted error message with next steps
193
- *
194
- * @example
195
- * ```typescript
196
- * const message = formatRetryExhaustionError(error, {
197
- * command: 'pnpm initiative:add-wu --wu WU-123 --initiative INIT-001',
198
- * });
199
- * console.error(message);
200
- * ```
201
- */
202
- export function formatRetryExhaustionError(error, options) {
203
- const { command } = options;
204
- return (`${error.message}\n\n` +
205
- `Next steps:\n` +
206
- ` 1. Wait a few seconds and retry the operation:\n` +
207
- ` ${command}\n` +
208
- ` 2. If the issue persists, check if another agent is rapidly pushing changes\n` +
209
- ` 3. Consider increasing git.push_retry.retries in .lumenflow.config.yaml`);
210
- }
211
- /**
212
- * WU-1308: Check if remote operations should be skipped based on git.requireRemote config
213
- *
214
- * When git.requireRemote is false, micro-worktree operations skip:
215
- * - Fetching origin/main before starting
216
- * - Pushing to origin/main after completion
217
- *
218
- * This enables local-only development without a remote repository.
219
- *
220
- * @returns {boolean} True if remote operations should be skipped (requireRemote=false)
221
- *
222
- * @example
223
- * ```yaml
224
- * # .lumenflow.config.yaml
225
- * git:
226
- * requireRemote: false # Enable local-only mode
227
- * ```
228
- */
229
- export function shouldSkipRemoteOperations() {
230
- const config = getConfig();
231
- // Default is requireRemote=true, so only skip if explicitly set to false
232
- return config.git.requireRemote === false;
233
- }
234
- /**
235
- * Temp branch prefix for micro-worktree operations
236
- *
237
- * @param {string} operation - Operation name (e.g., 'wu-create', 'wu-edit')
238
- * @param {string} id - WU ID (e.g., 'wu-123')
239
- * @returns {string} Temp branch name (e.g., 'tmp/wu-create/wu-123')
240
- */
241
- export function getTempBranchName(operation, id) {
242
- return `${BRANCHES.TEMP_PREFIX}${operation}/${id.toLowerCase()}`;
243
- }
244
- /**
245
- * Create micro-worktree in /tmp directory
246
- *
247
- * @param {string} prefix - Directory prefix (e.g., 'wu-create-', 'wu-edit-')
248
- * @returns {string} Path to created micro-worktree directory
249
- */
250
- export function createMicroWorktreeDir(prefix) {
251
- return mkdtempSync(join(tmpdir(), prefix));
252
- }
253
- /**
254
- * Parse git worktree list output to find worktrees by branch
255
- *
256
- * WU-2237: Helper to parse porcelain format output from `git worktree list --porcelain`
257
- *
258
- * @param {string} worktreeListOutput - Output from git worktree list --porcelain
259
- * @param {string} branchName - Branch name to search for (e.g., 'tmp/wu-create/wu-123')
260
- * @returns {string|null} Worktree path if found, null otherwise
261
- */
262
- export function findWorktreeByBranch(worktreeListOutput, branchName) {
263
- const branchRef = `refs/heads/${branchName}`;
264
- const lines = worktreeListOutput.split('\n');
265
- let currentWorktreePath = null;
266
- for (const line of lines) {
267
- if (line.startsWith('worktree ')) {
268
- currentWorktreePath = line.substring('worktree '.length);
269
- }
270
- else if (line.startsWith('branch ') && line.substring('branch '.length) === branchRef) {
271
- return currentWorktreePath;
272
- }
273
- else if (line === '') {
274
- currentWorktreePath = null;
275
- }
276
- }
277
- return null;
278
- }
279
- /**
280
- * Clean up orphaned micro-worktree and temp branch from a previous interrupted operation
281
- *
282
- * WU-2237: Before creating a new micro-worktree, detect and clean any existing temp
283
- * branch/worktree for the same operation+WU ID. This handles scenarios where:
284
- * - A previous wu:create/wu:edit was interrupted (timeout/crash)
285
- * - The temp branch and /tmp worktree were left behind
286
- * - A subsequent operation would fail with 'branch already exists'
287
- *
288
- * This function is idempotent - safe to call even when no orphans exist.
289
- *
290
- * @param {string} operation - Operation name (e.g., 'wu-create', 'wu-edit')
291
- * @param {string} id - WU ID (e.g., 'WU-123')
292
- * @param {Object} gitAdapter - GitAdapter instance to use (for testability)
293
- * @param {string} logPrefix - Log prefix for console output
294
- * @returns {Promise<{cleanedWorktree: boolean, cleanedBranch: boolean}>} Cleanup status
295
- */
296
- export async function cleanupOrphanedMicroWorktree(operation, id, gitAdapter, logPrefix = DEFAULT_LOG_PREFIX) {
297
- const tempBranchName = getTempBranchName(operation, id);
298
- let cleanedWorktree = false;
299
- let cleanedBranch = false;
300
- // Step 1: Check git worktree list for any worktree on this temp branch
301
- try {
302
- const worktreeListOutput = await gitAdapter.worktreeList();
303
- const orphanWorktreePath = findWorktreeByBranch(worktreeListOutput, tempBranchName);
304
- if (orphanWorktreePath) {
305
- console.log(`${logPrefix} Found orphaned worktree for ${tempBranchName}: ${orphanWorktreePath}`);
306
- try {
307
- await gitAdapter.worktreeRemove(orphanWorktreePath, { force: true });
308
- console.log(`${logPrefix} ✅ Removed orphaned worktree: ${orphanWorktreePath}`);
309
- cleanedWorktree = true;
310
- }
311
- catch (err) {
312
- const errMsg = err instanceof Error ? err.message : String(err);
313
- console.warn(`${logPrefix} ⚠️ Could not remove orphaned worktree: ${errMsg}`);
314
- // Try filesystem cleanup as fallback
315
- tryFilesystemCleanup(orphanWorktreePath);
316
- cleanedWorktree = true;
317
- }
318
- }
319
- }
320
- catch (err) {
321
- const errMsg = err instanceof Error ? err.message : String(err);
322
- console.warn(`${logPrefix} ⚠️ Could not check worktree list: ${errMsg}`);
323
- }
324
- // Step 2: Check if the temp branch exists and delete it
325
- try {
326
- const branchExists = await gitAdapter.branchExists(tempBranchName);
327
- if (branchExists) {
328
- console.log(`${logPrefix} Found orphaned temp branch: ${tempBranchName}`);
329
- await gitAdapter.deleteBranch(tempBranchName, { force: true });
330
- console.log(`${logPrefix} ✅ Deleted orphaned temp branch: ${tempBranchName}`);
331
- cleanedBranch = true;
332
- }
333
- }
334
- catch (err) {
335
- const errMsg = err instanceof Error ? err.message : String(err);
336
- console.warn(`${logPrefix} ⚠️ Could not delete orphaned branch: ${errMsg}`);
337
- }
338
- return { cleanedWorktree, cleanedBranch };
339
- }
340
- /**
341
- * Try to remove a worktree path via filesystem as fallback
342
- *
343
- * WU-2237: Extracted helper to reduce cognitive complexity.
344
- *
345
- * @param {string} worktreePath - Path to remove
346
- */
347
- function tryFilesystemCleanup(worktreePath) {
348
- try {
349
- if (existsSync(worktreePath)) {
350
- rmSync(worktreePath, { recursive: true, force: true });
351
- }
352
- }
353
- catch {
354
- // Ignore filesystem cleanup errors
355
- }
356
- }
357
- /**
358
- * Remove a worktree using git, with filesystem fallback
359
- *
360
- * WU-2237: Extracted helper to reduce cognitive complexity.
361
- *
362
- * @param {Object} gitAdapter - Git adapter instance
363
- * @param {string} worktreePath - Path to worktree
364
- * @param {string} logPrefix - Log prefix
365
- * @param {string} [contextLabel] - Optional label for logging (e.g., 'registered')
366
- */
367
- async function removeWorktreeSafe(gitAdapter, worktreePath, logPrefix, contextLabel = '') {
368
- const label = contextLabel ? ` ${contextLabel}` : '';
369
- try {
370
- await gitAdapter.worktreeRemove(worktreePath, { force: true });
371
- if (contextLabel) {
372
- console.log(`${logPrefix} ✅ Removed${label} worktree: ${worktreePath}`);
373
- }
374
- }
375
- catch (err) {
376
- const errMsg = err instanceof Error ? err.message : String(err);
377
- console.warn(`${logPrefix} ⚠️ Could not remove${label} worktree: ${errMsg}`);
378
- tryFilesystemCleanup(worktreePath);
379
- }
380
- }
381
- /**
382
- * Cleanup micro-worktree and temp branch
383
- *
384
- * Runs even on failure to prevent orphaned resources.
385
- * Safe to call multiple times (idempotent).
386
- *
387
- * WU-2237: Enhanced to also check git worktree list for registered worktrees
388
- * on the temp branch, in case the worktree path differs from what was expected.
389
- *
390
- * @param {string} worktreePath - Path to micro-worktree
391
- * @param {string} branchName - Temp branch name
392
- * @param {string} logPrefix - Log prefix for console output
393
- */
394
- export async function cleanupMicroWorktree(worktreePath, branchName, logPrefix = DEFAULT_LOG_PREFIX) {
395
- console.log(`${logPrefix} Cleaning up micro-worktree...`);
396
- const mainGit = getGitForCwd();
397
- // Remove the known worktree path first (must be done before deleting branch)
398
- if (existsSync(worktreePath)) {
399
- await removeWorktreeSafe(mainGit, worktreePath, logPrefix);
400
- }
401
- // WU-2237: Also check git worktree list for any registered worktrees on this branch
402
- await cleanupRegisteredWorktreeForBranch(mainGit, branchName, worktreePath, logPrefix);
403
- // Delete temp branch
404
- await deleteBranchSafe(mainGit, branchName, logPrefix);
405
- console.log(`${logPrefix} ✅ Cleanup complete`);
406
- }
407
- /**
408
- * Clean up any registered worktree for a branch that differs from the expected path
409
- *
410
- * WU-2237: Extracted helper to reduce cognitive complexity.
411
- *
412
- * @param {Object} gitAdapter - Git adapter instance
413
- * @param {string} branchName - Branch name to search for
414
- * @param {string} expectedPath - Expected worktree path (skip if matches)
415
- * @param {string} logPrefix - Log prefix
416
- */
417
- async function cleanupRegisteredWorktreeForBranch(gitAdapter, branchName, expectedPath, logPrefix) {
418
- try {
419
- const worktreeListOutput = await gitAdapter.worktreeList();
420
- const registeredPath = findWorktreeByBranch(worktreeListOutput, branchName);
421
- if (registeredPath && registeredPath !== expectedPath) {
422
- console.log(`${logPrefix} Found additional registered worktree: ${registeredPath}`);
423
- await removeWorktreeSafe(gitAdapter, registeredPath, logPrefix, 'registered');
424
- }
425
- }
426
- catch (err) {
427
- const errMsg = err instanceof Error ? err.message : String(err);
428
- console.warn(`${logPrefix} ⚠️ Could not check worktree list: ${errMsg}`);
429
- }
430
- }
431
- /**
432
- * Delete a branch safely, ignoring errors
433
- *
434
- * WU-2237: Extracted helper to reduce cognitive complexity.
435
- *
436
- * @param {Object} gitAdapter - Git adapter instance
437
- * @param {string} branchName - Branch to delete
438
- * @param {string} logPrefix - Log prefix
439
- */
440
- async function deleteBranchSafe(gitAdapter, branchName, logPrefix) {
441
- try {
442
- const branchExists = await gitAdapter.branchExists(branchName);
443
- if (branchExists) {
444
- await gitAdapter.deleteBranch(branchName, { force: true });
445
- }
446
- }
447
- catch (err) {
448
- const errMsg = err instanceof Error ? err.message : String(err);
449
- console.warn(`${logPrefix} ⚠️ Could not delete branch: ${errMsg}`);
450
- }
451
- }
452
- /**
453
- * Stage changes including deletions in micro-worktree
454
- *
455
- * WU-1813: Uses addWithDeletions to properly stage tracked file deletions.
456
- * This replaces the previous pattern of using gitWorktree.add(files) which
457
- * could miss deletions when files were removed.
458
- *
459
- * @param {Object} gitWorktree - GitAdapter instance for the worktree
460
- * @param {string[]|undefined} files - Files to stage (undefined/empty = stage all)
461
- * @returns {Promise<void>}
462
- */
43
+ export { MAX_MERGE_RETRIES, MAX_PUSH_RETRIES, DEFAULT_PUSH_RETRY_CONFIG, resolvePushRetryConfig, LUMENFLOW_FORCE_ENV, LUMENFLOW_FORCE_REASON_ENV, LUMENFLOW_WU_TOOL_ENV, DEFAULT_LOG_PREFIX, RetryExhaustionError, isRetryExhaustionError, formatRetryExhaustionError, shouldSkipRemoteOperations, getTempBranchName, createMicroWorktreeDir, findWorktreeByBranch, cleanupOrphanedMicroWorktree, cleanupMicroWorktree, pushRefspecWithForce, pushRefspecWithRetry, };
463
44
  export async function stageChangesWithDeletions(gitWorktree, files) {
464
45
  // Normalise undefined/null to empty array for addWithDeletions
465
46
  const filesToStage = files || [];
@@ -777,125 +358,6 @@ export async function pushWithRetryConfig(mainGit, worktreeGit, remote, branch,
777
358
  * @returns {Promise<void>}
778
359
  * @throws {Error} If push fails (env vars still restored)
779
360
  */
780
- export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteRef, reason) {
781
- // Save original env values
782
- const originalForce = process.env[LUMENFLOW_FORCE_ENV];
783
- const originalReason = process.env[LUMENFLOW_FORCE_REASON_ENV];
784
- try {
785
- // Set LUMENFLOW_FORCE for the push
786
- process.env[LUMENFLOW_FORCE_ENV] = '1';
787
- process.env[LUMENFLOW_FORCE_REASON_ENV] = reason;
788
- // Perform the push
789
- await gitAdapter.pushRefspec(remote, localRef, remoteRef);
790
- }
791
- finally {
792
- // Restore original env values
793
- if (originalForce === undefined) {
794
- Reflect.deleteProperty(process.env, LUMENFLOW_FORCE_ENV);
795
- }
796
- else {
797
- process.env[LUMENFLOW_FORCE_ENV] = originalForce;
798
- }
799
- if (originalReason === undefined) {
800
- Reflect.deleteProperty(process.env, LUMENFLOW_FORCE_REASON_ENV);
801
- }
802
- else {
803
- process.env[LUMENFLOW_FORCE_REASON_ENV] = originalReason;
804
- }
805
- }
806
- }
807
- /**
808
- * WU-1337: Push using refspec with LUMENFLOW_FORCE and retry logic
809
- *
810
- * Enhanced version of pushRefspecWithForce that adds retry with rebase
811
- * on non-fast-forward errors. Uses p-retry for exponential backoff and
812
- * respects git.push_retry configuration.
813
- *
814
- * On each retry:
815
- * 1. Fetch origin/main to get latest state
816
- * 2. Rebase the temp branch onto the updated main
817
- * 3. Retry the push with LUMENFLOW_FORCE
818
- *
819
- * This is used by pushOnly mode in withMicroWorktree to handle race conditions
820
- * when multiple agents are pushing to origin/main concurrently.
821
- *
822
- * @param {GitAdapter} gitWorktree - GitAdapter instance for the worktree (for rebase)
823
- * @param {GitAdapter} mainGit - GitAdapter instance for main checkout (for fetch)
824
- * @param {string} remote - Remote name (e.g., 'origin')
825
- * @param {string} localRef - Local ref to push (e.g., 'tmp/wu-claim/wu-123')
826
- * @param {string} remoteRef - Remote ref to update (e.g., 'main')
827
- * @param {string} reason - Audit reason for the LUMENFLOW_FORCE bypass
828
- * @param {string} logPrefix - Log prefix for console output
829
- * @param {PushRetryConfig} config - Push retry configuration
830
- * @returns {Promise<void>}
831
- * @throws {RetryExhaustionError} If push fails after all retries
832
- */
833
- export async function pushRefspecWithRetry(gitWorktree, mainGit, remote, localRef, remoteRef, reason, logPrefix = DEFAULT_LOG_PREFIX, config = DEFAULT_PUSH_RETRY_CONFIG) {
834
- // If retry is disabled, just try once and throw on failure
835
- if (!config.enabled) {
836
- console.log(`${logPrefix} Pushing to ${remote}/${remoteRef} (push-only, retry disabled)...`);
837
- await pushRefspecWithForce(gitWorktree, remote, localRef, remoteRef, reason);
838
- console.log(`${logPrefix} ✅ Pushed to ${remote}/${remoteRef}`);
839
- return;
840
- }
841
- let attemptNumber = 0;
842
- await pRetry(async () => {
843
- attemptNumber++;
844
- console.log(`${logPrefix} Pushing to ${remote}/${remoteRef} (push-only, attempt ${attemptNumber}/${config.retries})...`);
845
- try {
846
- await pushRefspecWithForce(gitWorktree, remote, localRef, remoteRef, reason);
847
- console.log(`${logPrefix} ✅ Pushed to ${remote}/${remoteRef}`);
848
- }
849
- catch (pushErr) {
850
- console.log(`${logPrefix} ⚠️ Push failed (origin moved). Fetching and rebasing before retry...`);
851
- // Fetch latest origin/main
852
- console.log(`${logPrefix} Fetching ${remote}/${remoteRef}...`);
853
- await mainGit.fetch(remote, remoteRef);
854
- // Rebase temp branch onto updated remote-tracking ref.
855
- // In pushOnly mode, local `main` is intentionally not updated (WU-1435),
856
- // so rebasing onto `main` will not make progress after a successful push.
857
- const remoteTrackingRef = `${remote}/${remoteRef}`;
858
- console.log(`${logPrefix} Rebasing temp branch onto ${remoteTrackingRef}...`);
859
- await gitWorktree.rebase(remoteTrackingRef);
860
- // Re-throw to trigger p-retry
861
- throw pushErr;
862
- }
863
- }, {
864
- retries: config.retries - 1, // p-retry counts retries after first attempt
865
- minTimeout: config.min_delay_ms,
866
- maxTimeout: config.max_delay_ms,
867
- randomize: config.jitter,
868
- onFailedAttempt: () => {
869
- // Logging is handled in the try/catch above
870
- },
871
- }).catch(() => {
872
- // p-retry exhausted all retries, throw typed error
873
- throw new RetryExhaustionError('push-only', config.retries);
874
- });
875
- }
876
- /**
877
- * Execute an operation in a micro-worktree with full isolation
878
- *
879
- * This is the main entry point for micro-worktree operations.
880
- * Handles the full lifecycle: create temp branch, create worktree,
881
- * execute operation, merge, push, and cleanup.
882
- *
883
- * WU-1435: Added pushOnly option to keep local main pristine.
884
- * WU-2237: Added pre-creation cleanup of orphaned temp branches/worktrees.
885
- * WU-1337: Push-only path now uses retry with rebase.
886
- *
887
- * @param {Object} options - Options for the operation
888
- * @param {string} options.operation - Operation name (e.g., 'wu-create', 'wu-edit')
889
- * @param {string} options.id - WU ID (e.g., 'WU-123')
890
- * @param {string} options.logPrefix - Log prefix for console output
891
- * @param {boolean} [options.pushOnly=false] - Skip local main merge, push directly to origin/main
892
- * @param {Object} [options.pushRetryOverride] - Optional push retry overrides for this operation
893
- * @param {Function} options.execute - Async function to execute in micro-worktree
894
- * Receives: { worktreePath: string, gitWorktree: GitAdapter }
895
- * Should return: { commitMessage: string, files: string[] }
896
- * @returns {Promise<Object>} Result with ref property for worktree creation
897
- * @throws {Error} If any step fails (cleanup still runs)
898
- */
899
361
  export async function withMicroWorktree(options) {
900
362
  const { operation, id, logPrefix = `[${operation}]`, execute, pushOnly = false, pushRetryOverride, } = options;
901
363
  const mainGit = getGitForCwd();
@@ -904,31 +366,37 @@ export async function withMicroWorktree(options) {
904
366
  // WU-2237: Clean up any orphaned temp branch/worktree from previous interrupted operations
905
367
  // This makes the operation idempotent - a retry after crash/timeout will succeed
906
368
  await cleanupOrphanedMicroWorktree(operation, id, mainGit, logPrefix);
907
- // WU-1179: Fetch origin/main before starting to minimize race condition window
908
- // This ensures we start from the latest origin state, reducing push failures
909
- // WU-1308: Skip when git.requireRemote=false (local-only mode)
910
- if (!pushOnly && !skipRemote) {
369
+ // WU-1179/WU-1672: Fetch origin/main before starting to minimize race condition window.
370
+ // In pushOnly mode, fetch updates origin/main tracking ref without touching local main.
371
+ // WU-1308: Skip when git.requireRemote=false (local-only mode).
372
+ if (!skipRemote) {
911
373
  console.log(`${logPrefix} Fetching ${REMOTES.ORIGIN}/${BRANCHES.MAIN} before starting...`);
912
374
  await mainGit.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
913
- // Update local main to match origin/main
914
- await mainGit.merge(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`, { ffOnly: true });
915
- console.log(`${logPrefix} ✅ Local main synced with ${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
375
+ if (pushOnly) {
376
+ console.log(`${logPrefix} ✅ Push-only mode will base from ${REMOTES.ORIGIN}/${BRANCHES.MAIN}; local main unchanged (WU-1672)`);
377
+ }
378
+ else {
379
+ // Update local main to match origin/main for standard mode.
380
+ await mainGit.merge(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`, { ffOnly: true });
381
+ console.log(`${logPrefix} ✅ Local main synced with ${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
382
+ }
916
383
  }
917
384
  else if (skipRemote) {
918
385
  console.log(`${logPrefix} Local-only mode (git.requireRemote=false): skipping origin sync`);
919
386
  }
920
387
  const tempBranchName = getTempBranchName(operation, id);
388
+ const baseRef = pushOnly && !skipRemote ? `${REMOTES.ORIGIN}/${BRANCHES.MAIN}` : BRANCHES.MAIN;
921
389
  const microWorktreePath = createMicroWorktreeDir(`${operation}-`);
922
390
  console.log(`${logPrefix} Using micro-worktree isolation (WU-1262)`);
923
391
  console.log(`${logPrefix} Temp branch: ${tempBranchName}`);
924
392
  console.log(`${logPrefix} Micro-worktree: ${microWorktreePath}`);
925
393
  if (pushOnly) {
926
- console.log(`${logPrefix} Push-only mode: local main will not be modified (WU-1435)`);
394
+ console.log(`${logPrefix} Push-only mode: local main will not be modified (WU-1435/WU-1672)`);
927
395
  }
928
396
  try {
929
397
  // Step 1: Create temp branch without switching
930
398
  console.log(`${logPrefix} Creating temp branch...`);
931
- await mainGit.createBranchNoCheckout(tempBranchName, BRANCHES.MAIN);
399
+ await mainGit.createBranchNoCheckout(tempBranchName, baseRef);
932
400
  // Step 2: Create micro-worktree pointing to temp branch
933
401
  console.log(`${logPrefix} Creating micro-worktree...`);
934
402
  await mainGit.worktreeAddExisting(microWorktreePath, tempBranchName);