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:
- Detect the target is a PR (not an Issue) before re-labelling or assigning.
- Detect
origin:interactive and refuse to flip it to origin:worker.
- 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:interactive→origin: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):
- Open an interactive shell session in a project repo.
- 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.
- Wait 8–15 minutes (the pulse-triage interval).
- 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):
-
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.
-
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.
-
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.
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:interactivetoorigin:worker, treats the PR's title and body as aworker task, opens a competing implementation branch (
feature/auto-DATE-ghN)plus a competing PR, and merges that competing PR with
Resolves #N. Thissilently 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 withoutfiltering 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:interactivePRs must be a hard skip in the dispatcher's pre-flight,regardless of any other heuristic. The pipeline should:
origin:interactiveand refuse to flip it toorigin:worker.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):
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:
extracts to:
PR #1108 was opened at 17:23 with
origin:interactive. At 17:32:15 (8m 11slater) the dispatcher unlabelled
origin:interactive, appliedstatus:queued+origin:worker, self-assigned, and at 17:32:23 launched adeterministic 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, titleGH#1108: fix: secure template switching permission states, body containingResolves #1108). #1109'smerge 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 fingerprint of the hijack — re-label
origin:interactive→origin:workeron a PR, self-assign, then open a sibling
feature/auto-DATE-ghNbranch —is unique to this code path. Whichever script emits the audit comment
Dispatching worker (deterministic). ... Issue: #Nis 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):
gh pr createwith normal title prefix patterns (fix:,feat:, etc.) and any body containing fenced code or a runtime testinstruction.
origin:interactivelabel is removed,origin:workeris added, the maintainer is auto-assigned, a worker is dispatched against
the PR number as if it were an issue, a
feature/auto-DATE-ghNbranch iscreated, a competing PR is opened with
Resolves #N, and the competingPR auto-merges (closing the original PR unmerged).
Workarounds Applied
In the observing session (PR #1108 → #1109 hijack):
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>andconfirmed 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.
Manual worktree + branch cleanup: after the close, my local worktree
/home/dave/Git/ultimate-multisite-fix-empty-template-switch/and localbranch
fix/empty-template-switch-pagewere stale and caused aCannot redeclare class Automattic\Jetpack\AutoloaderPHP-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: whenthe 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.
Memory note for future sessions: I stored
mem_20260505114933_631de325documenting the symptom and detectionpattern (
mergeCommit:nullon the original at the same minute afeature/auto-DATE-ghNbranch is merged withResolves #N).Environment
i.e. the dispatcher and the worker are on slightly different versions on
the same machine, suggesting the dispatcher pulls a snapshot before
aidevops updateran.Additional Context
fix: render permission state notices on template-switching page and authorize switch_template AJAX Ultimate-Multisite/ultimate-multisite#1108
GH#1108: fix: secure template switching permission states Ultimate-Multisite/ultimate-multisite#1109
7123cb51Dispatching worker (deterministic). Worker PID: 217496, Model: openai/gpt-5.5, Tier: standard, Issue: #1108. Note the literalIssue: #1108— thedispatcher's metadata format does not distinguish issue from PR.
reimplementation was equivalent), but the failure mode is bad on its own
terms: it discards the human's verification work, opens a competing PR
without notifying the human, and leaves stale local state behind.
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 theclosed 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 operatorhas to forensically reconstruct what happened from the timeline events
and audit comments.
Dispatcher metadata uses
Issue: #Nfor 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: #NvsPR: #N) would have made this hijack visiblein the timeline immediately — instead I had to compare
gh pr viewstate 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.