@lumenflow/core 2.4.0 → 2.5.1
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.
- package/dist/arg-parser.d.ts +7 -0
- package/dist/arg-parser.js +11 -1
- package/dist/lane-checker.d.ts +29 -2
- package/dist/lane-checker.js +176 -20
- package/dist/lane-lock.d.ts +8 -0
- package/dist/lane-lock.js +21 -0
- package/dist/lumenflow-config-schema.d.ts +220 -0
- package/dist/lumenflow-config-schema.js +180 -0
- package/dist/lumenflow-config.d.ts +3 -1
- package/dist/lumenflow-config.js +9 -0
- package/dist/micro-worktree.d.ts +169 -4
- package/dist/micro-worktree.js +333 -26
- package/dist/wu-constants.d.ts +8 -0
- package/dist/wu-constants.js +8 -0
- package/dist/wu-done-concurrent-merge.d.ts +13 -0
- package/dist/wu-done-concurrent-merge.js +41 -1
- package/dist/wu-done-metadata.js +3 -3
- package/dist/wu-paths.d.ts +20 -0
- package/dist/wu-paths.js +10 -0
- package/dist/wu-preflight-validators.d.ts +30 -0
- package/dist/wu-preflight-validators.js +6 -2
- package/dist/wu-transaction-collectors.d.ts +13 -0
- package/dist/wu-transaction-collectors.js +20 -32
- package/package.json +3 -2
package/dist/micro-worktree.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* @see {@link packages/@lumenflow/cli/src/initiative-create.ts} - Initiative creation (WU-1439)
|
|
29
29
|
*/
|
|
30
30
|
import type { GitAdapter } from './git-adapter.js';
|
|
31
|
+
import type { PushRetryConfig } from './lumenflow-config-schema.js';
|
|
31
32
|
/**
|
|
32
33
|
* Context passed to the execute function in withMicroWorktree
|
|
33
34
|
*/
|
|
@@ -81,8 +82,17 @@ export declare const MAX_MERGE_RETRIES = 3;
|
|
|
81
82
|
* WU-1179: When push fails due to race condition (origin advanced while we
|
|
82
83
|
* were working), rollback local main to origin/main and retry.
|
|
83
84
|
* Each retry: fetch -> rebase temp branch -> re-merge -> push.
|
|
85
|
+
*
|
|
86
|
+
* @deprecated Use DEFAULT_PUSH_RETRY_CONFIG.retries instead (WU-1332)
|
|
84
87
|
*/
|
|
85
88
|
export declare const MAX_PUSH_RETRIES = 3;
|
|
89
|
+
/**
|
|
90
|
+
* WU-1332: Default push retry configuration
|
|
91
|
+
*
|
|
92
|
+
* Provides sensible defaults for micro-worktree push operations.
|
|
93
|
+
* Can be overridden via .lumenflow.config.yaml git.push_retry section.
|
|
94
|
+
*/
|
|
95
|
+
export declare const DEFAULT_PUSH_RETRY_CONFIG: PushRetryConfig;
|
|
86
96
|
/**
|
|
87
97
|
* Environment variable name for LUMENFLOW_FORCE bypass
|
|
88
98
|
*
|
|
@@ -101,6 +111,102 @@ export declare const LUMENFLOW_FORCE_REASON_ENV = "LUMENFLOW_FORCE_REASON";
|
|
|
101
111
|
* Extracted to constant to satisfy sonarjs/no-duplicate-string rule.
|
|
102
112
|
*/
|
|
103
113
|
export declare const DEFAULT_LOG_PREFIX = "[micro-wt]";
|
|
114
|
+
/**
|
|
115
|
+
* WU-1336: Typed error for retry exhaustion in micro-worktree operations
|
|
116
|
+
*
|
|
117
|
+
* Thrown when push retries are exhausted due to race conditions with parallel agents.
|
|
118
|
+
* CLI commands should use `isRetryExhaustionError` to detect this error type and
|
|
119
|
+
* `formatRetryExhaustionError` to generate actionable user-facing messages.
|
|
120
|
+
*
|
|
121
|
+
* This centralizes retry exhaustion handling so CLI commands do not need to
|
|
122
|
+
* duplicate detection logic or error formatting.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* import { RetryExhaustionError, isRetryExhaustionError, formatRetryExhaustionError } from '@lumenflow/core';
|
|
127
|
+
*
|
|
128
|
+
* try {
|
|
129
|
+
* await withMicroWorktree({ ... });
|
|
130
|
+
* } catch (error) {
|
|
131
|
+
* if (isRetryExhaustionError(error)) {
|
|
132
|
+
* console.error(formatRetryExhaustionError(error, { command: 'pnpm initiative:add-wu ...' }));
|
|
133
|
+
* } else {
|
|
134
|
+
* throw error;
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export declare class RetryExhaustionError extends Error {
|
|
140
|
+
/** Name of the error class (for instanceof checks across module boundaries) */
|
|
141
|
+
readonly name = "RetryExhaustionError";
|
|
142
|
+
/** Operation that was being performed (e.g., 'initiative-add-wu') */
|
|
143
|
+
readonly operation: string;
|
|
144
|
+
/** Number of retry attempts that were exhausted */
|
|
145
|
+
readonly retries: number;
|
|
146
|
+
constructor(operation: string, retries: number);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* WU-1336: Options for formatting retry exhaustion error messages
|
|
150
|
+
*/
|
|
151
|
+
export interface FormatRetryExhaustionOptions {
|
|
152
|
+
/** Command to suggest for retrying (e.g., 'pnpm initiative:add-wu --wu WU-123 --initiative INIT-001') */
|
|
153
|
+
command: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* WU-1336: Type guard to check if an error is a retry exhaustion error
|
|
157
|
+
*
|
|
158
|
+
* Detects both the typed `RetryExhaustionError` class and legacy error messages
|
|
159
|
+
* that match the "Push failed after N attempts" pattern.
|
|
160
|
+
*
|
|
161
|
+
* @param {unknown} error - Error to check
|
|
162
|
+
* @returns {boolean} True if this is a retry exhaustion error
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* if (isRetryExhaustionError(error)) {
|
|
167
|
+
* // Handle retry exhaustion
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export declare function isRetryExhaustionError(error: unknown): error is Error;
|
|
172
|
+
/**
|
|
173
|
+
* WU-1336: Format retry exhaustion error with actionable next steps
|
|
174
|
+
*
|
|
175
|
+
* When push retries are exhausted, provides clear guidance on how to proceed.
|
|
176
|
+
* CLI commands should use this instead of duplicating error formatting logic.
|
|
177
|
+
*
|
|
178
|
+
* @param {Error} error - The retry exhaustion error
|
|
179
|
+
* @param {FormatRetryExhaustionOptions} options - Formatting options
|
|
180
|
+
* @returns {string} Formatted error message with next steps
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const message = formatRetryExhaustionError(error, {
|
|
185
|
+
* command: 'pnpm initiative:add-wu --wu WU-123 --initiative INIT-001',
|
|
186
|
+
* });
|
|
187
|
+
* console.error(message);
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export declare function formatRetryExhaustionError(error: Error, options: FormatRetryExhaustionOptions): string;
|
|
191
|
+
/**
|
|
192
|
+
* WU-1308: Check if remote operations should be skipped based on git.requireRemote config
|
|
193
|
+
*
|
|
194
|
+
* When git.requireRemote is false, micro-worktree operations skip:
|
|
195
|
+
* - Fetching origin/main before starting
|
|
196
|
+
* - Pushing to origin/main after completion
|
|
197
|
+
*
|
|
198
|
+
* This enables local-only development without a remote repository.
|
|
199
|
+
*
|
|
200
|
+
* @returns {boolean} True if remote operations should be skipped (requireRemote=false)
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```yaml
|
|
204
|
+
* # .lumenflow.config.yaml
|
|
205
|
+
* git:
|
|
206
|
+
* requireRemote: false # Enable local-only mode
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export declare function shouldSkipRemoteOperations(): boolean;
|
|
104
210
|
/**
|
|
105
211
|
* Temp branch prefix for micro-worktree operations
|
|
106
212
|
*
|
|
@@ -200,11 +306,16 @@ export declare function mergeWithRetry(tempBranchName: string, microWorktreePath
|
|
|
200
306
|
* Push to origin/main with retry logic for race conditions
|
|
201
307
|
*
|
|
202
308
|
* WU-1179: When push fails because origin/main advanced (race condition with
|
|
203
|
-
* parallel agents), this function
|
|
204
|
-
*
|
|
309
|
+
* parallel agents), this function retries with fetch and rebase.
|
|
310
|
+
*
|
|
311
|
+
* WU-1348: The retry logic no longer resets the main checkout. Instead, it:
|
|
312
|
+
* 1. Fetches origin/main to get latest remote state
|
|
313
|
+
* 2. Rebases the temp branch onto origin/main (in the micro-worktree)
|
|
314
|
+
* 3. Re-merges the rebased temp branch to local main (ff-only)
|
|
315
|
+
* 4. Retries the push
|
|
205
316
|
*
|
|
206
|
-
* This
|
|
207
|
-
*
|
|
317
|
+
* This preserves micro-worktree isolation - the main checkout files are never
|
|
318
|
+
* hard-reset, preventing file flash and preserving any uncommitted work.
|
|
208
319
|
*
|
|
209
320
|
* @param {Object} mainGit - GitAdapter instance for main checkout
|
|
210
321
|
* @param {Object} worktreeGit - GitAdapter instance for micro-worktree
|
|
@@ -215,6 +326,32 @@ export declare function mergeWithRetry(tempBranchName: string, microWorktreePath
|
|
|
215
326
|
* @throws {Error} If push fails after all retries
|
|
216
327
|
*/
|
|
217
328
|
export declare function pushWithRetry(mainGit: GitAdapter, worktreeGit: GitAdapter, remote: string, branch: string, tempBranchName: string, logPrefix?: string): Promise<void>;
|
|
329
|
+
/**
|
|
330
|
+
* WU-1332: Push to origin with configurable retry using p-retry
|
|
331
|
+
*
|
|
332
|
+
* Enhanced version of pushWithRetry that uses p-retry for exponential backoff
|
|
333
|
+
* and supports configuration via PushRetryConfig. When push fails due to
|
|
334
|
+
* non-fast-forward (origin moved), automatically rebases and retries.
|
|
335
|
+
*
|
|
336
|
+
* WU-1348: The retry logic no longer resets the main checkout. Instead, it:
|
|
337
|
+
* 1. Fetches origin/main to get latest remote state
|
|
338
|
+
* 2. Rebases the temp branch onto origin/main (in the micro-worktree)
|
|
339
|
+
* 3. Re-merges the rebased temp branch to local main (ff-only)
|
|
340
|
+
* 4. Retries the push
|
|
341
|
+
*
|
|
342
|
+
* This preserves micro-worktree isolation - the main checkout files are never
|
|
343
|
+
* hard-reset, preventing file flash and preserving any uncommitted work.
|
|
344
|
+
*
|
|
345
|
+
* @param {Object} mainGit - GitAdapter instance for main checkout
|
|
346
|
+
* @param {Object} worktreeGit - GitAdapter instance for micro-worktree
|
|
347
|
+
* @param {string} remote - Remote name (e.g., 'origin')
|
|
348
|
+
* @param {string} branch - Branch name (e.g., 'main')
|
|
349
|
+
* @param {string} tempBranchName - Temp branch that was merged (for rebase)
|
|
350
|
+
* @param {string} logPrefix - Log prefix for console output
|
|
351
|
+
* @param {PushRetryConfig} config - Push retry configuration
|
|
352
|
+
* @throws {Error} If push fails after all retries or if retry is disabled
|
|
353
|
+
*/
|
|
354
|
+
export declare function pushWithRetryConfig(mainGit: GitAdapter, worktreeGit: GitAdapter, remote: string, branch: string, tempBranchName: string, logPrefix?: string, config?: PushRetryConfig): Promise<void>;
|
|
218
355
|
/**
|
|
219
356
|
* Push using refspec with LUMENFLOW_FORCE to bypass pre-push hooks
|
|
220
357
|
*
|
|
@@ -234,6 +371,33 @@ export declare function pushWithRetry(mainGit: GitAdapter, worktreeGit: GitAdapt
|
|
|
234
371
|
* @throws {Error} If push fails (env vars still restored)
|
|
235
372
|
*/
|
|
236
373
|
export declare function pushRefspecWithForce(gitAdapter: GitAdapter, remote: string, localRef: string, remoteRef: string, reason: string): Promise<void>;
|
|
374
|
+
/**
|
|
375
|
+
* WU-1337: Push using refspec with LUMENFLOW_FORCE and retry logic
|
|
376
|
+
*
|
|
377
|
+
* Enhanced version of pushRefspecWithForce that adds retry with rebase
|
|
378
|
+
* on non-fast-forward errors. Uses p-retry for exponential backoff and
|
|
379
|
+
* respects git.push_retry configuration.
|
|
380
|
+
*
|
|
381
|
+
* On each retry:
|
|
382
|
+
* 1. Fetch origin/main to get latest state
|
|
383
|
+
* 2. Rebase the temp branch onto the updated main
|
|
384
|
+
* 3. Retry the push with LUMENFLOW_FORCE
|
|
385
|
+
*
|
|
386
|
+
* This is used by pushOnly mode in withMicroWorktree to handle race conditions
|
|
387
|
+
* when multiple agents are pushing to origin/main concurrently.
|
|
388
|
+
*
|
|
389
|
+
* @param {GitAdapter} gitWorktree - GitAdapter instance for the worktree (for rebase)
|
|
390
|
+
* @param {GitAdapter} mainGit - GitAdapter instance for main checkout (for fetch)
|
|
391
|
+
* @param {string} remote - Remote name (e.g., 'origin')
|
|
392
|
+
* @param {string} localRef - Local ref to push (e.g., 'tmp/wu-claim/wu-123')
|
|
393
|
+
* @param {string} remoteRef - Remote ref to update (e.g., 'main')
|
|
394
|
+
* @param {string} reason - Audit reason for the LUMENFLOW_FORCE bypass
|
|
395
|
+
* @param {string} logPrefix - Log prefix for console output
|
|
396
|
+
* @param {PushRetryConfig} config - Push retry configuration
|
|
397
|
+
* @returns {Promise<void>}
|
|
398
|
+
* @throws {RetryExhaustionError} If push fails after all retries
|
|
399
|
+
*/
|
|
400
|
+
export declare function pushRefspecWithRetry(gitWorktree: GitAdapter, mainGit: GitAdapter, remote: string, localRef: string, remoteRef: string, reason: string, logPrefix?: string, config?: PushRetryConfig): Promise<void>;
|
|
237
401
|
/**
|
|
238
402
|
* Execute an operation in a micro-worktree with full isolation
|
|
239
403
|
*
|
|
@@ -243,6 +407,7 @@ export declare function pushRefspecWithForce(gitAdapter: GitAdapter, remote: str
|
|
|
243
407
|
*
|
|
244
408
|
* WU-1435: Added pushOnly option to keep local main pristine.
|
|
245
409
|
* WU-2237: Added pre-creation cleanup of orphaned temp branches/worktrees.
|
|
410
|
+
* WU-1337: Push-only path now uses retry with rebase.
|
|
246
411
|
*
|
|
247
412
|
* @param {Object} options - Options for the operation
|
|
248
413
|
* @param {string} options.operation - Operation name (e.g., 'wu-create', 'wu-edit')
|
package/dist/micro-worktree.js
CHANGED
|
@@ -32,7 +32,9 @@ import { existsSync, rmSync, mkdtempSync } from 'node:fs';
|
|
|
32
32
|
import { execSync } from 'node:child_process';
|
|
33
33
|
import { tmpdir } from 'node:os';
|
|
34
34
|
import { join } from 'node:path';
|
|
35
|
+
import pRetry from 'p-retry';
|
|
35
36
|
import { BRANCHES, REMOTES, GIT_REFS, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, STDIO_MODES, } from './wu-constants.js';
|
|
37
|
+
import { getConfig } from './lumenflow-config.js';
|
|
36
38
|
/**
|
|
37
39
|
* Maximum retry attempts for ff-only merge when main moves
|
|
38
40
|
*
|
|
@@ -46,8 +48,23 @@ export const MAX_MERGE_RETRIES = 3;
|
|
|
46
48
|
* WU-1179: When push fails due to race condition (origin advanced while we
|
|
47
49
|
* were working), rollback local main to origin/main and retry.
|
|
48
50
|
* Each retry: fetch -> rebase temp branch -> re-merge -> push.
|
|
51
|
+
*
|
|
52
|
+
* @deprecated Use DEFAULT_PUSH_RETRY_CONFIG.retries instead (WU-1332)
|
|
49
53
|
*/
|
|
50
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
|
+
};
|
|
51
68
|
/**
|
|
52
69
|
* Environment variable name for LUMENFLOW_FORCE bypass
|
|
53
70
|
*
|
|
@@ -66,6 +83,130 @@ export const LUMENFLOW_FORCE_REASON_ENV = 'LUMENFLOW_FORCE_REASON';
|
|
|
66
83
|
* Extracted to constant to satisfy sonarjs/no-duplicate-string rule.
|
|
67
84
|
*/
|
|
68
85
|
export const DEFAULT_LOG_PREFIX = '[micro-wt]';
|
|
86
|
+
/**
|
|
87
|
+
* WU-1336: Pattern to detect retry exhaustion errors from error messages
|
|
88
|
+
*
|
|
89
|
+
* Matches error messages like "Push failed after N attempts"
|
|
90
|
+
* Used for backwards compatibility with legacy error messages.
|
|
91
|
+
*/
|
|
92
|
+
const RETRY_EXHAUSTION_PATTERN = /Push failed after \d+ attempts/;
|
|
93
|
+
/**
|
|
94
|
+
* WU-1336: Typed error for retry exhaustion in micro-worktree operations
|
|
95
|
+
*
|
|
96
|
+
* Thrown when push retries are exhausted due to race conditions with parallel agents.
|
|
97
|
+
* CLI commands should use `isRetryExhaustionError` to detect this error type and
|
|
98
|
+
* `formatRetryExhaustionError` to generate actionable user-facing messages.
|
|
99
|
+
*
|
|
100
|
+
* This centralizes retry exhaustion handling so CLI commands do not need to
|
|
101
|
+
* duplicate detection logic or error formatting.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* import { RetryExhaustionError, isRetryExhaustionError, formatRetryExhaustionError } from '@lumenflow/core';
|
|
106
|
+
*
|
|
107
|
+
* try {
|
|
108
|
+
* await withMicroWorktree({ ... });
|
|
109
|
+
* } catch (error) {
|
|
110
|
+
* if (isRetryExhaustionError(error)) {
|
|
111
|
+
* console.error(formatRetryExhaustionError(error, { command: 'pnpm initiative:add-wu ...' }));
|
|
112
|
+
* } else {
|
|
113
|
+
* throw error;
|
|
114
|
+
* }
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export class RetryExhaustionError extends Error {
|
|
119
|
+
/** Name of the error class (for instanceof checks across module boundaries) */
|
|
120
|
+
name = 'RetryExhaustionError';
|
|
121
|
+
/** Operation that was being performed (e.g., 'initiative-add-wu') */
|
|
122
|
+
operation;
|
|
123
|
+
/** Number of retry attempts that were exhausted */
|
|
124
|
+
retries;
|
|
125
|
+
constructor(operation, retries) {
|
|
126
|
+
super(`Push failed after ${retries} attempts. ` +
|
|
127
|
+
`Origin main may have significant traffic during ${operation}.`);
|
|
128
|
+
this.operation = operation;
|
|
129
|
+
this.retries = retries;
|
|
130
|
+
// Maintain proper prototype chain for instanceof checks
|
|
131
|
+
Object.setPrototypeOf(this, RetryExhaustionError.prototype);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* WU-1336: Type guard to check if an error is a retry exhaustion error
|
|
136
|
+
*
|
|
137
|
+
* Detects both the typed `RetryExhaustionError` class and legacy error messages
|
|
138
|
+
* that match the "Push failed after N attempts" pattern.
|
|
139
|
+
*
|
|
140
|
+
* @param {unknown} error - Error to check
|
|
141
|
+
* @returns {boolean} True if this is a retry exhaustion error
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* if (isRetryExhaustionError(error)) {
|
|
146
|
+
* // Handle retry exhaustion
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function isRetryExhaustionError(error) {
|
|
151
|
+
if (error instanceof RetryExhaustionError) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
// Also detect legacy error messages for backwards compatibility
|
|
155
|
+
if (error instanceof Error) {
|
|
156
|
+
return RETRY_EXHAUSTION_PATTERN.test(error.message);
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* WU-1336: Format retry exhaustion error with actionable next steps
|
|
162
|
+
*
|
|
163
|
+
* When push retries are exhausted, provides clear guidance on how to proceed.
|
|
164
|
+
* CLI commands should use this instead of duplicating error formatting logic.
|
|
165
|
+
*
|
|
166
|
+
* @param {Error} error - The retry exhaustion error
|
|
167
|
+
* @param {FormatRetryExhaustionOptions} options - Formatting options
|
|
168
|
+
* @returns {string} Formatted error message with next steps
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* const message = formatRetryExhaustionError(error, {
|
|
173
|
+
* command: 'pnpm initiative:add-wu --wu WU-123 --initiative INIT-001',
|
|
174
|
+
* });
|
|
175
|
+
* console.error(message);
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export function formatRetryExhaustionError(error, options) {
|
|
179
|
+
const { command } = options;
|
|
180
|
+
return (`${error.message}\n\n` +
|
|
181
|
+
`Next steps:\n` +
|
|
182
|
+
` 1. Wait a few seconds and retry the operation:\n` +
|
|
183
|
+
` ${command}\n` +
|
|
184
|
+
` 2. If the issue persists, check if another agent is rapidly pushing changes\n` +
|
|
185
|
+
` 3. Consider increasing git.push_retry.retries in .lumenflow.config.yaml`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* WU-1308: Check if remote operations should be skipped based on git.requireRemote config
|
|
189
|
+
*
|
|
190
|
+
* When git.requireRemote is false, micro-worktree operations skip:
|
|
191
|
+
* - Fetching origin/main before starting
|
|
192
|
+
* - Pushing to origin/main after completion
|
|
193
|
+
*
|
|
194
|
+
* This enables local-only development without a remote repository.
|
|
195
|
+
*
|
|
196
|
+
* @returns {boolean} True if remote operations should be skipped (requireRemote=false)
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```yaml
|
|
200
|
+
* # .lumenflow.config.yaml
|
|
201
|
+
* git:
|
|
202
|
+
* requireRemote: false # Enable local-only mode
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function shouldSkipRemoteOperations() {
|
|
206
|
+
const config = getConfig();
|
|
207
|
+
// Default is requireRemote=true, so only skip if explicitly set to false
|
|
208
|
+
return config.git.requireRemote === false;
|
|
209
|
+
}
|
|
69
210
|
/**
|
|
70
211
|
* Temp branch prefix for micro-worktree operations
|
|
71
212
|
*
|
|
@@ -377,11 +518,16 @@ export async function mergeWithRetry(tempBranchName, microWorktreePath, logPrefi
|
|
|
377
518
|
* Push to origin/main with retry logic for race conditions
|
|
378
519
|
*
|
|
379
520
|
* WU-1179: When push fails because origin/main advanced (race condition with
|
|
380
|
-
* parallel agents), this function
|
|
381
|
-
* retries the full sequence: fetch -> rebase temp branch -> re-merge -> push.
|
|
521
|
+
* parallel agents), this function retries with fetch and rebase.
|
|
382
522
|
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
523
|
+
* WU-1348: The retry logic no longer resets the main checkout. Instead, it:
|
|
524
|
+
* 1. Fetches origin/main to get latest remote state
|
|
525
|
+
* 2. Rebases the temp branch onto origin/main (in the micro-worktree)
|
|
526
|
+
* 3. Re-merges the rebased temp branch to local main (ff-only)
|
|
527
|
+
* 4. Retries the push
|
|
528
|
+
*
|
|
529
|
+
* This preserves micro-worktree isolation - the main checkout files are never
|
|
530
|
+
* hard-reset, preventing file flash and preserving any uncommitted work.
|
|
385
531
|
*
|
|
386
532
|
* @param {Object} mainGit - GitAdapter instance for main checkout
|
|
387
533
|
* @param {Object} worktreeGit - GitAdapter instance for micro-worktree
|
|
@@ -392,41 +538,119 @@ export async function mergeWithRetry(tempBranchName, microWorktreePath, logPrefi
|
|
|
392
538
|
* @throws {Error} If push fails after all retries
|
|
393
539
|
*/
|
|
394
540
|
export async function pushWithRetry(mainGit, worktreeGit, remote, branch, tempBranchName, logPrefix = DEFAULT_LOG_PREFIX) {
|
|
395
|
-
|
|
541
|
+
// eslint-disable-next-line sonarjs/deprecation -- Using deprecated constant for backwards compatibility
|
|
542
|
+
const maxRetries = MAX_PUSH_RETRIES;
|
|
543
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
396
544
|
try {
|
|
397
|
-
console.log(`${logPrefix} Pushing to ${remote}/${branch} (attempt ${attempt}/${
|
|
545
|
+
console.log(`${logPrefix} Pushing to ${remote}/${branch} (attempt ${attempt}/${maxRetries})...`);
|
|
398
546
|
await mainGit.push(remote, branch);
|
|
399
547
|
console.log(`${logPrefix} ✅ Pushed to ${remote}/${branch}`);
|
|
400
548
|
return;
|
|
401
549
|
}
|
|
402
550
|
catch (pushErr) {
|
|
403
|
-
if (attempt <
|
|
404
|
-
console.log(`${logPrefix} ⚠️ Push failed (origin moved).
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
// Step 2: Fetch latest origin/main
|
|
551
|
+
if (attempt < maxRetries) {
|
|
552
|
+
console.log(`${logPrefix} ⚠️ Push failed (origin moved). Fetching and rebasing before retry...`);
|
|
553
|
+
// WU-1348: Do NOT reset main checkout - preserve micro-worktree isolation
|
|
554
|
+
// Instead, fetch latest remote state and rebase the temp branch
|
|
555
|
+
// Step 1: Fetch latest origin/main
|
|
409
556
|
console.log(`${logPrefix} Fetching ${remote}/${branch}...`);
|
|
410
557
|
await mainGit.fetch(remote, branch);
|
|
411
|
-
// Step
|
|
412
|
-
console.log(`${logPrefix}
|
|
413
|
-
await
|
|
414
|
-
// Step
|
|
415
|
-
|
|
416
|
-
await worktreeGit.rebase(branch);
|
|
417
|
-
// Step 5: Re-merge temp branch to local main
|
|
558
|
+
// Step 2: Rebase temp branch onto updated origin/main
|
|
559
|
+
console.log(`${logPrefix} Rebasing temp branch onto ${remote}/${branch}...`);
|
|
560
|
+
await worktreeGit.rebase(`${remote}/${branch}`);
|
|
561
|
+
// Step 3: Re-merge temp branch to local main (ff-only)
|
|
562
|
+
// This updates local main to include the rebased commits
|
|
418
563
|
console.log(`${logPrefix} Re-merging temp branch to ${branch}...`);
|
|
419
564
|
await mainGit.merge(tempBranchName, { ffOnly: true });
|
|
420
565
|
}
|
|
421
566
|
else {
|
|
422
567
|
const errMsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
|
|
423
|
-
throw new Error(`Push failed after ${
|
|
424
|
-
`Origin ${branch} may have significant traffic.\n` +
|
|
568
|
+
throw new Error(`Push failed after ${maxRetries} attempts. ` +
|
|
569
|
+
`Origin ${branch} may have significant traffic.\n\n` +
|
|
570
|
+
`Suggestions:\n` +
|
|
571
|
+
` - Wait a few seconds and retry the operation\n` +
|
|
572
|
+
` - Check if another agent is rapidly pushing changes\n` +
|
|
425
573
|
`Error: ${errMsg}`);
|
|
426
574
|
}
|
|
427
575
|
}
|
|
428
576
|
}
|
|
429
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* WU-1332: Push to origin with configurable retry using p-retry
|
|
580
|
+
*
|
|
581
|
+
* Enhanced version of pushWithRetry that uses p-retry for exponential backoff
|
|
582
|
+
* and supports configuration via PushRetryConfig. When push fails due to
|
|
583
|
+
* non-fast-forward (origin moved), automatically rebases and retries.
|
|
584
|
+
*
|
|
585
|
+
* WU-1348: The retry logic no longer resets the main checkout. Instead, it:
|
|
586
|
+
* 1. Fetches origin/main to get latest remote state
|
|
587
|
+
* 2. Rebases the temp branch onto origin/main (in the micro-worktree)
|
|
588
|
+
* 3. Re-merges the rebased temp branch to local main (ff-only)
|
|
589
|
+
* 4. Retries the push
|
|
590
|
+
*
|
|
591
|
+
* This preserves micro-worktree isolation - the main checkout files are never
|
|
592
|
+
* hard-reset, preventing file flash and preserving any uncommitted work.
|
|
593
|
+
*
|
|
594
|
+
* @param {Object} mainGit - GitAdapter instance for main checkout
|
|
595
|
+
* @param {Object} worktreeGit - GitAdapter instance for micro-worktree
|
|
596
|
+
* @param {string} remote - Remote name (e.g., 'origin')
|
|
597
|
+
* @param {string} branch - Branch name (e.g., 'main')
|
|
598
|
+
* @param {string} tempBranchName - Temp branch that was merged (for rebase)
|
|
599
|
+
* @param {string} logPrefix - Log prefix for console output
|
|
600
|
+
* @param {PushRetryConfig} config - Push retry configuration
|
|
601
|
+
* @throws {Error} If push fails after all retries or if retry is disabled
|
|
602
|
+
*/
|
|
603
|
+
export async function pushWithRetryConfig(mainGit, worktreeGit, remote, branch, tempBranchName, logPrefix = DEFAULT_LOG_PREFIX, config = DEFAULT_PUSH_RETRY_CONFIG) {
|
|
604
|
+
// If retry is disabled, just try once and throw on failure
|
|
605
|
+
if (!config.enabled) {
|
|
606
|
+
console.log(`${logPrefix} Pushing to ${remote}/${branch} (retry disabled)...`);
|
|
607
|
+
await mainGit.push(remote, branch);
|
|
608
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${branch}`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
let attemptNumber = 0;
|
|
612
|
+
await pRetry(async () => {
|
|
613
|
+
attemptNumber++;
|
|
614
|
+
console.log(`${logPrefix} Pushing to ${remote}/${branch} (attempt ${attemptNumber}/${config.retries})...`);
|
|
615
|
+
try {
|
|
616
|
+
await mainGit.push(remote, branch);
|
|
617
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${branch}`);
|
|
618
|
+
}
|
|
619
|
+
catch (pushErr) {
|
|
620
|
+
console.log(`${logPrefix} ⚠️ Push failed (origin moved). Fetching and rebasing before retry...`);
|
|
621
|
+
// WU-1348: Do NOT reset main checkout - preserve micro-worktree isolation
|
|
622
|
+
// Instead, fetch latest remote state and rebase the temp branch
|
|
623
|
+
// Fetch latest origin/main
|
|
624
|
+
console.log(`${logPrefix} Fetching ${remote}/${branch}...`);
|
|
625
|
+
await mainGit.fetch(remote, branch);
|
|
626
|
+
// Rebase temp branch onto updated origin/main
|
|
627
|
+
console.log(`${logPrefix} Rebasing temp branch onto ${remote}/${branch}...`);
|
|
628
|
+
await worktreeGit.rebase(`${remote}/${branch}`);
|
|
629
|
+
// Re-merge temp branch to local main (ff-only)
|
|
630
|
+
// This updates local main to include the rebased commits
|
|
631
|
+
console.log(`${logPrefix} Re-merging temp branch to ${branch}...`);
|
|
632
|
+
await mainGit.merge(tempBranchName, { ffOnly: true });
|
|
633
|
+
// Re-throw to trigger p-retry
|
|
634
|
+
throw pushErr;
|
|
635
|
+
}
|
|
636
|
+
}, {
|
|
637
|
+
retries: config.retries - 1, // p-retry counts retries after first attempt
|
|
638
|
+
minTimeout: config.min_delay_ms,
|
|
639
|
+
maxTimeout: config.max_delay_ms,
|
|
640
|
+
randomize: config.jitter,
|
|
641
|
+
onFailedAttempt: () => {
|
|
642
|
+
// Logging is handled in the try/catch above
|
|
643
|
+
},
|
|
644
|
+
}).catch(() => {
|
|
645
|
+
// p-retry exhausted all retries, throw descriptive error
|
|
646
|
+
throw new Error(`Push failed after ${config.retries} attempts. ` +
|
|
647
|
+
`Origin ${branch} may have significant traffic.\n\n` +
|
|
648
|
+
`Suggestions:\n` +
|
|
649
|
+
` - Wait a few seconds and retry the operation\n` +
|
|
650
|
+
` - Increase git.push_retry.retries in .lumenflow.config.yaml\n` +
|
|
651
|
+
` - Check if another agent is rapidly pushing changes`);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
430
654
|
/**
|
|
431
655
|
* Push using refspec with LUMENFLOW_FORCE to bypass pre-push hooks
|
|
432
656
|
*
|
|
@@ -472,6 +696,72 @@ export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteR
|
|
|
472
696
|
}
|
|
473
697
|
}
|
|
474
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* WU-1337: Push using refspec with LUMENFLOW_FORCE and retry logic
|
|
701
|
+
*
|
|
702
|
+
* Enhanced version of pushRefspecWithForce that adds retry with rebase
|
|
703
|
+
* on non-fast-forward errors. Uses p-retry for exponential backoff and
|
|
704
|
+
* respects git.push_retry configuration.
|
|
705
|
+
*
|
|
706
|
+
* On each retry:
|
|
707
|
+
* 1. Fetch origin/main to get latest state
|
|
708
|
+
* 2. Rebase the temp branch onto the updated main
|
|
709
|
+
* 3. Retry the push with LUMENFLOW_FORCE
|
|
710
|
+
*
|
|
711
|
+
* This is used by pushOnly mode in withMicroWorktree to handle race conditions
|
|
712
|
+
* when multiple agents are pushing to origin/main concurrently.
|
|
713
|
+
*
|
|
714
|
+
* @param {GitAdapter} gitWorktree - GitAdapter instance for the worktree (for rebase)
|
|
715
|
+
* @param {GitAdapter} mainGit - GitAdapter instance for main checkout (for fetch)
|
|
716
|
+
* @param {string} remote - Remote name (e.g., 'origin')
|
|
717
|
+
* @param {string} localRef - Local ref to push (e.g., 'tmp/wu-claim/wu-123')
|
|
718
|
+
* @param {string} remoteRef - Remote ref to update (e.g., 'main')
|
|
719
|
+
* @param {string} reason - Audit reason for the LUMENFLOW_FORCE bypass
|
|
720
|
+
* @param {string} logPrefix - Log prefix for console output
|
|
721
|
+
* @param {PushRetryConfig} config - Push retry configuration
|
|
722
|
+
* @returns {Promise<void>}
|
|
723
|
+
* @throws {RetryExhaustionError} If push fails after all retries
|
|
724
|
+
*/
|
|
725
|
+
export async function pushRefspecWithRetry(gitWorktree, mainGit, remote, localRef, remoteRef, reason, logPrefix = DEFAULT_LOG_PREFIX, config = DEFAULT_PUSH_RETRY_CONFIG) {
|
|
726
|
+
// If retry is disabled, just try once and throw on failure
|
|
727
|
+
if (!config.enabled) {
|
|
728
|
+
console.log(`${logPrefix} Pushing to ${remote}/${remoteRef} (push-only, retry disabled)...`);
|
|
729
|
+
await pushRefspecWithForce(gitWorktree, remote, localRef, remoteRef, reason);
|
|
730
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${remoteRef}`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
let attemptNumber = 0;
|
|
734
|
+
await pRetry(async () => {
|
|
735
|
+
attemptNumber++;
|
|
736
|
+
console.log(`${logPrefix} Pushing to ${remote}/${remoteRef} (push-only, attempt ${attemptNumber}/${config.retries})...`);
|
|
737
|
+
try {
|
|
738
|
+
await pushRefspecWithForce(gitWorktree, remote, localRef, remoteRef, reason);
|
|
739
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${remoteRef}`);
|
|
740
|
+
}
|
|
741
|
+
catch (pushErr) {
|
|
742
|
+
console.log(`${logPrefix} ⚠️ Push failed (origin moved). Fetching and rebasing before retry...`);
|
|
743
|
+
// Fetch latest origin/main
|
|
744
|
+
console.log(`${logPrefix} Fetching ${remote}/${remoteRef}...`);
|
|
745
|
+
await mainGit.fetch(remote, remoteRef);
|
|
746
|
+
// Rebase temp branch onto updated main
|
|
747
|
+
console.log(`${logPrefix} Rebasing temp branch onto ${remoteRef}...`);
|
|
748
|
+
await gitWorktree.rebase(remoteRef);
|
|
749
|
+
// Re-throw to trigger p-retry
|
|
750
|
+
throw pushErr;
|
|
751
|
+
}
|
|
752
|
+
}, {
|
|
753
|
+
retries: config.retries - 1, // p-retry counts retries after first attempt
|
|
754
|
+
minTimeout: config.min_delay_ms,
|
|
755
|
+
maxTimeout: config.max_delay_ms,
|
|
756
|
+
randomize: config.jitter,
|
|
757
|
+
onFailedAttempt: () => {
|
|
758
|
+
// Logging is handled in the try/catch above
|
|
759
|
+
},
|
|
760
|
+
}).catch(() => {
|
|
761
|
+
// p-retry exhausted all retries, throw typed error
|
|
762
|
+
throw new RetryExhaustionError('push-only', config.retries);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
475
765
|
/**
|
|
476
766
|
* Execute an operation in a micro-worktree with full isolation
|
|
477
767
|
*
|
|
@@ -481,6 +771,7 @@ export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteR
|
|
|
481
771
|
*
|
|
482
772
|
* WU-1435: Added pushOnly option to keep local main pristine.
|
|
483
773
|
* WU-2237: Added pre-creation cleanup of orphaned temp branches/worktrees.
|
|
774
|
+
* WU-1337: Push-only path now uses retry with rebase.
|
|
484
775
|
*
|
|
485
776
|
* @param {Object} options - Options for the operation
|
|
486
777
|
* @param {string} options.operation - Operation name (e.g., 'wu-create', 'wu-edit')
|
|
@@ -496,18 +787,24 @@ export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteR
|
|
|
496
787
|
export async function withMicroWorktree(options) {
|
|
497
788
|
const { operation, id, logPrefix = `[${operation}]`, execute, pushOnly = false } = options;
|
|
498
789
|
const mainGit = getGitForCwd();
|
|
790
|
+
// WU-1308: Check if remote operations should be skipped (local-only mode)
|
|
791
|
+
const skipRemote = shouldSkipRemoteOperations();
|
|
499
792
|
// WU-2237: Clean up any orphaned temp branch/worktree from previous interrupted operations
|
|
500
793
|
// This makes the operation idempotent - a retry after crash/timeout will succeed
|
|
501
794
|
await cleanupOrphanedMicroWorktree(operation, id, mainGit, logPrefix);
|
|
502
795
|
// WU-1179: Fetch origin/main before starting to minimize race condition window
|
|
503
796
|
// This ensures we start from the latest origin state, reducing push failures
|
|
504
|
-
|
|
797
|
+
// WU-1308: Skip when git.requireRemote=false (local-only mode)
|
|
798
|
+
if (!pushOnly && !skipRemote) {
|
|
505
799
|
console.log(`${logPrefix} Fetching ${REMOTES.ORIGIN}/${BRANCHES.MAIN} before starting...`);
|
|
506
800
|
await mainGit.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
507
801
|
// Update local main to match origin/main
|
|
508
802
|
await mainGit.merge(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`, { ffOnly: true });
|
|
509
803
|
console.log(`${logPrefix} ✅ Local main synced with ${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
|
|
510
804
|
}
|
|
805
|
+
else if (skipRemote) {
|
|
806
|
+
console.log(`${logPrefix} Local-only mode (git.requireRemote=false): skipping origin sync`);
|
|
807
|
+
}
|
|
511
808
|
const tempBranchName = getTempBranchName(operation, id);
|
|
512
809
|
const microWorktreePath = createMicroWorktreeDir(`${operation}-`);
|
|
513
810
|
console.log(`${logPrefix} Using micro-worktree isolation (WU-1262)`);
|
|
@@ -536,12 +833,22 @@ export async function withMicroWorktree(options) {
|
|
|
536
833
|
await gitWorktree.commit(result.commitMessage);
|
|
537
834
|
console.log(`${logPrefix} ✅ Committed: ${result.commitMessage}`);
|
|
538
835
|
// Step 6: Push to origin (different paths for pushOnly vs standard)
|
|
539
|
-
|
|
836
|
+
// WU-1308: Skip push when git.requireRemote=false (local-only mode)
|
|
837
|
+
if (skipRemote) {
|
|
838
|
+
// Local-only mode: merge to local main but skip push
|
|
839
|
+
console.log(`${logPrefix} Local-only mode: merging to local main (skipping push)`);
|
|
840
|
+
await mainGit.merge(tempBranchName, { ffOnly: true });
|
|
841
|
+
console.log(`${logPrefix} ✅ Merged to local main (no remote push)`);
|
|
842
|
+
return { ...result, ref: BRANCHES.MAIN };
|
|
843
|
+
}
|
|
844
|
+
else if (pushOnly) {
|
|
540
845
|
// WU-1435: Push directly to origin/main without touching local main
|
|
541
846
|
// WU-1081: Use LUMENFLOW_FORCE to bypass pre-push hooks for micro-worktree pushes
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
847
|
+
// WU-1337: Use pushRefspecWithRetry to handle race conditions with rebase
|
|
848
|
+
// Get push_retry config from LumenFlow config
|
|
849
|
+
const config = getConfig();
|
|
850
|
+
const pushRetryConfig = config.git.push_retry || DEFAULT_PUSH_RETRY_CONFIG;
|
|
851
|
+
await pushRefspecWithRetry(gitWorktree, mainGit, REMOTES.ORIGIN, tempBranchName, BRANCHES.MAIN, `micro-worktree push for ${operation} (automated)`, logPrefix, pushRetryConfig);
|
|
545
852
|
// Fetch to update remote tracking ref (FETCH_HEAD)
|
|
546
853
|
console.log(`${logPrefix} Fetching ${REMOTES.ORIGIN}/${BRANCHES.MAIN}...`);
|
|
547
854
|
await mainGit.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|