From 10f0e7252b5cf0076c2cb95b5e01b507d73cf960 Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Fri, 24 Jul 2026 17:28:32 -0500 Subject: [PATCH 1/3] fix: harden auto-pilot stall handoffs --- .github/workflows/agents-auto-pilot.yml | 58 +++++++++++++++++++ .../.github/workflows/agents-auto-pilot.yml | 58 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/.github/workflows/agents-auto-pilot.yml b/.github/workflows/agents-auto-pilot.yml index e2441272b..ce8593764 100644 --- a/.github/workflows/agents-auto-pilot.yml +++ b/.github/workflows/agents-auto-pilot.yml @@ -2576,6 +2576,37 @@ jobs: core.info(`(agent:${decision.currentAgent} label not present: ${labelErr?.message})`); } } + // A stalled belt item may still carry agent:auto from an earlier + // keepalive handoff. Auto and explicit routing labels are mutually + // exclusive, so remove the former before assigning the next agent. + try { + await withRetry((client) => client.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: 'agent:auto', + })); + } catch (labelErr) { + core.info(`(agent:auto label not present: ${labelErr?.message})`); + } + if (decision.triedMarker) { + try { + await withRetry((client) => client.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: decision.triedMarker, + })); + } catch (labelErr) { + if (labelErr?.status !== 404) throw labelErr; + await withRetry((client) => client.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: decision.triedMarker, + color: '1d76db', + description: 'Records an agent already tried during bounded stall rotation', + })); + } + } await withRetry((client) => client.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, @@ -3195,6 +3226,11 @@ jobs: ISSUE_NUMBER: ${{ steps.context.outputs.issue_number }} STEP_COUNT: ${{ steps.cycles.outputs.count }} MAX_STALL_RETRIES: '5' + HAS_CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON != '' }} + HAS_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }} + HAS_CLAUDE_AUTH_JSON: ${{ secrets.CLAUDE_AUTH_JSON != '' }} + HAS_CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY != '' }} + HAS_GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY != '' }} with: script: | const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); @@ -3266,9 +3302,17 @@ jobs: .map((l) => (typeof l === 'string' ? l : l && l.name)) .filter(Boolean); const hasAuto = rotLabels.some((l) => String(l).trim().toLowerCase() === 'agent:auto'); + const availableSecrets = { + CODEX_AUTH_JSON: process.env.HAS_CODEX_AUTH_JSON === 'true', + CLAUDE_CODE_OAUTH_TOKEN: process.env.HAS_CLAUDE_CODE_OAUTH_TOKEN === 'true', + CLAUDE_AUTH_JSON: process.env.HAS_CLAUDE_AUTH_JSON === 'true', + CURSOR_API_KEY: process.env.HAS_CURSOR_API_KEY === 'true', + GEMINI_API_KEY: process.env.HAS_GEMINI_API_KEY === 'true', + }; const keepaliveAgents = eligibleAgents({ registry: loadAgentRegistry(), capability: 'pr_keepalive', + secrets: availableSecrets, }); if (!hasAuto && keepaliveAgents.length > 1) { // Add agent:auto to the issue AND the PR so the keepalive delegation @@ -3291,6 +3335,20 @@ jobs: core.info(`(could not label PR #${prNumber} with agent:auto: ${prLabelErr?.message})`); } } + const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo, + })); + await withRetry((client) => client.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'agents-81-gate-followups.yml', + ref: repoInfo.default_branch || 'main', + inputs: { + pr_number: String(prNumber), + force_retry: 'true', + }, + })); // Leading "🤖 **Auto-pilot step**" marker resets countConsecutiveWaits() // (see belt-rotation note above) so a later monitor round starts fresh. await withRetry((client) => client.rest.issues.createComment({ diff --git a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml index e2441272b..ce8593764 100644 --- a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml +++ b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml @@ -2576,6 +2576,37 @@ jobs: core.info(`(agent:${decision.currentAgent} label not present: ${labelErr?.message})`); } } + // A stalled belt item may still carry agent:auto from an earlier + // keepalive handoff. Auto and explicit routing labels are mutually + // exclusive, so remove the former before assigning the next agent. + try { + await withRetry((client) => client.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: 'agent:auto', + })); + } catch (labelErr) { + core.info(`(agent:auto label not present: ${labelErr?.message})`); + } + if (decision.triedMarker) { + try { + await withRetry((client) => client.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: decision.triedMarker, + })); + } catch (labelErr) { + if (labelErr?.status !== 404) throw labelErr; + await withRetry((client) => client.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: decision.triedMarker, + color: '1d76db', + description: 'Records an agent already tried during bounded stall rotation', + })); + } + } await withRetry((client) => client.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, @@ -3195,6 +3226,11 @@ jobs: ISSUE_NUMBER: ${{ steps.context.outputs.issue_number }} STEP_COUNT: ${{ steps.cycles.outputs.count }} MAX_STALL_RETRIES: '5' + HAS_CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON != '' }} + HAS_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }} + HAS_CLAUDE_AUTH_JSON: ${{ secrets.CLAUDE_AUTH_JSON != '' }} + HAS_CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY != '' }} + HAS_GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY != '' }} with: script: | const { createTokenAwareRetry } = require('./.github/scripts/github-api-with-retry.js'); @@ -3266,9 +3302,17 @@ jobs: .map((l) => (typeof l === 'string' ? l : l && l.name)) .filter(Boolean); const hasAuto = rotLabels.some((l) => String(l).trim().toLowerCase() === 'agent:auto'); + const availableSecrets = { + CODEX_AUTH_JSON: process.env.HAS_CODEX_AUTH_JSON === 'true', + CLAUDE_CODE_OAUTH_TOKEN: process.env.HAS_CLAUDE_CODE_OAUTH_TOKEN === 'true', + CLAUDE_AUTH_JSON: process.env.HAS_CLAUDE_AUTH_JSON === 'true', + CURSOR_API_KEY: process.env.HAS_CURSOR_API_KEY === 'true', + GEMINI_API_KEY: process.env.HAS_GEMINI_API_KEY === 'true', + }; const keepaliveAgents = eligibleAgents({ registry: loadAgentRegistry(), capability: 'pr_keepalive', + secrets: availableSecrets, }); if (!hasAuto && keepaliveAgents.length > 1) { // Add agent:auto to the issue AND the PR so the keepalive delegation @@ -3291,6 +3335,20 @@ jobs: core.info(`(could not label PR #${prNumber} with agent:auto: ${prLabelErr?.message})`); } } + const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo, + })); + await withRetry((client) => client.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'agents-81-gate-followups.yml', + ref: repoInfo.default_branch || 'main', + inputs: { + pr_number: String(prNumber), + force_retry: 'true', + }, + })); // Leading "🤖 **Auto-pilot step**" marker resets countConsecutiveWaits() // (see belt-rotation note above) so a later monitor round starts fresh. await withRetry((client) => client.rest.issues.createComment({ From 25a193a7a8857fa9ddf9913b4c958cfd234f76e8 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Sat, 25 Jul 2026 16:57:55 -0500 Subject: [PATCH 2/3] fix: complete auto-pilot stall handoff fallbacks --- .github/workflows/agents-auto-pilot.yml | 97 +++++++++++++++---- .../.github/workflows/agents-auto-pilot.yml | 97 +++++++++++++++---- 2 files changed, 152 insertions(+), 42 deletions(-) diff --git a/.github/workflows/agents-auto-pilot.yml b/.github/workflows/agents-auto-pilot.yml index ce8593764..d423b7276 100644 --- a/.github/workflows/agents-auto-pilot.yml +++ b/.github/workflows/agents-auto-pilot.yml @@ -3291,7 +3291,10 @@ jobs: // still stalled, so we escalate. Any error falls through to needs-human. let handedOff = false; try { - const { eligibleAgents } = require('./.github/scripts/agent_stall_rotation.js'); + const { + eligibleAgents, + currentAgentFromLabels, + } = require('./.github/scripts/agent_stall_rotation.js'); const { loadAgentRegistry } = require('./.github/scripts/agent_registry.js'); const { data: rotIssue } = await withRetry((client) => client.rest.issues.get({ owner: context.repo.owner, @@ -3302,19 +3305,48 @@ jobs: .map((l) => (typeof l === 'string' ? l : l && l.name)) .filter(Boolean); const hasAuto = rotLabels.some((l) => String(l).trim().toLowerCase() === 'agent:auto'); - const availableSecrets = { - CODEX_AUTH_JSON: process.env.HAS_CODEX_AUTH_JSON === 'true', - CLAUDE_CODE_OAUTH_TOKEN: process.env.HAS_CLAUDE_CODE_OAUTH_TOKEN === 'true', - CLAUDE_AUTH_JSON: process.env.HAS_CLAUDE_AUTH_JSON === 'true', - CURSOR_API_KEY: process.env.HAS_CURSOR_API_KEY === 'true', - GEMINI_API_KEY: process.env.HAS_GEMINI_API_KEY === 'true', - }; + const rotationRegistry = loadAgentRegistry(); + // Derive the credential surface from the registry. The Actions API + // exposes secret names (not values), so newly registered keepalive + // agents are considered without another provider-specific edit here. + const requiredSecretNames = [...new Set( + Object.values(rotationRegistry.agents || {}) + .filter((config) => config?.capabilities?.pr_keepalive === true) + .flatMap((config) => config.required_secrets || []) + )]; + const availableSecrets = Object.fromEntries( + requiredSecretNames.map((name) => [name, process.env[`HAS_${name}`] === 'true']) + ); + try { + const { data: secretData } = await withRetry((client) => + client.rest.actions.listRepoSecrets({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }) + ); + for (const secret of secretData.secrets || []) { + if (Object.hasOwn(availableSecrets, secret.name)) { + availableSecrets[secret.name] = true; + } + } + } catch (secretLookupError) { + core.warning( + `Could not list repository secret names; using exported availability flags: ` + + `${secretLookupError?.message || secretLookupError}` + ); + } const keepaliveAgents = eligibleAgents({ - registry: loadAgentRegistry(), + registry: rotationRegistry, capability: 'pr_keepalive', secrets: availableSecrets, }); - if (!hasAuto && keepaliveAgents.length > 1) { + const currentAgent = currentAgentFromLabels( + rotLabels, + Object.keys(rotationRegistry.agents || {}) + ) || rotationRegistry.default_agent || ''; + const hasAlternative = keepaliveAgents.some((agent) => agent !== currentAgent); + if (!hasAuto && hasAlternative) { // Add agent:auto to the issue AND the PR so the keepalive delegation // policy picks an alternative agent on its next round. await withRetry((client) => client.rest.issues.addLabels({ @@ -3339,16 +3371,37 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, })); - await withRetry((client) => client.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'agents-81-gate-followups.yml', - ref: repoInfo.default_branch || 'main', - inputs: { - pr_number: String(prNumber), - force_retry: 'true', - }, - })); + let dispatchedWorkflow = ''; + const dispatchFailures = []; + for (const workflowId of [ + 'agents-81-gate-followups.yml', + 'agents-keepalive-loop.yml', + ]) { + try { + await withRetry((client) => client.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowId, + ref: repoInfo.default_branch || 'main', + inputs: { + pr_number: String(prNumber), + force_retry: 'true', + }, + })); + dispatchedWorkflow = workflowId; + break; + } catch (dispatchError) { + dispatchFailures.push( + `${workflowId}: ${dispatchError?.message || dispatchError}` + ); + if (dispatchError?.status !== 404) break; + } + } + if (!dispatchedWorkflow) { + throw new Error( + `No keepalive handoff workflow could be dispatched: ${dispatchFailures.join('; ')}` + ); + } // Leading "🤖 **Auto-pilot step**" marker resets countConsecutiveWaits() // (see belt-rotation note above) so a later monitor round starts fresh. await withRetry((client) => client.rest.issues.createComment({ @@ -3361,7 +3414,9 @@ jobs: `(eligible: ${keepaliveAgents.join(', ')}).\n\n` + `\`needs-human\` is withheld until delegation has also been exhausted.`, })); - core.info(`Monitor-pr stall handed off to delegation policy (agent:auto added)`); + core.info( + `Monitor-pr stall handed off via ${dispatchedWorkflow} (agent:auto added)` + ); handedOff = true; } } catch (rotationErr) { diff --git a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml index ce8593764..d423b7276 100644 --- a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml +++ b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml @@ -3291,7 +3291,10 @@ jobs: // still stalled, so we escalate. Any error falls through to needs-human. let handedOff = false; try { - const { eligibleAgents } = require('./.github/scripts/agent_stall_rotation.js'); + const { + eligibleAgents, + currentAgentFromLabels, + } = require('./.github/scripts/agent_stall_rotation.js'); const { loadAgentRegistry } = require('./.github/scripts/agent_registry.js'); const { data: rotIssue } = await withRetry((client) => client.rest.issues.get({ owner: context.repo.owner, @@ -3302,19 +3305,48 @@ jobs: .map((l) => (typeof l === 'string' ? l : l && l.name)) .filter(Boolean); const hasAuto = rotLabels.some((l) => String(l).trim().toLowerCase() === 'agent:auto'); - const availableSecrets = { - CODEX_AUTH_JSON: process.env.HAS_CODEX_AUTH_JSON === 'true', - CLAUDE_CODE_OAUTH_TOKEN: process.env.HAS_CLAUDE_CODE_OAUTH_TOKEN === 'true', - CLAUDE_AUTH_JSON: process.env.HAS_CLAUDE_AUTH_JSON === 'true', - CURSOR_API_KEY: process.env.HAS_CURSOR_API_KEY === 'true', - GEMINI_API_KEY: process.env.HAS_GEMINI_API_KEY === 'true', - }; + const rotationRegistry = loadAgentRegistry(); + // Derive the credential surface from the registry. The Actions API + // exposes secret names (not values), so newly registered keepalive + // agents are considered without another provider-specific edit here. + const requiredSecretNames = [...new Set( + Object.values(rotationRegistry.agents || {}) + .filter((config) => config?.capabilities?.pr_keepalive === true) + .flatMap((config) => config.required_secrets || []) + )]; + const availableSecrets = Object.fromEntries( + requiredSecretNames.map((name) => [name, process.env[`HAS_${name}`] === 'true']) + ); + try { + const { data: secretData } = await withRetry((client) => + client.rest.actions.listRepoSecrets({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }) + ); + for (const secret of secretData.secrets || []) { + if (Object.hasOwn(availableSecrets, secret.name)) { + availableSecrets[secret.name] = true; + } + } + } catch (secretLookupError) { + core.warning( + `Could not list repository secret names; using exported availability flags: ` + + `${secretLookupError?.message || secretLookupError}` + ); + } const keepaliveAgents = eligibleAgents({ - registry: loadAgentRegistry(), + registry: rotationRegistry, capability: 'pr_keepalive', secrets: availableSecrets, }); - if (!hasAuto && keepaliveAgents.length > 1) { + const currentAgent = currentAgentFromLabels( + rotLabels, + Object.keys(rotationRegistry.agents || {}) + ) || rotationRegistry.default_agent || ''; + const hasAlternative = keepaliveAgents.some((agent) => agent !== currentAgent); + if (!hasAuto && hasAlternative) { // Add agent:auto to the issue AND the PR so the keepalive delegation // policy picks an alternative agent on its next round. await withRetry((client) => client.rest.issues.addLabels({ @@ -3339,16 +3371,37 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, })); - await withRetry((client) => client.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'agents-81-gate-followups.yml', - ref: repoInfo.default_branch || 'main', - inputs: { - pr_number: String(prNumber), - force_retry: 'true', - }, - })); + let dispatchedWorkflow = ''; + const dispatchFailures = []; + for (const workflowId of [ + 'agents-81-gate-followups.yml', + 'agents-keepalive-loop.yml', + ]) { + try { + await withRetry((client) => client.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowId, + ref: repoInfo.default_branch || 'main', + inputs: { + pr_number: String(prNumber), + force_retry: 'true', + }, + })); + dispatchedWorkflow = workflowId; + break; + } catch (dispatchError) { + dispatchFailures.push( + `${workflowId}: ${dispatchError?.message || dispatchError}` + ); + if (dispatchError?.status !== 404) break; + } + } + if (!dispatchedWorkflow) { + throw new Error( + `No keepalive handoff workflow could be dispatched: ${dispatchFailures.join('; ')}` + ); + } // Leading "🤖 **Auto-pilot step**" marker resets countConsecutiveWaits() // (see belt-rotation note above) so a later monitor round starts fresh. await withRetry((client) => client.rest.issues.createComment({ @@ -3361,7 +3414,9 @@ jobs: `(eligible: ${keepaliveAgents.join(', ')}).\n\n` + `\`needs-human\` is withheld until delegation has also been exhausted.`, })); - core.info(`Monitor-pr stall handed off to delegation policy (agent:auto added)`); + core.info( + `Monitor-pr stall handed off via ${dispatchedWorkflow} (agent:auto added)` + ); handedOff = true; } } catch (rotationErr) { From 0d5058845216af9ccfd459d24c634a98e757bdc1 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Fri, 31 Jul 2026 22:25:27 -0500 Subject: [PATCH 3/3] fix: make stalled agent handoffs retryable --- .github/workflows/agents-auto-pilot.yml | 44 ++++++++++--------- .../.github/workflows/agents-auto-pilot.yml | 44 ++++++++++--------- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/.github/workflows/agents-auto-pilot.yml b/.github/workflows/agents-auto-pilot.yml index d423b7276..f3bc975e4 100644 --- a/.github/workflows/agents-auto-pilot.yml +++ b/.github/workflows/agents-auto-pilot.yml @@ -3318,14 +3318,14 @@ jobs: requiredSecretNames.map((name) => [name, process.env[`HAS_${name}`] === 'true']) ); try { - const { data: secretData } = await withRetry((client) => - client.rest.actions.listRepoSecrets({ + const repoSecrets = await withRetry((client) => + client.paginate(client.rest.actions.listRepoSecrets, { owner: context.repo.owner, repo: context.repo.repo, per_page: 100, }) ); - for (const secret of secretData.secrets || []) { + for (const secret of repoSecrets) { if (Object.hasOwn(availableSecrets, secret.name)) { availableSecrets[secret.name] = true; } @@ -3349,24 +3349,6 @@ jobs: if (!hasAuto && hasAlternative) { // Add agent:auto to the issue AND the PR so the keepalive delegation // policy picks an alternative agent on its next round. - await withRetry((client) => client.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: ['agent:auto'], - })); - if (prNumber) { - try { - await withRetry((client) => client.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(prNumber, 10), - labels: ['agent:auto'], - })); - } catch (prLabelErr) { - core.info(`(could not label PR #${prNumber} with agent:auto: ${prLabelErr?.message})`); - } - } const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({ owner: context.repo.owner, repo: context.repo.repo, @@ -3402,6 +3384,26 @@ jobs: `No keepalive handoff workflow could be dispatched: ${dispatchFailures.join('; ')}` ); } + // Switch routing only after a dispatch succeeds, so a transient + // dispatch failure leaves the existing concrete route retryable. + await withRetry((client) => client.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['agent:auto'], + })); + if (prNumber) { + try { + await withRetry((client) => client.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt(prNumber, 10), + labels: ['agent:auto'], + })); + } catch (prLabelErr) { + core.info(`(could not label PR #${prNumber} with agent:auto: ${prLabelErr?.message})`); + } + } // Leading "🤖 **Auto-pilot step**" marker resets countConsecutiveWaits() // (see belt-rotation note above) so a later monitor round starts fresh. await withRetry((client) => client.rest.issues.createComment({ diff --git a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml index d423b7276..f3bc975e4 100644 --- a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml +++ b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml @@ -3318,14 +3318,14 @@ jobs: requiredSecretNames.map((name) => [name, process.env[`HAS_${name}`] === 'true']) ); try { - const { data: secretData } = await withRetry((client) => - client.rest.actions.listRepoSecrets({ + const repoSecrets = await withRetry((client) => + client.paginate(client.rest.actions.listRepoSecrets, { owner: context.repo.owner, repo: context.repo.repo, per_page: 100, }) ); - for (const secret of secretData.secrets || []) { + for (const secret of repoSecrets) { if (Object.hasOwn(availableSecrets, secret.name)) { availableSecrets[secret.name] = true; } @@ -3349,24 +3349,6 @@ jobs: if (!hasAuto && hasAlternative) { // Add agent:auto to the issue AND the PR so the keepalive delegation // policy picks an alternative agent on its next round. - await withRetry((client) => client.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: ['agent:auto'], - })); - if (prNumber) { - try { - await withRetry((client) => client.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(prNumber, 10), - labels: ['agent:auto'], - })); - } catch (prLabelErr) { - core.info(`(could not label PR #${prNumber} with agent:auto: ${prLabelErr?.message})`); - } - } const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({ owner: context.repo.owner, repo: context.repo.repo, @@ -3402,6 +3384,26 @@ jobs: `No keepalive handoff workflow could be dispatched: ${dispatchFailures.join('; ')}` ); } + // Switch routing only after a dispatch succeeds, so a transient + // dispatch failure leaves the existing concrete route retryable. + await withRetry((client) => client.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['agent:auto'], + })); + if (prNumber) { + try { + await withRetry((client) => client.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt(prNumber, 10), + labels: ['agent:auto'], + })); + } catch (prLabelErr) { + core.info(`(could not label PR #${prNumber} with agent:auto: ${prLabelErr?.message})`); + } + } // Leading "🤖 **Auto-pilot step**" marker resets countConsecutiveWaits() // (see belt-rotation note above) so a later monitor round starts fresh. await withRetry((client) => client.rest.issues.createComment({