Skip to content

auto-dispatch hijacks interactive PR: re-labels origin:worker, opens competing PR, auto-closes the original #22948

@superdav42

Description

@superdav42

Description

The auto-dispatch worker on a maintainer machine can hijack an interactive PR
opened by the same operator: the dispatcher re-labels the PR from
origin:interactive to origin:worker, treats the PR's title and body as a
worker task, opens a competing implementation branch (feature/auto-DATE-ghN)
plus a competing PR, and merges that competing PR with Resolves #N. This
silently auto-closes the original interactive PR even though the original
contained finished, browser-verified, CI-green work.

The dispatcher appears to enumerate auto-dispatch-eligible targets without
filtering on the GitHub object type — i.e. PRs and Issues are both consumed
as candidate inputs to the worker pipeline. An interactive PR matching the
heuristic is therefore indistinguishable from a worker-ready issue.

Expected Behavior

origin:interactive PRs must be a hard skip in the dispatcher's pre-flight,
regardless of any other heuristic. The pipeline should:

  1. Detect the target is a PR (not an Issue) before re-labelling or assigning.
  2. Detect origin:interactive and refuse to flip it to origin:worker.
  3. Refuse to open a competing implementation against a target that is itself
    a PR (since "implementing a PR" is incoherent — the PR is the
    implementation).

In the live evidence below, all three guards would have prevented the hijack.

Reproducer

Symptom command (the hijack signature is observable from gh):

gh pr view 1108 --repo Ultimate-Multisite/ultimate-multisite \
  --json state,closed,closedAt,mergedAt,mergedBy,mergeCommit

Actual output:

{
  "closed": true,
  "closedAt": "2026-05-05T17:39:17Z",
  "mergeCommit": null,
  "mergedAt": null,
  "mergedBy": null,
  "state": "CLOSED"
}

PR #1108 was closed but never merged. Browse the timeline:

gh api repos/Ultimate-Multisite/ultimate-multisite/issues/1108/events

extracts to:

2026-05-05T17:24:04Z | superdav42 | labeled    | origin:interactive
2026-05-05T17:32:15Z | superdav42 | unlabeled  | origin:interactive
2026-05-05T17:32:15Z | superdav42 | labeled    | status:queued
2026-05-05T17:32:15Z | superdav42 | labeled    | origin:worker
2026-05-05T17:32:16Z | superdav42 | assigned   |
2026-05-05T17:38:21Z | superdav42 | unlabeled  | status:queued
2026-05-05T17:38:22Z | superdav42 | labeled    | status:in-review
2026-05-05T17:39:18Z | superdav42 | closed     |

PR #1108 was opened at 17:23 with origin:interactive. At 17:32:15 (8m 11s
later) the dispatcher unlabelled origin:interactive, applied
status:queued+origin:worker, self-assigned, and at 17:32:23 launched a
deterministic worker (audit comment: "Dispatching worker (deterministic).
Worker PID 217496, Model openai/gpt-5.5, Tier standard, Issue: #1108").

At 17:39:16 the worker merged a new PR #1109 (branch
feature/auto-20260505-113218-gh1108, title GH#1108: fix: secure template switching permission states, body containing Resolves #1108). #1109's
merge auto-closed #1108 at 17:39:17.

Expected output (the desired state):

{
  "closed": false,
  "state": "OPEN",
  "labels": ["origin:interactive", ...]
}

PR #1108 should still be open with its original labels intact, ready for
human review. PR #1109 should never have been created because PR #1108 is
itself a PR, not a dispatchable issue.

Causal code (suspected):

# The dispatcher enumerates auto-dispatch-eligible targets without checking
# the GitHub object type. Examples that need filtering:
rg -n 'gh_issue_list.*auto-dispatch|--label.*auto-dispatch' \
  ~/.aidevops/agents/scripts/pulse-triage-dispatch.sh \
  ~/.aidevops/agents/scripts/dispatch-single-issue-helper.sh \
  ~/.aidevops/agents/scripts/pulse-dispatch-worker-launch.sh

# `gh issue list` returns issues only — but if the dispatcher also accepts
# explicit issue numbers from elsewhere (manual claim, comment trigger,
# label-watcher), it must verify via `gh api repos/{slug}/issues/{N}`
# whether the response object has a `pull_request` key (PR) or not (Issue)
# before performing any write.

The fingerprint of the hijack — re-label origin:interactiveorigin:worker
on a PR, self-assign, then open a sibling feature/auto-DATE-ghN branch —
is unique to this code path. Whichever script emits the audit comment
Dispatching worker (deterministic). ... Issue: #N is the entry point.

Steps to Reproduce

On a maintainer machine where the aidevops auto-dispatch pipeline runs in
the background (e.g. via launchd/cron pulse):

  1. Open an interactive shell session in a project repo.
  2. Open a PR via gh pr create with normal title prefix patterns (fix:,
    feat:, etc.) and any body containing fenced code or a runtime test
    instruction.
  3. Wait 8–15 minutes (the pulse-triage interval).
  4. Observe: the PR's origin:interactive label is removed, origin:worker
    is added, the maintainer is auto-assigned, a worker is dispatched against
    the PR number as if it were an issue, a feature/auto-DATE-ghN branch is
    created, a competing PR is opened with Resolves #N, and the competing
    PR auto-merges (closing the original PR unmerged).

Workarounds Applied

In the observing session (PR #1108#1109 hijack):

  1. Verify functional equivalence after the fact: I diffed the merged
    commit against my closed branch via
    git diff fix/empty-template-switch-page origin/main -- <files> and
    confirmed the worker's version was a clean refactor of mine, so I
    accepted the merged outcome rather than re-opening. Systemic fix: the
    hijack should not happen in the first place; equivalence verification
    is an inappropriate manual cost.

  2. Manual worktree + branch cleanup: after the close, my local worktree
    /home/dave/Git/ultimate-multisite-fix-empty-template-switch/ and local
    branch fix/empty-template-switch-page were stale and caused a
    Cannot redeclare class Automattic\Jetpack\Autoloader PHP-FPM fatal
    (two vendor copies of the autoloader registered the same namespaced
    class). I had to git worktree remove --force, git branch -D,
    git worktree prune, and reload PHP-FPM. Systemic fix candidate: when
    the dispatcher closes an interactive PR, it should at minimum log a
    "your branch was hijacked, here's how to clean up" comment on the
    closed PR — currently it leaves the operator with zero post-mortem
    guidance.

  3. Memory note for future sessions: I stored
    mem_20260505114933_631de325 documenting the symptom and detection
    pattern (mergeCommit:null on the original at the same minute a
    feature/auto-DATE-ghN branch is merged with Resolves #N).

Environment

  • aidevops version: 3.14.67
  • Latest version: 3.14.67
  • Install method: unknown (/home/dave/.local/bin/aidevops)
  • AI Assistant: OpenCode 1.14.33
  • OS: Ubuntu 24.04.4 LTS
  • Shell: bash 5.2.21(1)-release
  • gh CLI: gh version 2.45.0
  • Worker model: openai/gpt-5.5 (per the dispatch audit comment)
  • Worker aidevops version: 3.14.64 (per the dispatch audit comment) —
    i.e. the dispatcher and the worker are on slightly different versions on
    the same machine, suggesting the dispatcher pulls a snapshot before
    aidevops update ran.

Additional Context

The worst-case form of this bug is hijacking a PR whose CI is green and
which the human verified end-to-end against a real browser, in favour of a
worker re-implementation that has not been verified the same way. In this
session I verified PR #1109's behaviour matches PR #1108's via the same
browser test, but that re-verification took ~10 minutes I should not have
needed to spend.

Siblings

These are smaller workarounds the same root cause produced. They could be
filed separately or rolled into the primary fix:

  • No post-hijack guidance comment on the closed interactive PR. When
    the dispatcher closes an interactive PR via the
    Resolves #N-on-competing-PR path, leave an explanatory comment on the
    closed PR pointing the operator at the merged competing PR and at the
    cleanup commands (git worktree remove --force, git branch -D,
    git worktree prune). The current behaviour is silent — the operator
    has to forensically reconstruct what happened from the timeline events
    and audit comments.

  • Dispatcher metadata uses Issue: #N for both issue and PR targets.
    The audit comment format makes it impossible to tell from the comment
    alone whether the dispatcher saw the target as an issue or a PR. Adding
    a type tag (Issue: #N vs PR: #N) would have made this hijack visible
    in the timeline immediately — instead I had to compare gh pr view
    state to merge-commit timestamps to reconstruct the failure.


aidevops.sh v3.14.67 plugin for OpenCode v1.14.33 with claude-opus-4-7 spent 11h 19m and 226,200 tokens on this with the user in an interactive session.

Metadata

Metadata

Assignees

Labels

auto-dispatchAuto-created from TODO.md tagorigin:workerAuto-created by pulse labelless backfill (t2112)status:doneTask is complete

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions