Skip to content

fix: harden auto-pilot stall handoffs - #2826

Open
stranske wants to merge 2 commits into
mainfrom
fix/autopilot-stall-handoff-safeguards
Open

fix: harden auto-pilot stall handoffs#2826
stranske wants to merge 2 commits into
mainfrom
fix/autopilot-stall-handoff-safeguards

Conversation

@stranske

@stranske stranske commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • remove conflicting agent:auto before belt rotation and provision tried-marker labels
  • limit monitor-PR handoff candidates to agents with available required secrets
  • dispatch the gate-followups workflow before recording a successful handoff
  • keep the consumer template aligned with the source workflow

Validation

  • YAML parse for both workflow copies
  • node --test .github/scripts/__tests__/agent-stall-rotation.test.js
  • actionlint .github/workflows/agents-auto-pilot.yml templates/consumer-repo/.github/workflows/agents-auto-pilot.yml
  • source/template byte parity check

The focused Python workflow tests could not collect in the isolated clone because pytest used an interpreter without PyYAML.

Summary by CodeRabbit

  • Improvements
    • Improved automated handling of stalled pull requests by selecting eligible agents based on available credentials.
    • Preserved rotation state more reliably when assigning alternate agents.
    • Removed conflicting automatic-routing labels during agent handoffs.
    • Added fallback dispatching between supported follow-up workflows, with clearer status reporting when a handoff succeeds or fails.

@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The auto-pilot workflows now preserve stall-rotation state, assess keepalive agents against available credentials and current assignment, and explicitly dispatch ordered keepalive handoff workflows when monitor stalls occur.

Changes

Stall rotation and keepalive handoff

Layer / File(s) Summary
Belt rotation state handling
.github/workflows/agents-auto-pilot.yml, templates/consumer-repo/.github/workflows/agents-auto-pilot.yml
Belt rotation removes agent:auto before explicit routing and creates missing tried-marker labels.
Credential-aware eligibility
.github/workflows/agents-auto-pilot.yml, templates/consumer-repo/.github/workflows/agents-auto-pilot.yml
Monitor-pr exports credential flags, checks repository secrets, filters keepalive agents by required secrets, and compares eligible agents with the current labeled agent.
Explicit keepalive workflow dispatch
.github/workflows/agents-auto-pilot.yml, templates/consumer-repo/.github/workflows/agents-auto-pilot.yml
Monitor stalls dispatch ordered keepalive workflows with fallback handling and log the dispatched workflow identifier.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant monitor-pr
  participant agent_stall_rotation.js
  participant GitHubSecrets
  participant KeepaliveWorkflows
  monitor-pr->>agent_stall_rotation.js: load registry and current agent
  monitor-pr->>GitHubSecrets: verify available repository secrets
  GitHubSecrets-->>monitor-pr: secret names
  monitor-pr->>agent_stall_rotation.js: compute eligible keepalive agents
  agent_stall_rotation.js-->>monitor-pr: eligible alternatives
  monitor-pr->>KeepaliveWorkflows: dispatch ordered handoff workflow
  KeepaliveWorkflows-->>monitor-pr: dispatch result
Loading

Possibly related PRs

  • stranske/Workflows#2814: Updates the same auto-pilot workflow and shared stall-rotation label and keepalive delegation logic.

Suggested labels: github:actions, agents:keepalive, codex

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: hardening auto-pilot stall handoffs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/autopilot-stall-handoff-safeguards

Comment @coderabbitai help to get the list of available commands.

@stranske
stranske temporarily deployed to agent-high-privilege July 24, 2026 22:28 — with GitHub Actions Inactive
@stranske-keepalive

Copy link
Copy Markdown
Contributor

Workflow source needed

PR #2826 needs either a linked GitHub issue or one valid non-issue Workflow Source before PR metadata automation can manage it safely.

Please do one of:

  • Add <!-- meta:issue:123 --> or a normal Closes #123 / Related to #123 line.
  • Check one Workflow Source option in the PR body.
  • Add a hidden marker such as <!-- workflow-source:local_request -->, <!-- workflow-source:manual_remote -->, <!-- workflow-source:review_followup -->, <!-- workflow-source:sync_campaign -->, or <!-- workflow-source:dependabot -->.
  • Add a workflow source label such as workflow:source-direct-pr, workflow:source-local-request, workflow:source-review-followup, workflow:source-sync, or workflow:no-automation.

Once a valid source is present, this warning will not be reposted.

@agents-workflows-bot

agents-workflows-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Automated Status Summary

Head SHA: 1e671dc
Latest Runs: ⏳ pending — Gate
Required contexts: Gate / gate, Health 45 Agents Guard / guard
Required: core tests (3.12): ⏳ pending, core tests (3.13): ⏳ pending, docker smoke: ⏳ pending, gate: ⏳ pending

Workflow / Job Result Logs
(no jobs reported) ⏳ pending

Coverage Overview

  • Coverage history entries: 0

Updated automatically; will refresh on subsequent CI/Docker completions.


Keepalive checklist

Scope

No scope information available

Tasks

  • No tasks defined

Acceptance criteria

  • No acceptance criteria defined

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bdeee7d91e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/agents-auto-pilot.yml Outdated
Comment on lines +3342 to +3346
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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fall back to the follow-up workflow present in this repository

When this stall path runs in the Workflows repository itself, the dispatch targets a workflow that is absent: a repo-wide workflow search finds agents-81-gate-followups.yml only under templates/consumer-repo/, while the root still has .github/workflows/agents-keepalive-loop.yml. The API call therefore fails and the surrounding catch immediately applies needs-human, so monitor-PR handoff never works for this first-party repository; use the same first-available fallback already used by the create-PR path.

Useful? React with 👍 / 👎.

Comment on lines +3229 to +3233
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 != '' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive credential checks from the agent registry

Replace this fixed provider-secret list with registry-derived availability: whenever a new pr_keepalive agent is enabled with a different required_secrets entry, eligibleAgents receives no value for that secret and silently excludes the otherwise configured agent from stall handoffs. This makes auto-pilot routing require another provider-specific workflow edit instead of growing with the registry as required.

AGENTS.md reference: AGENTS.md:L47-L47

Useful? React with 👍 / 👎.

Comment thread .github/workflows/agents-auto-pilot.yml Outdated
Comment on lines 3315 to 3317
secrets: availableSecrets,
});
if (!hasAuto && keepaliveAgents.length > 1) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow handoff when one viable alternative remains

When the stalled current agent is unavailable because its credential was removed while exactly one different agent remains credentialed, the new secret filtering produces a one-element keepaliveAgents array. The following length > 1 guard then skips that valid alternative and escalates to needs-human; determine whether the filtered set contains an agent other than the current one rather than requiring two available agents.

Useful? React with 👍 / 👎.

@stranske
stranske force-pushed the fix/autopilot-stall-handoff-safeguards branch from bdeee7d to cde185e Compare July 25, 2026 21:58
@stranske
stranske temporarily deployed to agent-high-privilege July 25, 2026 21:58 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/agents-auto-pilot.yml:
- Around line 3370-3404: Extract the duplicated ordered workflow-dispatch
fallback logic from the current block and the local
dispatchFirstAvailableWorkflow implementation into a shared helper under
.github/scripts/*.js. Update both github-script steps to import and reuse that
helper, preserving ordered attempts, 404-only fallback behavior, failure
collection, and the existing no-dispatch error semantics.
- Around line 3370-3404: Ensure the agent:auto label update occurs only after
the dispatch loop successfully sets dispatchedWorkflow. Move the unconditional
label-add logic associated with the handoff below the successful dispatch
confirmation, or remove the label when both workflow dispatch attempts fail, so
the outer catch cannot leave agent:auto blocking future retries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b098871-9dde-44fd-a747-f822c7b62d2d

📥 Commits

Reviewing files that changed from the base of the PR and between 9aa4404 and cde185e.

📒 Files selected for processing (2)
  • .github/workflows/agents-auto-pilot.yml
  • templates/consumer-repo/.github/workflows/agents-auto-pilot.yml

Comment on lines +3370 to +3404
const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo,
}));
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('; ')}`
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial

Duplicate "dispatch first available workflow with fallback" logic.

This ordered-dispatch-with-non-404-fallback loop mirrors dispatchFirstAvailableWorkflow already defined locally in the "Execute step - Create PR" step (around line 2322). Since each github-script step is an isolated execution, this can't currently be shared without extracting it into a .github/scripts/*.js helper. Consider factoring this into a shared script to avoid the two implementations drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/agents-auto-pilot.yml around lines 3370 - 3404, Extract
the duplicated ordered workflow-dispatch fallback logic from the current block
and the local dispatchFirstAvailableWorkflow implementation into a shared helper
under .github/scripts/*.js. Update both github-script steps to import and reuse
that helper, preserving ordered attempts, 404-only fallback behavior, failure
collection, and the existing no-dispatch error semantics.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

agent:auto is committed to the issue/PR before the dispatch is confirmed to succeed — a failed dispatch permanently disables this handoff.

The unconditional agent:auto label add (lines 3350-3369, unchanged context) happens before this new dispatch loop. If neither agents-81-gate-followups.yml nor agents-keepalive-loop.yml can be dispatched, the loop throws (line 3400-3404), the outer catch swallows it, and handedOff stays false — but agent:auto is now stuck on the issue (and possibly the PR). On any future run, the guard if (!hasAuto && hasAlternative) at line 3349 will see hasAuto === true and permanently skip this handoff path, even after the underlying dispatch problem (e.g. missing workflow, transient outage) is fixed and auto-pilot is resumed.

Reorder so the labels are only added after a workflow is successfully dispatched, or remove agent:auto on dispatch failure so future stall cycles can retry.

🔧 Proposed reordering
                 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({
-                    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,
                   }));
                   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('; ')}`
                     );
                   }
+                  // Only commit the routing label switch once the handoff workflow
+                  // has actually been dispatched, so a failed dispatch can be retried
+                  // on the next stall cycle instead of permanently blocking on hasAuto.
+                  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})`);
+                    }
+                  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo,
}));
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('; ')}`
);
}
const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo,
}));
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('; ')}`
);
}
// Only commit the routing label switch once the handoff workflow
// has actually been dispatched, so a failed dispatch can be retried
// on the next stall cycle instead of permanently blocking on hasAuto.
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})`);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/agents-auto-pilot.yml around lines 3370 - 3404, Ensure the
agent:auto label update occurs only after the dispatch loop successfully sets
dispatchedWorkflow. Move the unconditional label-add logic associated with the
handoff below the successful dispatch confirmation, or remove the label when
both workflow dispatch attempts fail, so the outer catch cannot leave agent:auto
blocking future retries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants