@@ -15,7 +15,7 @@ const { sanitizeContent } = require("./sanitize_content.cjs");
1515 * Module-level state — populated by main(), read by the exported getters below.
1616 * Using module-level variables (rather than closure-only state) allows the handler
1717 * manager to read final output values after all messages have been processed.
18- * @type {Array<{issue_number: number|null, pull_number: number|null, agent: string, owner: string|null, repo: string|null, success: boolean, skipped?: boolean, error?: string}> }
18+ * @type {Array<{issue_number: number|null, pull_number: number|null, agent: string, owner: string|null, repo: string|null, pull_request_repo?: string|null, success: boolean, skipped?: boolean, error?: string}> }
1919 */
2020let _allResults = [ ] ;
2121
@@ -128,6 +128,7 @@ async function main(config = {}) {
128128 // Closure-level state
129129 let processedCount = 0 ;
130130 const agentCache = { } ;
131+ const processedAssignmentTargets = new Set ( ) ;
131132
132133 // Reset module-level results for this handler invocation
133134 _allResults = [ ] ;
@@ -232,10 +233,16 @@ async function main(config = {}) {
232233 const hasExplicitTarget = itemForTarget . issue_number != null || itemForTarget . pull_number != null ;
233234 const effectiveTarget = hasExplicitTarget ? "*" : targetConfig ;
234235
236+ const basePullRequestRepoSlug = pullRequestOwner && pullRequestRepo ? `${ pullRequestOwner } /${ pullRequestRepo } ` : `${ effectiveOwner } /${ effectiveRepo } ` ;
237+
235238 // Handle per-item pull_request_repo override
236239 let effectivePullRequestRepoId = pullRequestRepoId ;
237- if ( message . pull_request_repo ) {
238- const itemPullRequestRepo = String ( message . pull_request_repo ) . trim ( ) ;
240+ let effectivePullRequestRepoSlug = basePullRequestRepoSlug ;
241+ let hasValidatedPerItemPullRequestRepoOverride = false ;
242+ const hasPullRequestRepoOverrideField = message . pull_request_repo != null ;
243+ const trimmedPullRequestRepoOverride = typeof message . pull_request_repo === "string" ? message . pull_request_repo . trim ( ) : "" ;
244+ if ( trimmedPullRequestRepoOverride ) {
245+ const itemPullRequestRepo = trimmedPullRequestRepoOverride ;
239246 const pullRequestRepoParts = itemPullRequestRepo . split ( "/" ) ;
240247 if ( pullRequestRepoParts . length === 2 ) {
241248 const defaultPullRequestRepo = pullRequestRepoConfig || defaultTargetRepo ;
@@ -254,6 +261,8 @@ async function main(config = {}) {
254261 ` ;
255262 const itemPullRequestRepoResponse = await githubClient . graphql ( itemPullRequestRepoQuery , { owner : pullRequestRepoParts [ 0 ] , name : pullRequestRepoParts [ 1 ] } ) ;
256263 effectivePullRequestRepoId = itemPullRequestRepoResponse . repository . id ;
264+ effectivePullRequestRepoSlug = itemPullRequestRepo ;
265+ hasValidatedPerItemPullRequestRepoOverride = true ;
257266 core . info ( `Using per-item pull request repository: ${ itemPullRequestRepo } (ID: ${ effectivePullRequestRepoId } )` ) ;
258267 } catch ( error ) {
259268 const errorMsg = `Failed to fetch pull request repository ID for ${ itemPullRequestRepo } : ${ getErrorMessage ( error ) } ` ;
@@ -264,6 +273,10 @@ async function main(config = {}) {
264273 } else {
265274 core . warning ( `Invalid pull_request_repo format: ${ itemPullRequestRepo } . Expected owner/repo. Using global pull-request-repo if configured.` ) ;
266275 }
276+ } else if ( hasPullRequestRepoOverrideField && typeof message . pull_request_repo === "string" ) {
277+ core . warning ( "Invalid pull_request_repo value. Expected owner/repo. Using global pull-request-repo if configured." ) ;
278+ } else if ( hasPullRequestRepoOverrideField ) {
279+ core . warning ( "Invalid pull_request_repo value. Expected a non-empty owner/repo string. Using global pull-request-repo if configured." ) ;
267280 }
268281
269282 // Resolve the target issue or pull request number from context
@@ -351,14 +364,19 @@ async function main(config = {}) {
351364
352365 core . info ( `${ type } ID: ${ assignableId } ` ) ;
353366
354- const hasPerItemPullRequestRepoOverride = ! ! message . pull_request_repo ;
367+ const assignmentContextKey = `${ effectiveOwner } /${ effectiveRepo } :${ type } :${ number } :${ effectivePullRequestRepoSlug } ` ;
368+ const seenThisContextBefore = processedAssignmentTargets . has ( assignmentContextKey ) ;
369+ // Track assignment context (target + per-item pull_request_repo) to prevent duplicate
370+ // re-assignment calls while still allowing one global issue to fan out to multiple repos.
371+ processedAssignmentTargets . add ( assignmentContextKey ) ;
372+ const shouldAllowReassignment = hasValidatedPerItemPullRequestRepoOverride && ! seenThisContextBefore ;
355373
356374 // Skip if agent is already assigned and no explicit per-item pull_request_repo is specified.
357375 // When a different pull_request_repo is provided on the message, allow re-assignment
358376 // so Copilot can be triggered for a different target repository on the same issue.
359- if ( currentAssignees . some ( a => a . id === agentId ) && ! hasPerItemPullRequestRepoOverride ) {
377+ if ( currentAssignees . some ( a => a . id === agentId ) && ! shouldAllowReassignment ) {
360378 core . info ( `${ agentName } is already assigned to ${ type } #${ number } ` ) ;
361- _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , success : true } ) ;
379+ _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , pull_request_repo : effectivePullRequestRepoSlug , success : true } ) ;
362380 return { success : true } ;
363381 }
364382
@@ -372,7 +390,7 @@ async function main(config = {}) {
372390 if ( ! success ) throw new Error ( `Failed to assign ${ agentName } via GraphQL` ) ;
373391
374392 core . info ( `Successfully assigned ${ agentName } coding agent to ${ type } #${ number } ` ) ;
375- _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , success : true } ) ;
393+ _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , pull_request_repo : effectivePullRequestRepoSlug , success : true } ) ;
376394 return { success : true } ;
377395 } catch ( error ) {
378396 let errorMessage = getErrorMessage ( error ) ;
@@ -382,7 +400,7 @@ async function main(config = {}) {
382400 if ( ignoreIfError && isAuthError ) {
383401 core . warning ( `Agent assignment failed for ${ agentName } on ${ type } #${ number } due to authentication/permission error. Skipping due to ignore-if-error=true.` ) ;
384402 core . info ( `Error details: ${ errorMessage } ` ) ;
385- _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , success : true , skipped : true } ) ;
403+ _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , pull_request_repo : effectivePullRequestRepoSlug , success : true , skipped : true } ) ;
386404 return { success : true , skipped : true } ;
387405 }
388406
@@ -410,7 +428,7 @@ async function main(config = {}) {
410428 core . warning ( `Failed to post failure comment on ${ type } #${ number } : ${ getErrorMessage ( commentError ) } ` ) ;
411429 }
412430
413- _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , success : false , error : errorMessage } ) ;
431+ _allResults . push ( { issue_number : issueNumber , pull_number : pullNumber , agent : agentName , owner : effectiveOwner , repo : effectiveRepo , pull_request_repo : effectivePullRequestRepoSlug , success : false , error : errorMessage } ) ;
414432 return { success : false , error : errorMessage } ;
415433 }
416434 } ;
@@ -475,7 +493,7 @@ async function writeAssignToAgentSummary() {
475493 summaryContent += successResults
476494 . map ( r => {
477495 const itemType = r . issue_number ? `Issue #${ r . issue_number } ` : `Pull Request #${ r . pull_number } ` ;
478- return `- ${ itemType } → Agent: ${ r . agent } ` ;
496+ return `- ${ itemType } → Agent: ${ r . agent } ${ r . pull_request_repo ? ` (PR target: ${ r . pull_request_repo } )` : "" } ` ;
479497 } )
480498 . join ( "\n" ) ;
481499 summaryContent += "\n\n" ;
@@ -486,7 +504,7 @@ async function writeAssignToAgentSummary() {
486504 summaryContent += skippedResults
487505 . map ( r => {
488506 const itemType = r . issue_number ? `Issue #${ r . issue_number } ` : `Pull Request #${ r . pull_number } ` ;
489- return `- ${ itemType } → Agent: ${ r . agent } (assignment failed due to error)` ;
507+ return `- ${ itemType } → Agent: ${ r . agent } ${ r . pull_request_repo ? ` (PR target: ${ r . pull_request_repo } )` : "" } (assignment failed due to error)` ;
490508 } )
491509 . join ( "\n" ) ;
492510 summaryContent += "\n\n" ;
@@ -497,7 +515,7 @@ async function writeAssignToAgentSummary() {
497515 summaryContent += failedResults
498516 . map ( r => {
499517 const itemType = r . issue_number ? `Issue #${ r . issue_number } ` : `Pull Request #${ r . pull_number } ` ;
500- return `- ${ itemType } → Agent: ${ r . agent } : ${ r . error } ` ;
518+ return `- ${ itemType } → Agent: ${ r . agent } ${ r . pull_request_repo ? ` (PR target: ${ r . pull_request_repo } )` : "" } : ${ r . error } ` ;
501519 } )
502520 . join ( "\n" ) ;
503521
0 commit comments