@lumenflow/core 2.3.2 → 2.5.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.
- package/dist/arg-parser.d.ts +21 -2
- package/dist/arg-parser.js +74 -1
- package/dist/context/wu-state-reader.js +3 -2
- package/dist/lane-checker.d.ts +29 -2
- package/dist/lane-checker.js +176 -20
- package/dist/lane-inference.js +8 -2
- package/dist/lane-lock.d.ts +8 -0
- package/dist/lane-lock.js +21 -0
- package/dist/lumenflow-config-schema.d.ts +89 -0
- package/dist/lumenflow-config-schema.js +133 -0
- package/dist/lumenflow-config.d.ts +5 -1
- package/dist/lumenflow-config.js +12 -0
- package/dist/micro-worktree.d.ts +151 -0
- package/dist/micro-worktree.js +308 -9
- 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 +30 -0
- package/dist/wu-paths.js +15 -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.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
|
*
|
|
@@ -392,15 +533,17 @@ export async function mergeWithRetry(tempBranchName, microWorktreePath, logPrefi
|
|
|
392
533
|
* @throws {Error} If push fails after all retries
|
|
393
534
|
*/
|
|
394
535
|
export async function pushWithRetry(mainGit, worktreeGit, remote, branch, tempBranchName, logPrefix = DEFAULT_LOG_PREFIX) {
|
|
395
|
-
|
|
536
|
+
// eslint-disable-next-line sonarjs/deprecation -- Using deprecated constant for backwards compatibility
|
|
537
|
+
const maxRetries = MAX_PUSH_RETRIES;
|
|
538
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
396
539
|
try {
|
|
397
|
-
console.log(`${logPrefix} Pushing to ${remote}/${branch} (attempt ${attempt}/${
|
|
540
|
+
console.log(`${logPrefix} Pushing to ${remote}/${branch} (attempt ${attempt}/${maxRetries})...`);
|
|
398
541
|
await mainGit.push(remote, branch);
|
|
399
542
|
console.log(`${logPrefix} ✅ Pushed to ${remote}/${branch}`);
|
|
400
543
|
return;
|
|
401
544
|
}
|
|
402
545
|
catch (pushErr) {
|
|
403
|
-
if (attempt <
|
|
546
|
+
if (attempt < maxRetries) {
|
|
404
547
|
console.log(`${logPrefix} ⚠️ Push failed (origin moved). Rolling back and retrying...`);
|
|
405
548
|
// Step 1: Rollback local main to origin/main
|
|
406
549
|
console.log(`${logPrefix} Rolling back local ${branch} to ${remote}/${branch}...`);
|
|
@@ -420,13 +563,86 @@ export async function pushWithRetry(mainGit, worktreeGit, remote, branch, tempBr
|
|
|
420
563
|
}
|
|
421
564
|
else {
|
|
422
565
|
const errMsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
|
|
423
|
-
throw new Error(`Push failed after ${
|
|
566
|
+
throw new Error(`Push failed after ${maxRetries} attempts. ` +
|
|
424
567
|
`Origin ${branch} may have significant traffic.\n` +
|
|
425
568
|
`Error: ${errMsg}`);
|
|
426
569
|
}
|
|
427
570
|
}
|
|
428
571
|
}
|
|
429
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* WU-1332: Push to origin with configurable retry using p-retry
|
|
575
|
+
*
|
|
576
|
+
* Enhanced version of pushWithRetry that uses p-retry for exponential backoff
|
|
577
|
+
* and supports configuration via PushRetryConfig. When push fails due to
|
|
578
|
+
* non-fast-forward (origin moved), automatically rebases and retries.
|
|
579
|
+
*
|
|
580
|
+
* @param {Object} mainGit - GitAdapter instance for main checkout
|
|
581
|
+
* @param {Object} worktreeGit - GitAdapter instance for micro-worktree
|
|
582
|
+
* @param {string} remote - Remote name (e.g., 'origin')
|
|
583
|
+
* @param {string} branch - Branch name (e.g., 'main')
|
|
584
|
+
* @param {string} tempBranchName - Temp branch that was merged (for rebase)
|
|
585
|
+
* @param {string} logPrefix - Log prefix for console output
|
|
586
|
+
* @param {PushRetryConfig} config - Push retry configuration
|
|
587
|
+
* @throws {Error} If push fails after all retries or if retry is disabled
|
|
588
|
+
*/
|
|
589
|
+
export async function pushWithRetryConfig(mainGit, worktreeGit, remote, branch, tempBranchName, logPrefix = DEFAULT_LOG_PREFIX, config = DEFAULT_PUSH_RETRY_CONFIG) {
|
|
590
|
+
// If retry is disabled, just try once and throw on failure
|
|
591
|
+
if (!config.enabled) {
|
|
592
|
+
console.log(`${logPrefix} Pushing to ${remote}/${branch} (retry disabled)...`);
|
|
593
|
+
await mainGit.push(remote, branch);
|
|
594
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${branch}`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
let attemptNumber = 0;
|
|
598
|
+
await pRetry(async () => {
|
|
599
|
+
attemptNumber++;
|
|
600
|
+
console.log(`${logPrefix} Pushing to ${remote}/${branch} (attempt ${attemptNumber}/${config.retries})...`);
|
|
601
|
+
try {
|
|
602
|
+
await mainGit.push(remote, branch);
|
|
603
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${branch}`);
|
|
604
|
+
}
|
|
605
|
+
catch (pushErr) {
|
|
606
|
+
console.log(`${logPrefix} ⚠️ Push failed (origin moved). Rolling back and retrying...`);
|
|
607
|
+
// Rollback local main to origin/main
|
|
608
|
+
console.log(`${logPrefix} Rolling back local ${branch} to ${remote}/${branch}...`);
|
|
609
|
+
await mainGit.reset(`${remote}/${branch}`, { hard: true });
|
|
610
|
+
// Fetch latest origin/main
|
|
611
|
+
console.log(`${logPrefix} Fetching ${remote}/${branch}...`);
|
|
612
|
+
await mainGit.fetch(remote, branch);
|
|
613
|
+
// Update local main to match origin/main (ff-only)
|
|
614
|
+
console.log(`${logPrefix} Updating local ${branch}...`);
|
|
615
|
+
await mainGit.merge(`${remote}/${branch}`, { ffOnly: true });
|
|
616
|
+
// Rebase temp branch onto updated main
|
|
617
|
+
console.log(`${logPrefix} Rebasing temp branch onto ${branch}...`);
|
|
618
|
+
await worktreeGit.rebase(branch);
|
|
619
|
+
// Re-merge temp branch to local main
|
|
620
|
+
console.log(`${logPrefix} Re-merging temp branch to ${branch}...`);
|
|
621
|
+
await mainGit.merge(tempBranchName, { ffOnly: true });
|
|
622
|
+
// Re-throw to trigger p-retry
|
|
623
|
+
throw pushErr;
|
|
624
|
+
}
|
|
625
|
+
}, {
|
|
626
|
+
retries: config.retries - 1, // p-retry counts retries after first attempt
|
|
627
|
+
minTimeout: config.min_delay_ms,
|
|
628
|
+
maxTimeout: config.max_delay_ms,
|
|
629
|
+
randomize: config.jitter,
|
|
630
|
+
onFailedAttempt: (error) => {
|
|
631
|
+
// Log is handled in the try/catch above
|
|
632
|
+
if (error.retriesLeft === 0) {
|
|
633
|
+
// This will be the final failure
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
}).catch(() => {
|
|
637
|
+
// p-retry exhausted all retries, throw descriptive error
|
|
638
|
+
throw new Error(`Push failed after ${config.retries} attempts. ` +
|
|
639
|
+
`Origin ${branch} may have significant traffic.\n\n` +
|
|
640
|
+
`Suggestions:\n` +
|
|
641
|
+
` - Wait a few seconds and retry the operation\n` +
|
|
642
|
+
` - Increase git.push_retry.retries in .lumenflow.config.yaml\n` +
|
|
643
|
+
` - Check if another agent is rapidly pushing changes`);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
430
646
|
/**
|
|
431
647
|
* Push using refspec with LUMENFLOW_FORCE to bypass pre-push hooks
|
|
432
648
|
*
|
|
@@ -472,6 +688,72 @@ export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteR
|
|
|
472
688
|
}
|
|
473
689
|
}
|
|
474
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* WU-1337: Push using refspec with LUMENFLOW_FORCE and retry logic
|
|
693
|
+
*
|
|
694
|
+
* Enhanced version of pushRefspecWithForce that adds retry with rebase
|
|
695
|
+
* on non-fast-forward errors. Uses p-retry for exponential backoff and
|
|
696
|
+
* respects git.push_retry configuration.
|
|
697
|
+
*
|
|
698
|
+
* On each retry:
|
|
699
|
+
* 1. Fetch origin/main to get latest state
|
|
700
|
+
* 2. Rebase the temp branch onto the updated main
|
|
701
|
+
* 3. Retry the push with LUMENFLOW_FORCE
|
|
702
|
+
*
|
|
703
|
+
* This is used by pushOnly mode in withMicroWorktree to handle race conditions
|
|
704
|
+
* when multiple agents are pushing to origin/main concurrently.
|
|
705
|
+
*
|
|
706
|
+
* @param {GitAdapter} gitWorktree - GitAdapter instance for the worktree (for rebase)
|
|
707
|
+
* @param {GitAdapter} mainGit - GitAdapter instance for main checkout (for fetch)
|
|
708
|
+
* @param {string} remote - Remote name (e.g., 'origin')
|
|
709
|
+
* @param {string} localRef - Local ref to push (e.g., 'tmp/wu-claim/wu-123')
|
|
710
|
+
* @param {string} remoteRef - Remote ref to update (e.g., 'main')
|
|
711
|
+
* @param {string} reason - Audit reason for the LUMENFLOW_FORCE bypass
|
|
712
|
+
* @param {string} logPrefix - Log prefix for console output
|
|
713
|
+
* @param {PushRetryConfig} config - Push retry configuration
|
|
714
|
+
* @returns {Promise<void>}
|
|
715
|
+
* @throws {RetryExhaustionError} If push fails after all retries
|
|
716
|
+
*/
|
|
717
|
+
export async function pushRefspecWithRetry(gitWorktree, mainGit, remote, localRef, remoteRef, reason, logPrefix = DEFAULT_LOG_PREFIX, config = DEFAULT_PUSH_RETRY_CONFIG) {
|
|
718
|
+
// If retry is disabled, just try once and throw on failure
|
|
719
|
+
if (!config.enabled) {
|
|
720
|
+
console.log(`${logPrefix} Pushing to ${remote}/${remoteRef} (push-only, retry disabled)...`);
|
|
721
|
+
await pushRefspecWithForce(gitWorktree, remote, localRef, remoteRef, reason);
|
|
722
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${remoteRef}`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
let attemptNumber = 0;
|
|
726
|
+
await pRetry(async () => {
|
|
727
|
+
attemptNumber++;
|
|
728
|
+
console.log(`${logPrefix} Pushing to ${remote}/${remoteRef} (push-only, attempt ${attemptNumber}/${config.retries})...`);
|
|
729
|
+
try {
|
|
730
|
+
await pushRefspecWithForce(gitWorktree, remote, localRef, remoteRef, reason);
|
|
731
|
+
console.log(`${logPrefix} ✅ Pushed to ${remote}/${remoteRef}`);
|
|
732
|
+
}
|
|
733
|
+
catch (pushErr) {
|
|
734
|
+
console.log(`${logPrefix} ⚠️ Push failed (origin moved). Fetching and rebasing before retry...`);
|
|
735
|
+
// Fetch latest origin/main
|
|
736
|
+
console.log(`${logPrefix} Fetching ${remote}/${remoteRef}...`);
|
|
737
|
+
await mainGit.fetch(remote, remoteRef);
|
|
738
|
+
// Rebase temp branch onto updated main
|
|
739
|
+
console.log(`${logPrefix} Rebasing temp branch onto ${remoteRef}...`);
|
|
740
|
+
await gitWorktree.rebase(remoteRef);
|
|
741
|
+
// Re-throw to trigger p-retry
|
|
742
|
+
throw pushErr;
|
|
743
|
+
}
|
|
744
|
+
}, {
|
|
745
|
+
retries: config.retries - 1, // p-retry counts retries after first attempt
|
|
746
|
+
minTimeout: config.min_delay_ms,
|
|
747
|
+
maxTimeout: config.max_delay_ms,
|
|
748
|
+
randomize: config.jitter,
|
|
749
|
+
onFailedAttempt: () => {
|
|
750
|
+
// Logging is handled in the try/catch above
|
|
751
|
+
},
|
|
752
|
+
}).catch(() => {
|
|
753
|
+
// p-retry exhausted all retries, throw typed error
|
|
754
|
+
throw new RetryExhaustionError('push-only', config.retries);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
475
757
|
/**
|
|
476
758
|
* Execute an operation in a micro-worktree with full isolation
|
|
477
759
|
*
|
|
@@ -481,6 +763,7 @@ export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteR
|
|
|
481
763
|
*
|
|
482
764
|
* WU-1435: Added pushOnly option to keep local main pristine.
|
|
483
765
|
* WU-2237: Added pre-creation cleanup of orphaned temp branches/worktrees.
|
|
766
|
+
* WU-1337: Push-only path now uses retry with rebase.
|
|
484
767
|
*
|
|
485
768
|
* @param {Object} options - Options for the operation
|
|
486
769
|
* @param {string} options.operation - Operation name (e.g., 'wu-create', 'wu-edit')
|
|
@@ -496,18 +779,24 @@ export async function pushRefspecWithForce(gitAdapter, remote, localRef, remoteR
|
|
|
496
779
|
export async function withMicroWorktree(options) {
|
|
497
780
|
const { operation, id, logPrefix = `[${operation}]`, execute, pushOnly = false } = options;
|
|
498
781
|
const mainGit = getGitForCwd();
|
|
782
|
+
// WU-1308: Check if remote operations should be skipped (local-only mode)
|
|
783
|
+
const skipRemote = shouldSkipRemoteOperations();
|
|
499
784
|
// WU-2237: Clean up any orphaned temp branch/worktree from previous interrupted operations
|
|
500
785
|
// This makes the operation idempotent - a retry after crash/timeout will succeed
|
|
501
786
|
await cleanupOrphanedMicroWorktree(operation, id, mainGit, logPrefix);
|
|
502
787
|
// WU-1179: Fetch origin/main before starting to minimize race condition window
|
|
503
788
|
// This ensures we start from the latest origin state, reducing push failures
|
|
504
|
-
|
|
789
|
+
// WU-1308: Skip when git.requireRemote=false (local-only mode)
|
|
790
|
+
if (!pushOnly && !skipRemote) {
|
|
505
791
|
console.log(`${logPrefix} Fetching ${REMOTES.ORIGIN}/${BRANCHES.MAIN} before starting...`);
|
|
506
792
|
await mainGit.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
507
793
|
// Update local main to match origin/main
|
|
508
794
|
await mainGit.merge(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`, { ffOnly: true });
|
|
509
795
|
console.log(`${logPrefix} ✅ Local main synced with ${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
|
|
510
796
|
}
|
|
797
|
+
else if (skipRemote) {
|
|
798
|
+
console.log(`${logPrefix} Local-only mode (git.requireRemote=false): skipping origin sync`);
|
|
799
|
+
}
|
|
511
800
|
const tempBranchName = getTempBranchName(operation, id);
|
|
512
801
|
const microWorktreePath = createMicroWorktreeDir(`${operation}-`);
|
|
513
802
|
console.log(`${logPrefix} Using micro-worktree isolation (WU-1262)`);
|
|
@@ -536,12 +825,22 @@ export async function withMicroWorktree(options) {
|
|
|
536
825
|
await gitWorktree.commit(result.commitMessage);
|
|
537
826
|
console.log(`${logPrefix} ✅ Committed: ${result.commitMessage}`);
|
|
538
827
|
// Step 6: Push to origin (different paths for pushOnly vs standard)
|
|
539
|
-
|
|
828
|
+
// WU-1308: Skip push when git.requireRemote=false (local-only mode)
|
|
829
|
+
if (skipRemote) {
|
|
830
|
+
// Local-only mode: merge to local main but skip push
|
|
831
|
+
console.log(`${logPrefix} Local-only mode: merging to local main (skipping push)`);
|
|
832
|
+
await mainGit.merge(tempBranchName, { ffOnly: true });
|
|
833
|
+
console.log(`${logPrefix} ✅ Merged to local main (no remote push)`);
|
|
834
|
+
return { ...result, ref: BRANCHES.MAIN };
|
|
835
|
+
}
|
|
836
|
+
else if (pushOnly) {
|
|
540
837
|
// WU-1435: Push directly to origin/main without touching local main
|
|
541
838
|
// WU-1081: Use LUMENFLOW_FORCE to bypass pre-push hooks for micro-worktree pushes
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
839
|
+
// WU-1337: Use pushRefspecWithRetry to handle race conditions with rebase
|
|
840
|
+
// Get push_retry config from LumenFlow config
|
|
841
|
+
const config = getConfig();
|
|
842
|
+
const pushRetryConfig = config.git.push_retry || DEFAULT_PUSH_RETRY_CONFIG;
|
|
843
|
+
await pushRefspecWithRetry(gitWorktree, mainGit, REMOTES.ORIGIN, tempBranchName, BRANCHES.MAIN, `micro-worktree push for ${operation} (automated)`, logPrefix, pushRetryConfig);
|
|
545
844
|
// Fetch to update remote tracking ref (FETCH_HEAD)
|
|
546
845
|
console.log(`${logPrefix} Fetching ${REMOTES.ORIGIN}/${BRANCHES.MAIN}...`);
|
|
547
846
|
await mainGit.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
package/dist/wu-constants.d.ts
CHANGED
|
@@ -253,6 +253,10 @@ export declare const LOG_PREFIX: {
|
|
|
253
253
|
CONSISTENCY: string;
|
|
254
254
|
PREFLIGHT: string;
|
|
255
255
|
INITIATIVE_PLAN: string;
|
|
256
|
+
PLAN_CREATE: string;
|
|
257
|
+
PLAN_LINK: string;
|
|
258
|
+
PLAN_EDIT: string;
|
|
259
|
+
PLAN_PROMOTE: string;
|
|
256
260
|
};
|
|
257
261
|
/**
|
|
258
262
|
* Consistency check types (WU-1276)
|
|
@@ -940,6 +944,8 @@ export declare const GATE_NAMES: {
|
|
|
940
944
|
SYSTEM_MAP_VALIDATE: string;
|
|
941
945
|
/** WU-1191: Lane health check (overlap detection) */
|
|
942
946
|
LANE_HEALTH: string;
|
|
947
|
+
/** WU-1315: Onboarding smoke test (init + wu:create validation) */
|
|
948
|
+
ONBOARDING_SMOKE_TEST: string;
|
|
943
949
|
};
|
|
944
950
|
/**
|
|
945
951
|
* Gate command sentinels (special values for non-shell commands)
|
|
@@ -959,6 +965,8 @@ export declare const GATE_COMMANDS: {
|
|
|
959
965
|
SAFETY_CRITICAL_TEST: string;
|
|
960
966
|
/** WU-2062: Triggers tiered test execution based on risk */
|
|
961
967
|
TIERED_TEST: string;
|
|
968
|
+
/** WU-1315: Triggers onboarding smoke test */
|
|
969
|
+
ONBOARDING_SMOKE_TEST: string;
|
|
962
970
|
};
|
|
963
971
|
/**
|
|
964
972
|
* CLI mode flags
|
package/dist/wu-constants.js
CHANGED
|
@@ -269,6 +269,10 @@ export const LOG_PREFIX = {
|
|
|
269
269
|
CONSISTENCY: '[wu-consistency]',
|
|
270
270
|
PREFLIGHT: '[wu-preflight]',
|
|
271
271
|
INITIATIVE_PLAN: '[initiative:plan]',
|
|
272
|
+
PLAN_CREATE: '[plan:create]',
|
|
273
|
+
PLAN_LINK: '[plan:link]',
|
|
274
|
+
PLAN_EDIT: '[plan:edit]',
|
|
275
|
+
PLAN_PROMOTE: '[plan:promote]',
|
|
272
276
|
};
|
|
273
277
|
/**
|
|
274
278
|
* Consistency check types (WU-1276)
|
|
@@ -985,6 +989,8 @@ export const GATE_NAMES = {
|
|
|
985
989
|
SYSTEM_MAP_VALIDATE: 'system-map:validate',
|
|
986
990
|
/** WU-1191: Lane health check (overlap detection) */
|
|
987
991
|
LANE_HEALTH: 'lane-health',
|
|
992
|
+
/** WU-1315: Onboarding smoke test (init + wu:create validation) */
|
|
993
|
+
ONBOARDING_SMOKE_TEST: 'onboarding-smoke-test',
|
|
988
994
|
};
|
|
989
995
|
/**
|
|
990
996
|
* Gate command sentinels (special values for non-shell commands)
|
|
@@ -1004,6 +1010,8 @@ export const GATE_COMMANDS = {
|
|
|
1004
1010
|
SAFETY_CRITICAL_TEST: 'safety-critical-test',
|
|
1005
1011
|
/** WU-2062: Triggers tiered test execution based on risk */
|
|
1006
1012
|
TIERED_TEST: 'tiered-test',
|
|
1013
|
+
/** WU-1315: Triggers onboarding smoke test */
|
|
1014
|
+
ONBOARDING_SMOKE_TEST: 'onboarding-smoke-test',
|
|
1007
1015
|
};
|
|
1008
1016
|
/**
|
|
1009
1017
|
* CLI mode flags
|
|
@@ -86,6 +86,19 @@ export declare function mergeWithMainState(worktreeStateDir: string): Promise<WU
|
|
|
86
86
|
* @returns Merged backlog.md content
|
|
87
87
|
*/
|
|
88
88
|
export declare function computeBacklogContentWithMainMerge(backlogPath: string, wuId: string): Promise<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Compute status.md content with merged state from origin/main.
|
|
91
|
+
*
|
|
92
|
+
* WU-1319: This function generates status.md from the merged state store
|
|
93
|
+
* instead of editing the local file snapshot. This prevents reintroducing
|
|
94
|
+
* stale "In Progress" entries when concurrent WUs complete on main.
|
|
95
|
+
*
|
|
96
|
+
* @param backlogPath - Path to backlog.md in the worktree (used to find state dir)
|
|
97
|
+
* @param wuId - WU ID being completed
|
|
98
|
+
* @param mainStateDir - Optional explicit path to main state dir (for testing)
|
|
99
|
+
* @returns Merged status.md content
|
|
100
|
+
*/
|
|
101
|
+
export declare function computeStatusContentWithMainMerge(backlogPath: string, wuId: string, mainStateDir?: string): Promise<string>;
|
|
89
102
|
/**
|
|
90
103
|
* Compute wu-events.jsonl content with merged state from origin/main.
|
|
91
104
|
*
|
|
@@ -15,7 +15,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { WUStateStore, WU_EVENTS_FILE_NAME } from './wu-state-store.js';
|
|
17
17
|
import { validateWUEvent } from './wu-state-schema.js';
|
|
18
|
-
import { generateBacklog } from './backlog-generator.js';
|
|
18
|
+
import { generateBacklog, generateStatus } from './backlog-generator.js';
|
|
19
19
|
import { getStateStoreDirFromBacklog } from './wu-paths.js';
|
|
20
20
|
import { getGitForCwd } from './git-adapter.js';
|
|
21
21
|
import { REMOTES, BRANCHES, BEACON_PATHS } from './wu-constants.js';
|
|
@@ -286,6 +286,46 @@ export async function computeBacklogContentWithMainMerge(backlogPath, wuId) {
|
|
|
286
286
|
// Generate backlog from merged state
|
|
287
287
|
return generateBacklog(mergedStore);
|
|
288
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Compute status.md content with merged state from origin/main.
|
|
291
|
+
*
|
|
292
|
+
* WU-1319: This function generates status.md from the merged state store
|
|
293
|
+
* instead of editing the local file snapshot. This prevents reintroducing
|
|
294
|
+
* stale "In Progress" entries when concurrent WUs complete on main.
|
|
295
|
+
*
|
|
296
|
+
* @param backlogPath - Path to backlog.md in the worktree (used to find state dir)
|
|
297
|
+
* @param wuId - WU ID being completed
|
|
298
|
+
* @param mainStateDir - Optional explicit path to main state dir (for testing)
|
|
299
|
+
* @returns Merged status.md content
|
|
300
|
+
*/
|
|
301
|
+
export async function computeStatusContentWithMainMerge(backlogPath, wuId, mainStateDir) {
|
|
302
|
+
const worktreeStateDir = getStateStoreDirFromBacklog(backlogPath);
|
|
303
|
+
let mergedStore;
|
|
304
|
+
if (mainStateDir) {
|
|
305
|
+
// Direct merge with provided main state dir (for testing)
|
|
306
|
+
mergedStore = await mergeStateStores(worktreeStateDir, mainStateDir);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// Merge with main state via git show
|
|
310
|
+
mergedStore = await mergeWithMainState(worktreeStateDir);
|
|
311
|
+
}
|
|
312
|
+
// Check if the WU exists in the merged state
|
|
313
|
+
const currentState = mergedStore.getWUState(wuId);
|
|
314
|
+
if (!currentState) {
|
|
315
|
+
throw new Error(`WU ${wuId} not found in merged state store. ` +
|
|
316
|
+
`This may indicate the WU was never properly claimed.`);
|
|
317
|
+
}
|
|
318
|
+
// If not already done, create and apply the complete event
|
|
319
|
+
if (currentState.status !== 'done') {
|
|
320
|
+
if (currentState.status !== 'in_progress') {
|
|
321
|
+
throw new Error(`WU ${wuId} is in status "${currentState.status}", expected "in_progress"`);
|
|
322
|
+
}
|
|
323
|
+
const completeEvent = mergedStore.createCompleteEvent(wuId);
|
|
324
|
+
mergedStore.applyEvent(completeEvent);
|
|
325
|
+
}
|
|
326
|
+
// Generate status from merged state
|
|
327
|
+
return generateStatus(mergedStore);
|
|
328
|
+
}
|
|
289
329
|
/**
|
|
290
330
|
* Compute wu-events.jsonl content with merged state from origin/main.
|
|
291
331
|
*
|
package/dist/wu-done-metadata.js
CHANGED
|
@@ -10,7 +10,7 @@ import { updateStatusRemoveInProgress, addToStatusCompleted } from './wu-status-
|
|
|
10
10
|
import { moveWUToDoneBacklog } from './wu-backlog-updater.js';
|
|
11
11
|
import { createStamp } from './stamp-utils.js';
|
|
12
12
|
import { WU_EVENTS_FILE_NAME } from './wu-state-store.js';
|
|
13
|
-
import { computeWUYAMLContent,
|
|
13
|
+
import { computeWUYAMLContent, computeStatusContentFromMergedState, computeBacklogContent, computeWUEventsContentAfterComplete, computeStampContent, } from './wu-transaction-collectors.js';
|
|
14
14
|
import { DEFAULTS, LOG_PREFIX, EMOJI, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS, BEACON_PATHS, } from './wu-constants.js';
|
|
15
15
|
import { applyExposureDefaults } from './wu-done-validation.js';
|
|
16
16
|
import { createFileNotFoundError, createValidationError } from './wu-done-errors.js';
|
|
@@ -139,8 +139,8 @@ export async function collectMetadataToTransaction({ id, title, doc, wuPath, sta
|
|
|
139
139
|
// Compute WU YAML content (mutates doc, returns YAML string)
|
|
140
140
|
const wuYAMLContent = computeWUYAMLContent(doc);
|
|
141
141
|
transaction.addWrite(wuPath, wuYAMLContent, 'WU YAML');
|
|
142
|
-
// Compute status.md content
|
|
143
|
-
const statusContent =
|
|
142
|
+
// Compute status.md content (WU-1319: now uses merged state)
|
|
143
|
+
const statusContent = await computeStatusContentFromMergedState(backlogPath, id);
|
|
144
144
|
transaction.addWrite(statusPath, statusContent, 'status.md');
|
|
145
145
|
// Compute backlog.md content (WU-1574: now async)
|
|
146
146
|
const backlogContent = await computeBacklogContent(backlogPath, id, title);
|
package/dist/wu-paths.d.ts
CHANGED
|
@@ -77,6 +77,21 @@ export declare function createWuPaths(options?: {
|
|
|
77
77
|
* @returns Path to worktrees directory
|
|
78
78
|
*/
|
|
79
79
|
WORKTREES_DIR: () => string;
|
|
80
|
+
/**
|
|
81
|
+
* Get path to plans directory
|
|
82
|
+
* @returns Path to plans directory (WU-1301)
|
|
83
|
+
*/
|
|
84
|
+
PLANS_DIR: () => string;
|
|
85
|
+
/**
|
|
86
|
+
* Get path to templates directory
|
|
87
|
+
* @returns Path to templates directory (WU-1310)
|
|
88
|
+
*/
|
|
89
|
+
TEMPLATES_DIR: () => string;
|
|
90
|
+
/**
|
|
91
|
+
* Get path to onboarding directory
|
|
92
|
+
* @returns Path to onboarding directory (WU-1310)
|
|
93
|
+
*/
|
|
94
|
+
ONBOARDING_DIR: () => string;
|
|
80
95
|
};
|
|
81
96
|
/**
|
|
82
97
|
* Default WU paths using default config
|
|
@@ -130,6 +145,21 @@ export declare const WU_PATHS: {
|
|
|
130
145
|
* @returns Path to worktrees directory
|
|
131
146
|
*/
|
|
132
147
|
WORKTREES_DIR: () => string;
|
|
148
|
+
/**
|
|
149
|
+
* Get path to plans directory
|
|
150
|
+
* @returns Path to plans directory (WU-1301)
|
|
151
|
+
*/
|
|
152
|
+
PLANS_DIR: () => string;
|
|
153
|
+
/**
|
|
154
|
+
* Get path to templates directory
|
|
155
|
+
* @returns Path to templates directory (WU-1310)
|
|
156
|
+
*/
|
|
157
|
+
TEMPLATES_DIR: () => string;
|
|
158
|
+
/**
|
|
159
|
+
* Get path to onboarding directory
|
|
160
|
+
* @returns Path to onboarding directory (WU-1310)
|
|
161
|
+
*/
|
|
162
|
+
ONBOARDING_DIR: () => string;
|
|
133
163
|
};
|
|
134
164
|
/**
|
|
135
165
|
* Generate default worktree path from WU document
|
package/dist/wu-paths.js
CHANGED
|
@@ -104,6 +104,21 @@ export function createWuPaths(options = {}) {
|
|
|
104
104
|
* @returns Path to worktrees directory
|
|
105
105
|
*/
|
|
106
106
|
WORKTREES_DIR: () => config.directories.worktrees,
|
|
107
|
+
/**
|
|
108
|
+
* Get path to plans directory
|
|
109
|
+
* @returns Path to plans directory (WU-1301)
|
|
110
|
+
*/
|
|
111
|
+
PLANS_DIR: () => config.directories.plansDir,
|
|
112
|
+
/**
|
|
113
|
+
* Get path to templates directory
|
|
114
|
+
* @returns Path to templates directory (WU-1310)
|
|
115
|
+
*/
|
|
116
|
+
TEMPLATES_DIR: () => config.directories.templatesDir,
|
|
117
|
+
/**
|
|
118
|
+
* Get path to onboarding directory
|
|
119
|
+
* @returns Path to onboarding directory (WU-1310)
|
|
120
|
+
*/
|
|
121
|
+
ONBOARDING_DIR: () => config.directories.onboardingDir,
|
|
107
122
|
};
|
|
108
123
|
}
|
|
109
124
|
/**
|
|
@@ -50,6 +50,36 @@ export declare function createPreflightResult({ valid, errors, missingCodePaths,
|
|
|
50
50
|
missingTestPaths: any[];
|
|
51
51
|
suggestedTestPaths: {};
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* Validate code_paths files exist
|
|
55
|
+
*
|
|
56
|
+
* WU-1329: Exported for use by wu:create strict validation.
|
|
57
|
+
*
|
|
58
|
+
* @param {string[]} codePaths - List of code paths from WU YAML
|
|
59
|
+
* @param {string} rootDir - Root directory to resolve paths against
|
|
60
|
+
* @returns {{ valid: boolean, errors: string[], missing: string[] }}
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateCodePathsExistence(codePaths: any, rootDir: any): {
|
|
63
|
+
valid: boolean;
|
|
64
|
+
errors: string[];
|
|
65
|
+
missing: any[];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Validate test file paths exist (unit, e2e, integration - not manual)
|
|
69
|
+
*
|
|
70
|
+
* Manual tests are descriptions, not file paths, so they're skipped.
|
|
71
|
+
*
|
|
72
|
+
* WU-1329: Exported for use by wu:create strict validation.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} tests - tests object from WU YAML
|
|
75
|
+
* @param {string} rootDir - Root directory to resolve paths against
|
|
76
|
+
* @returns {{ valid: boolean, errors: string[], missing: string[] }}
|
|
77
|
+
*/
|
|
78
|
+
export declare function validateTestPathsExistence(tests: any, rootDir: any): {
|
|
79
|
+
valid: boolean;
|
|
80
|
+
errors: string[];
|
|
81
|
+
missing: any[];
|
|
82
|
+
};
|
|
53
83
|
/**
|
|
54
84
|
* Run preflight validation for a WU
|
|
55
85
|
*
|