@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.
@@ -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
- for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
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}/${MAX_PUSH_RETRIES})...`);
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 < MAX_PUSH_RETRIES) {
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 ${MAX_PUSH_RETRIES} attempts. ` +
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
- if (!pushOnly) {
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
- if (pushOnly) {
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
- console.log(`${logPrefix} Pushing directly to ${REMOTES.ORIGIN}/${BRANCHES.MAIN} (push-only)...`);
543
- await pushRefspecWithForce(gitWorktree, REMOTES.ORIGIN, tempBranchName, BRANCHES.MAIN, `micro-worktree push for ${operation} (automated)`);
544
- console.log(`${logPrefix} Pushed to ${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
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);
@@ -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
@@ -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
  *
@@ -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, computeStatusContent, computeBacklogContent, computeWUEventsContentAfterComplete, computeStampContent, } from './wu-transaction-collectors.js';
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 = computeStatusContent(statusPath, id, title);
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);
@@ -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
  *