@@ -856,7 +856,15 @@ export class SSHRuntime implements Runtime {
856856 }
857857
858858 async initWorkspace ( params : WorkspaceInitParams ) : Promise < WorkspaceInitResult > {
859- const { projectPath, branchName, trunkBranch, workspacePath, initLogger, abortSignal } = params ;
859+ const {
860+ projectPath,
861+ branchName,
862+ trunkBranch,
863+ workspacePath,
864+ initLogger,
865+ abortSignal,
866+ pullLatest,
867+ } = params ;
860868
861869 try {
862870 // 1. Sync project to remote (opportunistic rsync with scp fallback)
@@ -906,7 +914,12 @@ export class SSHRuntime implements Runtime {
906914 }
907915 initLogger . logStep ( "Branch checked out successfully" ) ;
908916
909- // 3. Run .mux/init hook if it exists
917+ // 3. Pull latest from origin if requested (best-effort, non-blocking on failure)
918+ if ( pullLatest ) {
919+ await this . pullLatestFromOrigin ( workspacePath , trunkBranch , initLogger , abortSignal ) ;
920+ }
921+
922+ // 4. Run .mux/init hook if it exists
910923 // Note: runInitHook calls logComplete() internally if hook exists
911924 const hookExists = await checkInitHookExists ( projectPath ) ;
912925 if ( hookExists ) {
@@ -928,6 +941,71 @@ export class SSHRuntime implements Runtime {
928941 }
929942 }
930943
944+ /**
945+ * Fetch and rebase on latest origin/<trunkBranch> on remote
946+ * Best-effort operation - logs warnings but doesn't fail workspace initialization
947+ */
948+ private async pullLatestFromOrigin (
949+ workspacePath : string ,
950+ trunkBranch : string ,
951+ initLogger : InitLogger ,
952+ abortSignal ?: AbortSignal
953+ ) : Promise < void > {
954+ try {
955+ initLogger . logStep ( "Fetching latest from origin..." ) ;
956+
957+ // Fetch the trunk branch from origin
958+ const fetchCmd = `git fetch origin ${ shescape . quote ( trunkBranch ) } ` ;
959+ const fetchStream = await this . exec ( fetchCmd , {
960+ cwd : workspacePath ,
961+ timeout : 120 , // 2 minutes for network operation
962+ abortSignal,
963+ } ) ;
964+
965+ const fetchExitCode = await fetchStream . exitCode ;
966+ if ( fetchExitCode !== 0 ) {
967+ const fetchStderr = await streamToString ( fetchStream . stderr ) ;
968+ initLogger . logStderr ( `Warning: Failed to fetch latest (${ fetchStderr } ), continuing anyway` ) ;
969+ return ;
970+ }
971+
972+ initLogger . logStep ( "Rebasing on latest..." ) ;
973+
974+ // Attempt rebase on origin/<trunkBranch>
975+ const rebaseCmd = `git rebase origin/${ shescape . quote ( trunkBranch ) } ` ;
976+ const rebaseStream = await this . exec ( rebaseCmd , {
977+ cwd : workspacePath ,
978+ timeout : 300 , // 5 minutes for rebase
979+ abortSignal,
980+ } ) ;
981+
982+ const [ rebaseStderr , rebaseExitCode ] = await Promise . all ( [
983+ streamToString ( rebaseStream . stderr ) ,
984+ rebaseStream . exitCode ,
985+ ] ) ;
986+
987+ if ( rebaseExitCode !== 0 || rebaseStderr . includes ( "CONFLICT" ) ) {
988+ // Rebase has conflicts or failed - abort and warn
989+ const abortCmd = "git rebase --abort" ;
990+ const abortStream = await this . exec ( abortCmd , {
991+ cwd : workspacePath ,
992+ timeout : 30 ,
993+ abortSignal,
994+ } ) ;
995+ await abortStream . exitCode ;
996+ initLogger . logStderr (
997+ "Warning: Rebase failed due to conflicts, continuing with current state"
998+ ) ;
999+ } else {
1000+ initLogger . logStep ( "Rebased on latest successfully" ) ;
1001+ }
1002+ } catch ( error ) {
1003+ // Non-fatal: log warning and continue
1004+ const errorMsg = getErrorMessage ( error ) ;
1005+ initLogger . logStderr ( `Warning: Failed to pull latest (${ errorMsg } ), continuing anyway` ) ;
1006+ }
1007+ }
1008+
9311009 async renameWorkspace (
9321010 projectPath : string ,
9331011 oldName : string ,
0 commit comments