@@ -336,6 +336,10 @@ export interface WorkspaceSyncDeps {
336336 warn ?: ( message ?: any , ...optionalParams : any [ ] ) => void ;
337337}
338338
339+ export function hasWorkspaceChangesOrNewCommit ( statusOutput : string , baseline ?: string | null , currentHead ?: string | null ) : boolean {
340+ return statusOutput . trim ( ) . length > 0 || currentHead !== baseline ;
341+ }
342+
339343interface AutoCommitFlair {
340344 publishEvent ( event : { kind : string ; summary : string ; detail ?: string ; refId ?: string } ) : Promise < void > ;
341345}
@@ -411,6 +415,74 @@ export async function syncWorkspaceBeforeTask(
411415 console . log ( `[${ config . agentId } ] Workspace synced to origin/${ branch } .` ) ;
412416}
413417
418+ function openPullRequest (
419+ runSync : typeof spawnSync ,
420+ flair : AutoCommitFlair ,
421+ options : AutoCommitOptions ,
422+ authorEmail : string ,
423+ repo : string ,
424+ ) : Promise < void > {
425+ const {
426+ taskId,
427+ branchName,
428+ commitMessage,
429+ prRepo,
430+ ghAgent,
431+ prTitle,
432+ prBody,
433+ authorName,
434+ } = options ;
435+
436+ return ( async ( ) => {
437+ if ( ! options . push || ! options . openPr || ! prRepo ) return ;
438+ const prArgs = [
439+ ghAgent ?? authorEmail . split ( "@" ) [ 0 ] ?? "" ,
440+ "pr" ,
441+ "create" ,
442+ "--repo" ,
443+ prRepo ,
444+ "--head" ,
445+ branchName ,
446+ "--title" , prTitle ?? `task: ${ taskId } ` ,
447+ "--body" ,
448+ prBody ?? commitMessage ,
449+ ] ;
450+ console . log ( `[autoCommit] opening PR: gh-as ${ prArgs [ 0 ] } pr create --repo ${ prRepo } --head ${ branchName } ` ) ;
451+ const prResult = runSync ( "gh-as" , prArgs , { cwd : repo , encoding : "utf-8" } ) ;
452+ const prStdout2 = typeof prResult . stdout === "string" ? prResult . stdout . trim ( ) : "" ;
453+ const prStderr2 = typeof prResult . stderr === "string" ? prResult . stderr . trim ( ) : "" ;
454+ console . log ( `[autoCommit] gh-as pr create exit=${ prResult . status } stdout=${ prStdout2 . slice ( 0 , 200 ) } ` ) ;
455+ if ( prStderr2 ) console . log ( `[autoCommit] gh-as stderr: ${ prStderr2 . slice ( 0 , 200 ) } ` ) ;
456+ if ( ( prResult . status ?? 1 ) === 0 ) {
457+ const prUrl = prStdout2 . trim ( ) ;
458+ const prNumber = prUrl . match ( / \/ p u l l \/ ( \d + ) / ) ?. [ 1 ] ?? "?" ;
459+ if ( options . reviewNotify ?. length && options . mailDir ) {
460+ for ( const reviewer of options . reviewNotify ) {
461+ try {
462+ const { sendMessage } = await import ( "../utils/mail.js" ) ;
463+ sendMessage ( reviewer , `PR #${ prNumber } for review: ${ prUrl } ` , authorName . toLowerCase ( ) ) ;
464+ console . log ( `[autoCommit] Notified reviewer: ${ reviewer } (PR #${ prNumber } )` ) ;
465+ } catch ( notifyErr : any ) {
466+ console . warn ( `[autoCommit] Failed to notify ${ reviewer } : ${ notifyErr . message } ` ) ;
467+ }
468+ }
469+ }
470+ return ;
471+ }
472+
473+ const prErrMsg = prStderr2 || prStdout2 || `exit ${ prResult . status ?? "unknown" } ` ;
474+ try {
475+ await flair . publishEvent ( {
476+ kind : "blocker" ,
477+ summary : `PR creation failed for ${ taskId } ` ,
478+ detail : prErrMsg ,
479+ refId : taskId ,
480+ } ) ;
481+ } catch { /* non-fatal */ }
482+ throw new Error ( `gh-as pr create failed: ${ prErrMsg } ` ) ;
483+ } ) ( ) ;
484+ }
485+
414486export async function runAutoCommit (
415487 config : Pick < CodexRuntimeConfig , "workspace" > ,
416488 flair : AutoCommitFlair ,
@@ -444,6 +516,34 @@ export async function runAutoCommit(
444516 }
445517 }
446518
519+ const statusResult = runSync ( GIT_BIN , [ "status" , "--porcelain" ] , { cwd : repo , encoding : "utf-8" } ) ;
520+ const hasWorkingTreeChanges = ( ( statusResult . stdout ?? "" ) as string ) . trim ( ) . length > 0 ;
521+ const remoteRefCheck = runSync ( GIT_BIN , [ "rev-parse" , "--verify" , `refs/remotes/origin/${ branchName } ` ] , { cwd : repo , encoding : "utf-8" } ) ;
522+ const aheadResult = runSync (
523+ GIT_BIN ,
524+ [ "rev-list" , "--count" , remoteRefCheck . status === 0 ? `origin/${ branchName } ..HEAD` : "HEAD" ] ,
525+ { cwd : repo , encoding : "utf-8" } ,
526+ ) ;
527+ const aheadCount = parseInt ( ( ( aheadResult . stdout ?? "" ) as string ) . trim ( ) , 10 ) ;
528+ const hasExistingCommits = ! Number . isNaN ( aheadCount ) && aheadCount > 0 ;
529+
530+ if ( ! hasWorkingTreeChanges && hasExistingCommits ) {
531+ console . log ( `[autoCommit] reusing existing commits on ${ branchName } ` ) ;
532+ if ( push ) {
533+ const pushResult = runSync ( GIT_BIN , [ "push" , "-u" , "origin" , branchName ] , { cwd : repo , encoding : "utf-8" } ) ;
534+ const pushStdout = typeof pushResult . stdout === "string" ? pushResult . stdout . trim ( ) : "" ;
535+ const pushStderr = typeof pushResult . stderr === "string" ? pushResult . stderr . trim ( ) : "" ;
536+ console . log ( `[autoCommit] git push exit=${ pushResult . status } branch=${ branchName } ` ) ;
537+ if ( pushStdout ) console . log ( `[autoCommit] push stdout: ${ pushStdout . slice ( 0 , 200 ) } ` ) ;
538+ if ( pushStderr ) console . log ( `[autoCommit] push stderr: ${ pushStderr . slice ( 0 , 200 ) } ` ) ;
539+ if ( ( pushResult . status ?? 1 ) !== 0 ) {
540+ throw new Error ( `git push failed: ${ pushStderr || pushStdout || `exit ${ pushResult . status ?? "unknown" } ` } ` ) ;
541+ }
542+ }
543+ await openPullRequest ( runSync , flair , options , authorEmail , repo ) ;
544+ return ;
545+ }
546+
447547 const args = [
448548 "agent" , "commit" ,
449549 "--repo" , repo ,
@@ -461,55 +561,7 @@ export async function runAutoCommit(
461561 if ( resultStdout ) console . log ( `[autoCommit] stdout: ${ resultStdout . slice ( 0 , 200 ) } ` ) ;
462562 if ( resultStderr ) console . log ( `[autoCommit] stderr: ${ resultStderr . slice ( 0 , 200 ) } ` ) ;
463563 if ( result . status === 0 ) {
464- if ( push && openPr && prRepo ) {
465- const prArgs = [
466- ghAgent ?? authorEmail . split ( "@" ) [ 0 ] ?? "" ,
467- "pr" ,
468- "create" ,
469- "--repo" ,
470- prRepo ,
471- "--head" ,
472- branchName ,
473- "--title" , prTitle ?? `task: ${ taskId } ` ,
474- "--body" ,
475- prBody ?? commitMessage ,
476- ] ;
477- console . log ( `[autoCommit] opening PR: gh-as ${ prArgs [ 0 ] } pr create --repo ${ prRepo } --head ${ branchName } ` ) ;
478- const prResult = runSync ( "gh-as" , prArgs , { cwd : repo , encoding : "utf-8" } ) ;
479- const prStdout2 = typeof prResult . stdout === "string" ? prResult . stdout . trim ( ) : "" ;
480- const prStderr2 = typeof prResult . stderr === "string" ? prResult . stderr . trim ( ) : "" ;
481- console . log ( `[autoCommit] gh-as pr create exit=${ prResult . status } stdout=${ prStdout2 . slice ( 0 , 200 ) } ` ) ;
482- if ( prStderr2 ) console . log ( `[autoCommit] gh-as stderr: ${ prStderr2 . slice ( 0 , 200 ) } ` ) ;
483- if ( ( prResult . status ?? 1 ) === 0 ) {
484- const prUrl = prStdout2 . trim ( ) ;
485- const prNumber = prUrl . match ( / \/ p u l l \/ ( \d + ) / ) ?. [ 1 ] ?? "?" ;
486- if ( options . reviewNotify ?. length && options . mailDir ) {
487- for ( const reviewer of options . reviewNotify ) {
488- try {
489- const { sendMessage } = await import ( "../utils/mail.js" ) ;
490- sendMessage ( reviewer , `PR #${ prNumber } for review: ${ prUrl } ` , authorName . toLowerCase ( ) ) ;
491- console . log ( `[autoCommit] Notified reviewer: ${ reviewer } (PR #${ prNumber } )` ) ;
492- } catch ( notifyErr : any ) {
493- console . warn ( `[autoCommit] Failed to notify ${ reviewer } : ${ notifyErr . message } ` ) ;
494- }
495- }
496- }
497- return ;
498- }
499-
500- const prStderr = prStderr2 ;
501- const prStdout = prStdout2 ;
502- const prErrMsg = prStderr || prStdout || `exit ${ prResult . status ?? "unknown" } ` ;
503- try {
504- await flair . publishEvent ( {
505- kind : "blocker" ,
506- summary : `PR creation failed for ${ taskId } ` ,
507- detail : prErrMsg ,
508- refId : taskId ,
509- } ) ;
510- } catch { /* non-fatal */ }
511- throw new Error ( `gh-as pr create failed: ${ prErrMsg } ` ) ;
512- }
564+ await openPullRequest ( runSync , flair , options , authorEmail , repo ) ;
513565 return ;
514566 }
515567
@@ -751,6 +803,7 @@ export async function runCodexRuntime(config: CodexRuntimeConfig): Promise<void>
751803 const flairPub2 = { publishEvent : async ( ev : Record < string , unknown > ) => {
752804 try { await ( flair as any ) . request ( "POST" , "/OrgEvent" , { ...ev , authorId : agentId } ) ; } catch { /* non-fatal */ }
753805 } } ;
806+ const baseline = spawnSync ( GIT_BIN , [ "rev-parse" , "HEAD" ] , { cwd : config . workspace , encoding : "utf-8" } ) . stdout ?. trim ( ) ;
754807 const result = await runCodex ( msg , config , config . taskTimeoutMs ?? 30 * 60 * 1000 , {
755808 flairPublisher : flairPub2 ,
756809 onStall : ( ) => { sendMail ( mailDir , agentId , msg . from , `Task stalled: no Codex output for ${ Math . round ( ( config . watchdogTimeoutMs ?? 300000 ) / 60000 ) } m — process killed. Please resend the task.` ) ; } ,
@@ -799,9 +852,10 @@ export async function runCodexRuntime(config: CodexRuntimeConfig): Promise<void>
799852 console . warn ( `[${ agentId } ] Stale rebase state detected before autoCommit — aborting rebase and proceeding` ) ;
800853 spawnSync ( GIT_BIN , [ "rebase" , "--abort" ] , { cwd : config . workspace , encoding : "utf-8" } ) ;
801854 }
802- // Check for file changes before attempting autoCommit
855+ // Check for file changes or Codex-created commits before attempting autoCommit
803856 const gitStatusResult = spawnSync ( GIT_BIN , [ "status" , "--porcelain" ] , { cwd : config . workspace , encoding : "utf-8" } ) ;
804- const hasChanges = ( gitStatusResult . stdout ?? "" ) . trim ( ) . length > 0 ;
857+ const currentHead = spawnSync ( GIT_BIN , [ "rev-parse" , "HEAD" ] , { cwd : config . workspace , encoding : "utf-8" } ) . stdout ?. trim ( ) ;
858+ const hasChanges = hasWorkspaceChangesOrNewCommit ( gitStatusResult . stdout ?? "" , baseline , currentHead ) ;
805859 if ( ! hasChanges ) {
806860 console . warn ( `[${ agentId } ] Task produced no file changes — skipping autoCommit` ) ;
807861 sendMail ( mailDir , agentId , msg . from ,
0 commit comments