@@ -72,7 +72,8 @@ function createDeps(input: {
7272 existingBranches ?: string [ ] ;
7373 fetchResult ?: FetchIssuesResult ;
7474 onFetch ?: ( options : { skipCache ?: boolean } | undefined ) => void ;
75- runOneshotForIssue ?: ( issueId : string ) => Promise < void > ;
75+ runOneshotForIssue ?: ( issueId : string ) => Promise < { branch : string } > ;
76+ onOneshotPickedUp ?: ( input : { issue : LinearIssue ; branch : string } ) => Promise < void > ;
7677} = { } ) : {
7778 deps : LinearAutoCreateDependencies ;
7879 created : CreateLifecycleWorktreeInput [ ] ;
@@ -116,6 +117,7 @@ function createDeps(input: {
116117 } ;
117118 } ,
118119 ...( input . runOneshotForIssue ? { runOneshotForIssue : input . runOneshotForIssue } : { } ) ,
120+ ...( input . onOneshotPickedUp ? { onOneshotPickedUp : input . onOneshotPickedUp } : { } ) ,
119121 } ,
120122 } ;
121123}
@@ -360,6 +362,7 @@ describe("runLinearAutoCreateOnce", () => {
360362 issues : [ oneshotIssue , createIssue1 ] ,
361363 runOneshotForIssue : async ( id ) => {
362364 triggered . push ( id ) ;
365+ return { branch : "eng-200-oneshot" } ;
363366 } ,
364367 } ) ;
365368
@@ -369,6 +372,97 @@ describe("runLinearAutoCreateOnce", () => {
369372 expect ( created . map ( ( c ) => c . branch ) ) . toEqual ( [ "eng-201-create" ] ) ;
370373 } ) ;
371374
375+ it ( "does not notify onOneshotPickedUp for the regular `webmux` (create) path" , async ( ) => {
376+ // Regular pickups are user-driven (the user added the label themselves);
377+ // the comment would be noise. Only the autonomous oneshot path gets it.
378+ const issue = createIssue ( ) ;
379+ const pickups : string [ ] = [ ] ;
380+ const { deps } = createDeps ( {
381+ issues : [ issue ] ,
382+ onOneshotPickedUp : async ( { issue : picked } ) => {
383+ pickups . push ( picked . identifier ) ;
384+ } ,
385+ } ) ;
386+
387+ await runLinearAutoCreateOnce ( deps ) ;
388+
389+ expect ( pickups ) . toEqual ( [ ] ) ;
390+ } ) ;
391+
392+ it ( "notifies onOneshotPickedUp with the branch resolved by runOneshotForIssue, not the issue's branchName" , async ( ) => {
393+ // Regression guard: `buildSeedFromLinear` may resolve to an attachment-payload
394+ // or PR branch instead of `issue.branchName`. The pickup-comment contract
395+ // requires the *actual* working branch.
396+ const oneshotIssue = createIssue ( {
397+ id : "issue-oneshot" , identifier : "ENG-200" , branchName : "eng-200-original" ,
398+ labels : [ { name : "webmux_oneshot" , color : "#fff" } ] ,
399+ } ) ;
400+ const pickups : Array < { identifier : string ; branch : string } > = [ ] ;
401+ const { deps } = createDeps ( {
402+ issues : [ oneshotIssue ] ,
403+ runOneshotForIssue : async ( ) => ( { branch : "eng-200-resumed-from-attachment" } ) ,
404+ onOneshotPickedUp : async ( { issue : picked , branch } ) => {
405+ pickups . push ( { identifier : picked . identifier , branch } ) ;
406+ } ,
407+ } ) ;
408+
409+ await runLinearAutoCreateOnce ( deps ) ;
410+
411+ expect ( pickups ) . toEqual ( [
412+ { identifier : "ENG-200" , branch : "eng-200-resumed-from-attachment" } ,
413+ ] ) ;
414+ } ) ;
415+
416+ it ( "does not notify onOneshotPickedUp when the oneshot launch itself fails" , async ( ) => {
417+ const oneshotIssue = createIssue ( {
418+ id : "issue-oneshot" , identifier : "ENG-200" , branchName : "eng-200-oneshot" ,
419+ labels : [ { name : "webmux_oneshot" , color : "#fff" } ] ,
420+ } ) ;
421+ const pickups : string [ ] = [ ] ;
422+ const deps : LinearAutoCreateDependencies = {
423+ lifecycleService : {
424+ async createWorktree ( ) : Promise < { branch : string ; worktreeId : string } > {
425+ throw new Error ( "should not be called" ) ;
426+ } ,
427+ } ,
428+ git : { listWorktrees : ( ) => [ ] } ,
429+ projectRoot : "/repo" ,
430+ fetchIssues : async ( ) => ( { ok : true , data : [ oneshotIssue ] } ) ,
431+ runOneshotForIssue : async ( ) => { throw new Error ( "server unreachable" ) ; } ,
432+ onOneshotPickedUp : async ( { issue : picked } ) => {
433+ pickups . push ( picked . identifier ) ;
434+ } ,
435+ } ;
436+
437+ await runLinearAutoCreateOnce ( deps ) ;
438+
439+ expect ( pickups ) . toEqual ( [ ] ) ;
440+ } ) ;
441+
442+ it ( "swallows onOneshotPickedUp failures so the pickup still completes" , async ( ) => {
443+ const oneshotIssue = createIssue ( {
444+ id : "issue-oneshot" , identifier : "ENG-200" , branchName : "eng-200-oneshot" ,
445+ labels : [ { name : "webmux_oneshot" , color : "#fff" } ] ,
446+ } ) ;
447+ const oneshotCalls : string [ ] = [ ] ;
448+ const { deps } = createDeps ( {
449+ issues : [ oneshotIssue ] ,
450+ runOneshotForIssue : async ( id ) => {
451+ oneshotCalls . push ( id ) ;
452+ return { branch : "eng-200-oneshot" } ;
453+ } ,
454+ onOneshotPickedUp : async ( ) => {
455+ throw new Error ( "Linear comment failed" ) ;
456+ } ,
457+ } ) ;
458+
459+ await runLinearAutoCreateOnce ( deps ) ;
460+ // Pickup still happened and is deduped on the next pass.
461+ await runLinearAutoCreateOnce ( deps ) ;
462+
463+ expect ( oneshotCalls ) . toEqual ( [ "ENG-200" ] ) ;
464+ } ) ;
465+
372466 it ( "skips webmux_oneshot issues when no runOneshotForIssue dep is provided" , async ( ) => {
373467 const oneshotIssue = createIssue ( {
374468 id : "issue-oneshot" , identifier : "ENG-200" , branchName : "eng-200-oneshot" ,
0 commit comments