Skip to content

fix(worktree): resume aborts when dependabot force-pushes any open PR branch #120

@oscarvalenzuelab

Description

@oscarvalenzuelab

Repro

  1. ctrlrelay run secops --repo <repo> from CLI manually.
  2. Pipeline returns BLOCKED with a question; row lands in pending_resumes.
  3. Operator answers (via SQL or Telegram bridge).
  4. Poller's _run_pending_resume_sweeper fires resume_secops_from_pending.
  5. Resume calls worktree.ensure_bare_repo(repo)git fetch --prune origin refs/heads/*:refs/heads/*.
  6. Fetch fails because dependabot has rebased an unrelated open PR's branch:
Resume error: git failed: From https://github.com/SemClone/ccda-dashboard
- [deleted]         (none)     -> dependabot/pip/werkzeug-3.1.8
! [rejected]        dependabot/pip/requests-2.33.1 -> dependabot/pip/requests-2.33.1  (non-fast-forward)
  b14bda1..3216dec  main       -> main
  1. Resume aborts; session sits at failed. Operator's answer is never delivered to the agent.

Root cause

ensure_bare_repo (src/ctrlrelay/core/worktree.py:734-779) deliberately uses non-force refspec refs/heads/*:refs/heads/* (no leading +). Per the docstring this is intentional — it preserves unpushed local-ahead commits for the dev pipeline's create_worktree_with_new_branch reuse path (added in 210404d / #106).

The trade-off is that git fetch exits non-zero whenever any branch is non-fast-forward. _run_git raises on non-zero, so a single rebased dependabot branch on an unrelated PR is fatal — even though:

  • The default branch updated cleanly (b14bda1..3216dec main -> main succeeded).
  • The resume only does git worktree add <bare> main (worktree.py:141-146), so it doesn't care about any dependabot branch.

In short: a dev-pipeline safeguard is killing secops resumes on repos that get a steady stream of dependabot activity (i.e., every active repo).

Suggested fixes (in order of preference)

  1. Targeted fetch in the resume path. Have resume_secops_from_pending (and any caller that just needs a fresh default branch) do a narrow git fetch origin <default_branch>:<default_branch> — non-force, but scoped so unrelated rebases don't matter.
  2. Pipeline-aware refspec. secops can tolerate force updates (it never pushes to dependabot branches). Plumb a force=True flag through ensure_bare_repo and have secops use +refs/heads/*:refs/heads/*. Dev keeps the current safe semantics.
  3. Tolerate per-branch rejection in ensure_bare_repo. Parse the fetch stderr, downgrade to a warning if every rejected ref is one we don't care about (i.e., not the default branch), but escalate if the default branch itself rejects. Riskier — silent staleness if logic drifts.

(1) is the smallest blast radius and mirrors how the bare-clone fix in #106 narrowed scope. Happy to send a PR if you want.

Related

Workaround

Rerun the pipeline fresh against current main:

ctrlrelay run secops --repo SemClone/ccda-dashboard

A fresh session creates a new worktree from current default-branch HEAD, so the stale-ref issue doesn't recur — but the operator's earlier BLOCKED answer is lost and the agent has to redo the analysis from scratch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions